feat(ui-toolkit, explorer): navigation
This commit is contained in:
@@ -1,6 +1,4 @@
|
||||
import classnames from 'classnames';
|
||||
import { NetworkLoader, useInitializeEnv } from '@vegaprotocol/environment';
|
||||
import { Nav } from './components/nav';
|
||||
import { Header } from './components/header';
|
||||
import { Main } from './components/main';
|
||||
import { TendermintWebsocketProvider } from './contexts/websocket/tendermint-websocket-provider';
|
||||
@@ -11,6 +9,7 @@ import {
|
||||
useAssetDetailsDialogStore,
|
||||
} from '@vegaprotocol/assets';
|
||||
import { DEFAULT_CACHE_CONFIG } from '@vegaprotocol/apollo-client';
|
||||
import classNames from 'classnames';
|
||||
|
||||
const DialogsContainer = () => {
|
||||
const { isOpen, id, trigger, asJson, setOpen } = useAssetDetailsDialogStore();
|
||||
@@ -25,35 +24,42 @@ const DialogsContainer = () => {
|
||||
);
|
||||
};
|
||||
|
||||
function App() {
|
||||
const layoutClasses = classnames(
|
||||
'grid grid-rows-[auto_1fr_auto] grid-cols-[1fr] md:grid-rows-[auto_minmax(700px,_1fr)_auto] md:grid-cols-[300px_1fr]',
|
||||
'min-h-[100vh] mx-auto my-0',
|
||||
'border-neutral-700 dark:border-neutral-300 lg:border-l lg:border-r',
|
||||
'bg-white dark:bg-black',
|
||||
'antialiased text-black dark:text-white',
|
||||
'overflow-hidden relative'
|
||||
);
|
||||
const MainnetSimAd = () => (
|
||||
<AnnouncementBanner>
|
||||
<div className="font-alpha calt uppercase text-center text-lg text-white">
|
||||
<span className="pr-4">Mainnet sim 2 is live!</span>
|
||||
<ExternalLink href="https://fairground.wtf/">
|
||||
Come help stress test the network
|
||||
</ExternalLink>
|
||||
</div>
|
||||
</AnnouncementBanner>
|
||||
);
|
||||
|
||||
function App() {
|
||||
return (
|
||||
<TendermintWebsocketProvider>
|
||||
<NetworkLoader cache={DEFAULT_CACHE_CONFIG}>
|
||||
<AnnouncementBanner>
|
||||
<div className="font-alpha calt uppercase text-center text-lg text-white">
|
||||
<span className="pr-4">Mainnet sim 2 is live!</span>
|
||||
<ExternalLink href="https://fairground.wtf/">
|
||||
Come help stress test the network
|
||||
</ExternalLink>
|
||||
<div
|
||||
className={classNames(
|
||||
'max-w-[1500px] min-h-[100vh]',
|
||||
'mx-auto my-0',
|
||||
'grid grid-rows-[auto_1fr_auto] grid-cols-1',
|
||||
'border-vega-light-200 dark:border-vega-dark-200 lg:border-l lg:border-r',
|
||||
'antialiased text-black dark:text-white',
|
||||
'overflow-hidden relative'
|
||||
)}
|
||||
>
|
||||
<div>
|
||||
<MainnetSimAd />
|
||||
<Header />
|
||||
</div>
|
||||
<div>
|
||||
<Main />
|
||||
</div>
|
||||
<div>
|
||||
<Footer />
|
||||
</div>
|
||||
</AnnouncementBanner>
|
||||
|
||||
<div className={layoutClasses}>
|
||||
<Header />
|
||||
<Nav />
|
||||
<Main />
|
||||
<Footer />
|
||||
</div>
|
||||
|
||||
<DialogsContainer />
|
||||
</NetworkLoader>
|
||||
</TendermintWebsocketProvider>
|
||||
|
||||
@@ -16,7 +16,7 @@ export const Footer = () => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<footer className="grid grid-rows-2 grid-cols-[1fr_auto] text-xs md:text-md md:flex md:col-span-2 px-4 py-2 gap-4 border-t border-neutral-700 dark:border-neutral-300">
|
||||
<footer className="grid grid-rows-2 grid-cols-[1fr_auto] text-xs md:text-md md:flex md:col-span-2 px-4 py-2 gap-4 border-t border-vega-light-200 dark:border-vega-dark-200">
|
||||
<div className="flex justify-between gap-2 align-middle">
|
||||
{GIT_COMMIT_HASH && (
|
||||
<div className="content-center flex border-r border-neutral-700 dark:border-neutral-300 pr-4">
|
||||
|
||||
@@ -19,12 +19,10 @@ const renderComponent = () => (
|
||||
);
|
||||
|
||||
describe('Header', () => {
|
||||
it('should render heading', () => {
|
||||
it('should render navigation', () => {
|
||||
render(renderComponent());
|
||||
|
||||
expect(screen.getByTestId('explorer-header')).toHaveTextContent(
|
||||
'Vega Explorer'
|
||||
);
|
||||
expect(screen.getByTestId('navigation')).toHaveTextContent('Explorer');
|
||||
});
|
||||
it('should render search', () => {
|
||||
render(renderComponent());
|
||||
|
||||
@@ -1,43 +1,90 @@
|
||||
import classnames from 'classnames';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { ThemeSwitcher, Icon } from '@vegaprotocol/ui-toolkit';
|
||||
import { matchPath, useLocation } from 'react-router-dom';
|
||||
import {
|
||||
ThemeSwitcher,
|
||||
Navigation,
|
||||
NavigationList,
|
||||
NavigationItem,
|
||||
NavigationLink,
|
||||
NavigationBreakpoint,
|
||||
NavigationTrigger,
|
||||
NavigationContent,
|
||||
} from '@vegaprotocol/ui-toolkit';
|
||||
import { t } from '@vegaprotocol/i18n';
|
||||
import { Search } from '../search';
|
||||
import { Routes } from '../../routes/route-names';
|
||||
import { NetworkSwitcher } from '@vegaprotocol/environment';
|
||||
import { useNavStore } from '../nav';
|
||||
import type { Navigable } from '../../routes/router-config';
|
||||
import routerConfig from '../../routes/router-config';
|
||||
import { useMemo } from 'react';
|
||||
import compact from 'lodash/compact';
|
||||
import { Search } from '../search';
|
||||
|
||||
const routeToNavigationItem = (r: Navigable) => (
|
||||
<NavigationItem key={r.name}>
|
||||
<NavigationLink to={r.path}>{r.text}</NavigationLink>
|
||||
</NavigationItem>
|
||||
);
|
||||
|
||||
export const Header = () => {
|
||||
const [open, toggle] = useNavStore((state) => [state.open, state.toggle]);
|
||||
const headerClasses = classnames(
|
||||
'md:col-span-2',
|
||||
'grid grid-rows-2 md:grid-rows-1 grid-cols-[1fr_auto] md:grid-cols-[auto_1fr_auto] items-center',
|
||||
'p-4 gap-2 md:gap-4',
|
||||
'border-b border-neutral-700 dark:border-neutral-300 bg-black',
|
||||
'dark text-white'
|
||||
const mainItems = compact(
|
||||
[Routes.TX, Routes.BLOCKS, Routes.ORACLES, Routes.VALIDATORS].map((n) =>
|
||||
routerConfig.find((r) => r.path === n)
|
||||
)
|
||||
);
|
||||
|
||||
const groupedItems = compact(
|
||||
[
|
||||
Routes.PARTIES,
|
||||
Routes.ASSETS,
|
||||
Routes.MARKETS,
|
||||
Routes.GOVERNANCE,
|
||||
Routes.NETWORK_PARAMETERS,
|
||||
Routes.GENESIS,
|
||||
].map((n) => routerConfig.find((r) => r.path === n))
|
||||
);
|
||||
|
||||
const { pathname } = useLocation();
|
||||
const isOnOther = useMemo(() => {
|
||||
for (const path of groupedItems.map((r) => r.path)) {
|
||||
const matched = matchPath(`${path}/*`, pathname);
|
||||
if (matched) return true;
|
||||
}
|
||||
return false;
|
||||
}, [groupedItems, pathname]);
|
||||
|
||||
return (
|
||||
<header className={headerClasses}>
|
||||
<div className="flex h-full items-center sm:items-stretch gap-4">
|
||||
<Link to={Routes.HOME}>
|
||||
<h1
|
||||
className="text-white text-3xl font-alpha uppercase calt mb-0"
|
||||
data-testid="explorer-header"
|
||||
>
|
||||
{t('Vega Explorer')}
|
||||
</h1>
|
||||
</Link>
|
||||
<NetworkSwitcher />
|
||||
</div>
|
||||
<button
|
||||
data-testid="open-menu"
|
||||
className="md:hidden text-white"
|
||||
onClick={() => toggle()}
|
||||
<Navigation
|
||||
appName="Explorer"
|
||||
theme="system"
|
||||
breakpoints={[400, 800]}
|
||||
actions={
|
||||
<>
|
||||
<ThemeSwitcher />
|
||||
<Search />
|
||||
</>
|
||||
}
|
||||
>
|
||||
<NavigationList hide={[NavigationBreakpoint.Small]}>
|
||||
<NavigationItem>
|
||||
<NetworkSwitcher />
|
||||
</NavigationItem>
|
||||
</NavigationList>
|
||||
<NavigationList
|
||||
hide={[NavigationBreakpoint.Small, NavigationBreakpoint.Narrow]}
|
||||
>
|
||||
<Icon name={open ? 'cross' : 'menu'} />
|
||||
</button>
|
||||
<Search />
|
||||
<ThemeSwitcher className="-my-4" />
|
||||
</header>
|
||||
{mainItems.map(routeToNavigationItem)}
|
||||
{groupedItems && (
|
||||
<NavigationItem>
|
||||
<NavigationTrigger isActive={Boolean(isOnOther)}>
|
||||
{t('Other')}
|
||||
</NavigationTrigger>
|
||||
<NavigationContent>
|
||||
<NavigationList>
|
||||
{groupedItems.map(routeToNavigationItem)}
|
||||
</NavigationList>
|
||||
</NavigationContent>
|
||||
</NavigationItem>
|
||||
)}
|
||||
</NavigationList>
|
||||
</Navigation>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import { useCallback, useState } from 'react';
|
||||
import { t } from '@vegaprotocol/i18n';
|
||||
import { Button, Input, InputError } from '@vegaprotocol/ui-toolkit';
|
||||
import { Button, Icon, Input, InputError } from '@vegaprotocol/ui-toolkit';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { getSearchType, SearchTypes, toHex } from './detect-search';
|
||||
import { Routes } from '../../routes/route-names';
|
||||
import * as DropdownMenu from '@radix-ui/react-dropdown-menu';
|
||||
import classNames from 'classnames';
|
||||
|
||||
interface FormFields {
|
||||
search: string;
|
||||
@@ -49,39 +51,91 @@ export const Search = () => {
|
||||
[navigate]
|
||||
);
|
||||
|
||||
return (
|
||||
<form
|
||||
onSubmit={handleSubmit(onSubmit)}
|
||||
className="w-full md:max-w-[620px] justify-self-end"
|
||||
>
|
||||
<label htmlFor="search" className="sr-only">
|
||||
{t('Search by block number or transaction hash')}
|
||||
</label>
|
||||
<div className="flex items-stretch gap-2">
|
||||
<div className="flex grow relative">
|
||||
<Input
|
||||
{...register('search')}
|
||||
id="search"
|
||||
data-testid="search"
|
||||
className="text-white"
|
||||
hasError={Boolean(error?.message)}
|
||||
type="text"
|
||||
placeholder={t(
|
||||
'Enter block number, public key or transaction hash'
|
||||
)}
|
||||
/>
|
||||
{error?.message && (
|
||||
<div className="bg-white border border-t-0 border-accent absolute top-[100%] flex-1 w-full pb-2 px-2 rounded-b text-black">
|
||||
<InputError data-testid="search-error" intent="danger">
|
||||
{error.message}
|
||||
</InputError>
|
||||
</div>
|
||||
const searchForm = (
|
||||
<form onSubmit={handleSubmit(onSubmit)}>
|
||||
<div className="flex relative items-stretch gap-2 text-xs">
|
||||
<label htmlFor="search" className="sr-only">
|
||||
{t('Search by block number or transaction hash')}
|
||||
</label>
|
||||
<button
|
||||
className={classNames(
|
||||
'absolute top-[50%] translate-y-[-50%] left-2',
|
||||
'text-vega-light-300 dark:text-vega-dark-300'
|
||||
)}
|
||||
</div>
|
||||
<Button type="submit" size="sm" data-testid="search-button">
|
||||
>
|
||||
<Icon name="search" />
|
||||
</button>
|
||||
<Input
|
||||
{...register('search')}
|
||||
id="search"
|
||||
data-testid="search"
|
||||
className={classNames(
|
||||
'peer',
|
||||
'pl-8 py-2 text-xs',
|
||||
'border rounded border-vega-light-200 dark:border-vega-dark-200'
|
||||
)}
|
||||
hasError={Boolean(error?.message)}
|
||||
type="text"
|
||||
placeholder={t('Enter block number, public key or transaction hash')}
|
||||
/>
|
||||
{error?.message && (
|
||||
<div
|
||||
className={classNames(
|
||||
'hidden peer-focus:block',
|
||||
'bg-white dark:bg-black',
|
||||
'border rounded-b border-t-0 border-vega-light-200 dark:border-vega-dark-200',
|
||||
'absolute top-[100%] flex-1 w-full pb-2 px-2 text-black dark:text-white'
|
||||
)}
|
||||
>
|
||||
<InputError
|
||||
data-testid="search-error"
|
||||
intent="danger"
|
||||
className="text-xs"
|
||||
>
|
||||
{error.message}
|
||||
</InputError>
|
||||
</div>
|
||||
)}
|
||||
<Button
|
||||
className="hidden [.search-dropdown_&]:block"
|
||||
type="submit"
|
||||
size="xs"
|
||||
data-testid="search-button"
|
||||
>
|
||||
{t('Search')}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="[.nav-size-narrow_&]:hidden [.nav-size-small_&]:hidden min-w-[290px]">
|
||||
{searchForm}
|
||||
</div>
|
||||
<DropdownMenu.Root>
|
||||
<DropdownMenu.Trigger asChild>
|
||||
<button className="mt-[-1px] text-vega-light-300 dark:text-vega-dark-300 data-open:text-black dark:data-open:text-white [.nav-size-full_&]:hidden">
|
||||
<Icon name="search" size={4} />
|
||||
</button>
|
||||
</DropdownMenu.Trigger>
|
||||
<DropdownMenu.Portal>
|
||||
<DropdownMenu.Content
|
||||
className={classNames(
|
||||
'search-dropdown',
|
||||
'p-2 min-w-[290px] z-20',
|
||||
'text-vega-light-300 dark:text-vega-dark-300',
|
||||
'bg-white dark:bg-black',
|
||||
'border rounded border-vega-light-200 dark:border-vega-dark-200',
|
||||
'shadow-[8px_8px_16px_0_rgba(0,0,0,0.4)]'
|
||||
)}
|
||||
align="end"
|
||||
sideOffset={10}
|
||||
>
|
||||
{searchForm}
|
||||
</DropdownMenu.Content>
|
||||
</DropdownMenu.Portal>
|
||||
</DropdownMenu.Root>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -23,14 +23,18 @@ import { NetworkParameters } from './network-parameters';
|
||||
import type { RouteObject } from 'react-router-dom';
|
||||
import { MarketPage, MarketsPage } from './markets';
|
||||
|
||||
export type Navigable = { path: string; name: string; text: string };
|
||||
export type Navigable = {
|
||||
path: string;
|
||||
name: string;
|
||||
text: string;
|
||||
};
|
||||
type Route = RouteObject & Navigable;
|
||||
|
||||
const partiesRoutes: Route[] = flags.parties
|
||||
? [
|
||||
{
|
||||
path: Routes.PARTIES,
|
||||
name: 'Parties',
|
||||
name: t('Parties'),
|
||||
text: t('Parties'),
|
||||
element: <Party />,
|
||||
children: [
|
||||
@@ -52,7 +56,7 @@ const assetsRoutes: Route[] = flags.assets
|
||||
{
|
||||
path: Routes.ASSETS,
|
||||
text: t('Assets'),
|
||||
name: 'Assets',
|
||||
name: t('Assets'),
|
||||
children: [
|
||||
{
|
||||
index: true,
|
||||
@@ -71,7 +75,7 @@ const genesisRoutes: Route[] = flags.genesis
|
||||
? [
|
||||
{
|
||||
path: Routes.GENESIS,
|
||||
name: 'Genesis',
|
||||
name: t('Genesis'),
|
||||
text: t('Genesis Parameters'),
|
||||
element: <Genesis />,
|
||||
},
|
||||
@@ -82,7 +86,7 @@ const governanceRoutes: Route[] = flags.governance
|
||||
? [
|
||||
{
|
||||
path: Routes.GOVERNANCE,
|
||||
name: 'Governance proposals',
|
||||
name: t('Governance proposals'),
|
||||
text: t('Governance Proposals'),
|
||||
element: <Proposals />,
|
||||
},
|
||||
@@ -93,7 +97,7 @@ const marketsRoutes: Route[] = flags.markets
|
||||
? [
|
||||
{
|
||||
path: Routes.MARKETS,
|
||||
name: 'Markets',
|
||||
name: t('Markets'),
|
||||
text: t('Markets'),
|
||||
children: [
|
||||
{
|
||||
@@ -113,17 +117,18 @@ const networkParametersRoutes: Route[] = flags.networkParameters
|
||||
? [
|
||||
{
|
||||
path: Routes.NETWORK_PARAMETERS,
|
||||
name: 'NetworkParameters',
|
||||
name: t('NetworkParameters'),
|
||||
text: t('Network Parameters'),
|
||||
element: <NetworkParameters />,
|
||||
},
|
||||
]
|
||||
: [];
|
||||
|
||||
const validators: Route[] = flags.validators
|
||||
? [
|
||||
{
|
||||
path: Routes.VALIDATORS,
|
||||
name: 'Validators',
|
||||
name: t('Validators'),
|
||||
text: t('Validators'),
|
||||
element: <ValidatorsPage />,
|
||||
},
|
||||
@@ -133,14 +138,14 @@ const validators: Route[] = flags.validators
|
||||
const routerConfig: Route[] = [
|
||||
{
|
||||
path: Routes.HOME,
|
||||
name: 'Home',
|
||||
name: t('Home'),
|
||||
text: t('Home'),
|
||||
element: <Home />,
|
||||
index: true,
|
||||
},
|
||||
{
|
||||
path: Routes.TX,
|
||||
name: 'Txs',
|
||||
name: t('Txs'),
|
||||
text: t('Transactions'),
|
||||
element: <Txs />,
|
||||
children: [
|
||||
@@ -160,7 +165,7 @@ const routerConfig: Route[] = [
|
||||
},
|
||||
{
|
||||
path: Routes.BLOCKS,
|
||||
name: 'Blocks',
|
||||
name: t('Blocks'),
|
||||
text: t('Blocks'),
|
||||
element: <BlockPage />,
|
||||
children: [
|
||||
@@ -176,7 +181,7 @@ const routerConfig: Route[] = [
|
||||
},
|
||||
{
|
||||
path: Routes.ORACLES,
|
||||
name: 'Oracles',
|
||||
name: t('Oracles'),
|
||||
text: t('Oracles'),
|
||||
element: <OraclePage />,
|
||||
children: [
|
||||
|
||||
@@ -3,3 +3,13 @@
|
||||
// expect(element).toHaveTextContent(/react/i)
|
||||
// learn more: https://github.com/testing-library/jest-dom
|
||||
import '@testing-library/jest-dom';
|
||||
|
||||
Object.defineProperty(window, 'ResizeObserver', {
|
||||
writable: false,
|
||||
value: jest.fn().mockImplementation(() => ({
|
||||
observe: jest.fn(),
|
||||
unobserve: jest.fn(),
|
||||
connect: jest.fn(),
|
||||
disconnect: jest.fn(),
|
||||
})),
|
||||
});
|
||||
|
||||
@@ -80,7 +80,13 @@ const NetworkLabel = ({
|
||||
</span>
|
||||
);
|
||||
|
||||
export const NetworkSwitcher = () => {
|
||||
type NetworkSwitcherProps = {
|
||||
/**
|
||||
* The current network identifier, defaults to the `VEGA_ENV` if unset.
|
||||
*/
|
||||
currentNetwork?: Networks;
|
||||
};
|
||||
export const NetworkSwitcher = ({ currentNetwork }: NetworkSwitcherProps) => {
|
||||
const { VEGA_ENV, VEGA_NETWORKS } = useEnvironment();
|
||||
const tokenLink = useLinks(DApp.Token);
|
||||
const [isOpen, setOpen] = useState(false);
|
||||
@@ -97,6 +103,8 @@ export const NetworkSwitcher = () => {
|
||||
);
|
||||
const menuRef = useRef<HTMLButtonElement | null>(null);
|
||||
|
||||
const current = currentNetwork || VEGA_ENV;
|
||||
|
||||
return (
|
||||
<DropdownMenu
|
||||
open={isOpen}
|
||||
@@ -106,7 +114,7 @@ export const NetworkSwitcher = () => {
|
||||
ref={menuRef}
|
||||
className="flex justify-between items-center"
|
||||
>
|
||||
{envTriggerMapping[VEGA_ENV]}
|
||||
{envTriggerMapping[current]}
|
||||
</DropdownMenuTrigger>
|
||||
}
|
||||
>
|
||||
@@ -125,7 +133,7 @@ export const NetworkSwitcher = () => {
|
||||
<a href={VEGA_NETWORKS[key]}>
|
||||
{envNameMapping[key]}
|
||||
<NetworkLabel
|
||||
isCurrent={VEGA_ENV === key}
|
||||
isCurrent={current === key}
|
||||
isAvailable={!!VEGA_NETWORKS[key]}
|
||||
/>
|
||||
</a>
|
||||
@@ -150,7 +158,7 @@ export const NetworkSwitcher = () => {
|
||||
<div className="mr-4">
|
||||
<Link href={VEGA_NETWORKS[key]}>{envNameMapping[key]}</Link>
|
||||
<NetworkLabel
|
||||
isCurrent={VEGA_ENV === key}
|
||||
isCurrent={current === key}
|
||||
isAvailable={!!VEGA_NETWORKS[key]}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -165,5 +165,6 @@ module.exports = {
|
||||
},
|
||||
data: {
|
||||
selected: 'state~="checked"',
|
||||
open: 'state~="open"',
|
||||
},
|
||||
};
|
||||
|
||||
@@ -31,7 +31,7 @@ export const Default = Template.bind({});
|
||||
Default.args = {
|
||||
open: false,
|
||||
children: (
|
||||
<p className="h-full bg-black dark:bg-white text-white dark:text-black">
|
||||
<p className="h-full bg-white dark:bg-black text-black dark:text-white">
|
||||
Some content
|
||||
</p>
|
||||
),
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import type { ReactNode } from 'react';
|
||||
import { createContext } from 'react';
|
||||
import classNames from 'classnames';
|
||||
import * as DialogPrimitives from '@radix-ui/react-dialog';
|
||||
import { Icon } from '../icon';
|
||||
import { create } from 'zustand';
|
||||
|
||||
interface DrawerProps {
|
||||
children: ReactNode;
|
||||
@@ -15,6 +17,19 @@ interface DrawerProps {
|
||||
trigger?: ReactNode;
|
||||
}
|
||||
|
||||
export const DrawerContext = createContext<true | undefined>(undefined);
|
||||
|
||||
type DrawerStore = {
|
||||
drawerOpen: boolean;
|
||||
setDrawerOpen: (isOpen: boolean) => void;
|
||||
};
|
||||
export const useDrawer = create<DrawerStore>((set) => ({
|
||||
drawerOpen: false,
|
||||
setDrawerOpen: (isOpen) => {
|
||||
set({ drawerOpen: isOpen });
|
||||
},
|
||||
}));
|
||||
|
||||
export function Drawer({
|
||||
children,
|
||||
open,
|
||||
@@ -27,11 +42,12 @@ export function Drawer({
|
||||
trigger,
|
||||
}: DrawerProps) {
|
||||
const contentClasses = classNames(
|
||||
'group/drawer',
|
||||
'h-full max-w-[500px] w-[90vw] z-10 top-0 right-0 fixed transition-transform',
|
||||
className,
|
||||
{
|
||||
'translate-x-[100%]': !open,
|
||||
'translate-x-0 z-40': open,
|
||||
'translate-x-0 z-20': open,
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
@@ -73,7 +73,7 @@ export const DropdownMenuContent = forwardRef<
|
||||
<DropdownMenuPrimitive.Content
|
||||
{...contentProps}
|
||||
ref={forwardedRef}
|
||||
className="min-w-[290px] bg-neutral-200 dark:bg-white p-2 rounded text-black"
|
||||
className="min-w-[290px] bg-neutral-200 dark:bg-white p-2 rounded z-20"
|
||||
align="start"
|
||||
sideOffset={10}
|
||||
/>
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
export * from './accordion';
|
||||
export * from './announcement-banner';
|
||||
export * from './arrows';
|
||||
export * from './async-renderer';
|
||||
export * from './background-video';
|
||||
export * from './announcement-banner';
|
||||
export * from './button';
|
||||
export * from './callout';
|
||||
export * from './checkbox';
|
||||
@@ -19,12 +19,16 @@ export * from './key-value-table';
|
||||
export * from './link';
|
||||
export * from './loader';
|
||||
export * from './lozenge';
|
||||
export * from './nav';
|
||||
export * from './maintenance-page';
|
||||
export * from './nav-dropdown';
|
||||
export * from './nav';
|
||||
export * from './navigation';
|
||||
export * from './notification';
|
||||
export * from './popover';
|
||||
export * from './progress-bar';
|
||||
export * from './radio-group';
|
||||
export * from './resizable-grid';
|
||||
export * from './rounded-wrapper';
|
||||
export * from './select';
|
||||
export * from './simple-grid';
|
||||
export * from './slider';
|
||||
@@ -35,13 +39,10 @@ export * from './tabs';
|
||||
export * from './text-area';
|
||||
export * from './theme-switcher';
|
||||
export * from './thumbs';
|
||||
export * from './toast';
|
||||
export * from './toggle';
|
||||
export * from './tooltip';
|
||||
export * from './traffic-light';
|
||||
export * from './vega-icons';
|
||||
export * from './vega-logo';
|
||||
export * from './viewing-as-user';
|
||||
export * from './traffic-light';
|
||||
export * from './toast';
|
||||
export * from './notification';
|
||||
export * from './rounded-wrapper';
|
||||
export * from './maintenance-page';
|
||||
|
||||
@@ -13,6 +13,7 @@ export const InputError = ({
|
||||
children,
|
||||
forInput,
|
||||
testId,
|
||||
className,
|
||||
...props
|
||||
}: InputErrorProps) => {
|
||||
const effectiveClassName = classNames(
|
||||
@@ -31,7 +32,7 @@ export const InputError = ({
|
||||
<div
|
||||
data-testid={testId || 'input-error-text'}
|
||||
aria-describedby={forInput}
|
||||
className={effectiveClassName}
|
||||
className={classNames(effectiveClassName, className)}
|
||||
{...props}
|
||||
role="alert"
|
||||
>
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
export * from './navigation';
|
||||
@@ -0,0 +1,246 @@
|
||||
import type { ComponentMeta, ComponentStory } from '@storybook/react';
|
||||
import { MemoryRouter, Route, Routes, useMatch } from 'react-router-dom';
|
||||
import { Button, ExternalLink, Icon, ThemeSwitcher } from '..';
|
||||
import {
|
||||
Navigation,
|
||||
NavigationBreakpoint,
|
||||
NavigationContent,
|
||||
NavigationItem,
|
||||
NavigationLink,
|
||||
NavigationList,
|
||||
NavigationTrigger,
|
||||
} from './navigation';
|
||||
|
||||
export default {
|
||||
title: 'Navigation',
|
||||
component: Navigation,
|
||||
} as ComponentMeta<typeof Navigation>;
|
||||
|
||||
const Template: ComponentStory<typeof Navigation> = ({
|
||||
children,
|
||||
...props
|
||||
}) => {
|
||||
const nav = <Navigation {...props}>{children}</Navigation>;
|
||||
return (
|
||||
<MemoryRouter>
|
||||
<div className="h-[300px]">
|
||||
{nav}
|
||||
<div className="mt-2">
|
||||
<Routes>
|
||||
<Route path="transactions" element={<h1>Transactions</h1>} />
|
||||
<Route path="blocks" element={<h1>Blocks</h1>} />
|
||||
<Route path="markets/all" element={<h1>All markets</h1>} />
|
||||
</Routes>
|
||||
<p>
|
||||
Lorem ipsum dolor sit amet, consectetur adipisicing elit. Quis
|
||||
pariatur a nemo quos sed! Voluptas itaque voluptate dolores minima.
|
||||
Iste laudantium perspiciatis accusamus facere eius repudiandae sit
|
||||
odio saepe nisi.
|
||||
</p>
|
||||
<p>
|
||||
Lorem ipsum dolor sit amet consectetur adipisicing elit. Ea facere
|
||||
ab at incidunt numquam nemo natus eos iure, iste tenetur illo
|
||||
dolores, commodi magni quam dolor totam quae velit eaque.
|
||||
</p>
|
||||
<p>
|
||||
Lorem ipsum dolor, sit amet consectetur adipisicing elit. Quaerat
|
||||
minus perspiciatis quas temporibus odit? Enim ipsam nisi amet
|
||||
molestias magnam esse blanditiis aperiam sapiente quaerat. Veniam
|
||||
unde magnam exercitationem distinctio.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</MemoryRouter>
|
||||
);
|
||||
};
|
||||
|
||||
export const Default = Template.bind({});
|
||||
Default.args = {
|
||||
appName: '',
|
||||
};
|
||||
|
||||
const ExplorerNav = () => {
|
||||
const isOnMarkets = useMatch('markets/*');
|
||||
|
||||
return (
|
||||
<>
|
||||
<NavigationList>
|
||||
<NavigationItem hide={[NavigationBreakpoint.Small]}>
|
||||
{/* <NetworkSwitcher currentNetwork={Networks.CUSTOM} /> */}
|
||||
<Button size="xs">Network Switcher</Button>
|
||||
</NavigationItem>
|
||||
</NavigationList>
|
||||
<NavigationList
|
||||
hide={[NavigationBreakpoint.Narrow, NavigationBreakpoint.Small]}
|
||||
>
|
||||
<NavigationItem>
|
||||
<NavigationLink to="transactions">Transactions</NavigationLink>
|
||||
</NavigationItem>
|
||||
<NavigationItem>
|
||||
<NavigationLink to="blocks">Blocks</NavigationLink>
|
||||
</NavigationItem>
|
||||
<NavigationItem>
|
||||
<NavigationTrigger isActive={Boolean(isOnMarkets)}>
|
||||
Markets
|
||||
</NavigationTrigger>
|
||||
<NavigationContent>
|
||||
<NavigationList>
|
||||
<NavigationItem>
|
||||
<NavigationLink to="markets/all">All markets</NavigationLink>
|
||||
</NavigationItem>
|
||||
<NavigationItem>
|
||||
<NavigationLink to="markets/proposed">
|
||||
Proposed markets
|
||||
</NavigationLink>
|
||||
</NavigationItem>
|
||||
<NavigationItem>
|
||||
<NavigationLink to="markets/failed">
|
||||
Failed markets
|
||||
</NavigationLink>
|
||||
</NavigationItem>
|
||||
</NavigationList>
|
||||
</NavigationContent>
|
||||
</NavigationItem>
|
||||
<NavigationItem>
|
||||
<NavigationLink to="validators">Validators</NavigationLink>
|
||||
</NavigationItem>
|
||||
</NavigationList>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export const Explorer = Template.bind({});
|
||||
Explorer.args = {
|
||||
appName: 'Explorer',
|
||||
theme: 'system',
|
||||
children: <ExplorerNav />,
|
||||
actions: (
|
||||
<>
|
||||
<ThemeSwitcher />
|
||||
{/* JUST A PLACEHOLDER */}
|
||||
<div className="border rounded px-2 py-1 text-xs font-alpha w-60 flex items-center gap-1">
|
||||
<Icon
|
||||
name="search"
|
||||
size={3}
|
||||
className="text-vega-light-200 dark:text-vega-dark-200"
|
||||
/>
|
||||
<input
|
||||
className="w-full bg-transparent outline-none"
|
||||
placeholder="Enter block number or transaction hash"
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
),
|
||||
};
|
||||
|
||||
const ConsoleNav = () => {
|
||||
return (
|
||||
<>
|
||||
<NavigationList>
|
||||
<NavigationItem>
|
||||
<Button size="xs">Network Switcher</Button>
|
||||
</NavigationItem>
|
||||
</NavigationList>
|
||||
<NavigationList
|
||||
hide={[NavigationBreakpoint.Narrow, NavigationBreakpoint.Small]}
|
||||
>
|
||||
<NavigationItem>
|
||||
<NavigationLink to="markets">Markets</NavigationLink>
|
||||
</NavigationItem>
|
||||
<NavigationItem>
|
||||
<NavigationLink to="trading">Trading</NavigationLink>
|
||||
</NavigationItem>
|
||||
<NavigationItem>
|
||||
<NavigationLink to="portfolio">Portfolio</NavigationLink>
|
||||
</NavigationItem>
|
||||
<NavigationItem>
|
||||
<ExternalLink>
|
||||
<span className="flex items-center gap-2">
|
||||
<span>Governance</span> <Icon name="arrow-top-right" size={3} />
|
||||
</span>
|
||||
</ExternalLink>
|
||||
</NavigationItem>
|
||||
</NavigationList>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export const Console = Template.bind({});
|
||||
Console.args = {
|
||||
appName: 'Console',
|
||||
theme: 'system',
|
||||
children: <ConsoleNav />,
|
||||
breakpoints: [478, 770],
|
||||
actions: <ThemeSwitcher />,
|
||||
};
|
||||
|
||||
const GovernanceNav = () => {
|
||||
const isOnToken = useMatch('token/*');
|
||||
return (
|
||||
<>
|
||||
<NavigationList>
|
||||
<NavigationItem hide={[NavigationBreakpoint.Small]}>
|
||||
<Button size="xs">Network Switcher</Button>
|
||||
</NavigationItem>
|
||||
</NavigationList>
|
||||
<NavigationList
|
||||
hide={[NavigationBreakpoint.Narrow, NavigationBreakpoint.Small]}
|
||||
>
|
||||
<NavigationItem>
|
||||
<NavigationLink to="proposals">Proposals</NavigationLink>
|
||||
</NavigationItem>
|
||||
<NavigationItem>
|
||||
<NavigationLink to="validators">Validators</NavigationLink>
|
||||
</NavigationItem>
|
||||
<NavigationItem>
|
||||
<NavigationLink to="rewards">Rewards</NavigationLink>
|
||||
</NavigationItem>
|
||||
<NavigationItem>
|
||||
<NavigationTrigger isActive={Boolean(isOnToken)}>
|
||||
Token
|
||||
</NavigationTrigger>
|
||||
<NavigationContent>
|
||||
<NavigationList>
|
||||
<NavigationItem>
|
||||
<NavigationLink to="token/index">Token</NavigationLink>
|
||||
</NavigationItem>
|
||||
<NavigationItem>
|
||||
<NavigationLink to="token/tranches">
|
||||
Supply & Vesting
|
||||
</NavigationLink>
|
||||
</NavigationItem>
|
||||
<NavigationItem>
|
||||
<NavigationLink to="token/withdraw">Withdraw</NavigationLink>
|
||||
</NavigationItem>
|
||||
<NavigationItem>
|
||||
<NavigationLink to="token/redeem">Redeem</NavigationLink>
|
||||
</NavigationItem>
|
||||
<NavigationItem>
|
||||
<NavigationLink to="token/associate">Associate</NavigationLink>
|
||||
</NavigationItem>
|
||||
<NavigationItem>
|
||||
<NavigationLink to="token/disassociate">
|
||||
Disassociate
|
||||
</NavigationLink>
|
||||
</NavigationItem>
|
||||
</NavigationList>
|
||||
</NavigationContent>
|
||||
</NavigationItem>
|
||||
</NavigationList>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export const Governance = Template.bind({});
|
||||
Governance.args = {
|
||||
appName: 'Governance',
|
||||
theme: 'dark',
|
||||
children: <GovernanceNav />,
|
||||
actions: (
|
||||
<Button size="sm">
|
||||
<span className="flex items-center gap-2">
|
||||
<span>Connect</span> <Icon name="arrow-right" size={3} />
|
||||
</span>
|
||||
</Button>
|
||||
),
|
||||
};
|
||||
@@ -0,0 +1,491 @@
|
||||
import classNames from 'classnames';
|
||||
import type { ComponentProps, ReactNode } from 'react';
|
||||
import { createContext } from 'react';
|
||||
import { useContext } from 'react';
|
||||
import { useRef } from 'react';
|
||||
import { VegaLogo } from '../vega-logo';
|
||||
import * as NavigationMenu from '@radix-ui/react-navigation-menu';
|
||||
import { useResizeObserver } from '@vegaprotocol/react-helpers';
|
||||
import { Icon } from '../icon';
|
||||
import { Drawer, DrawerContext, useDrawer } from '../drawer';
|
||||
import type { Link } from 'react-router-dom';
|
||||
import { NavLink } from 'react-router-dom';
|
||||
|
||||
type NavigationProps = {
|
||||
/**
|
||||
* The display name of the dApp, e.g. "Console", "Explorer"
|
||||
*/
|
||||
appName: string;
|
||||
/**
|
||||
* URL pointing to the home page.
|
||||
*/
|
||||
homeLink?: string;
|
||||
/**
|
||||
* The theme of the navigation.
|
||||
* @default "system"
|
||||
*/
|
||||
theme: 'system' | 'light' | 'dark' | 'yellow';
|
||||
/**
|
||||
* The navigation items (links, buttons, dropdowns, etc.)
|
||||
*/
|
||||
children?: ReactNode;
|
||||
/**
|
||||
* The navigation actions (e.g. theme switcher, wallet connector)
|
||||
*/
|
||||
actions?: ReactNode;
|
||||
/**
|
||||
* Size variants breakpoints
|
||||
*/
|
||||
breakpoints?: [number, number];
|
||||
};
|
||||
|
||||
export enum NavigationBreakpoint {
|
||||
/**
|
||||
* Full width navigation
|
||||
* `width > breakpoints[1]`
|
||||
*/
|
||||
Full = 'nav-size-full',
|
||||
/**
|
||||
* Narrow variant
|
||||
* `width > breakpoints[0] && width <= breakpoints[1]`
|
||||
*/
|
||||
Narrow = 'nav-size-narrow',
|
||||
/**
|
||||
* Small variant
|
||||
* `width <= breakpoints[0]`
|
||||
*/
|
||||
Small = 'nav-size-small',
|
||||
}
|
||||
type NavigationElementProps = {
|
||||
hide?: NavigationBreakpoint[];
|
||||
hideInDrawer?: boolean;
|
||||
};
|
||||
|
||||
const determineIfHidden = ({ hide, hideInDrawer }: NavigationElementProps) => [
|
||||
{
|
||||
'[.nav-size-full_.navbar_&]:hidden': hide?.includes(
|
||||
NavigationBreakpoint.Full
|
||||
),
|
||||
'[.nav-size-narrow_.navbar_&]:hidden': hide?.includes(
|
||||
NavigationBreakpoint.Narrow
|
||||
),
|
||||
'[.nav-size-small_.navbar_&]:hidden': hide?.includes(
|
||||
NavigationBreakpoint.Small
|
||||
),
|
||||
'[.drawer-content_&]:hidden': hideInDrawer,
|
||||
},
|
||||
];
|
||||
|
||||
const Logo = ({
|
||||
appName,
|
||||
homeLink,
|
||||
}: Pick<NavigationProps, 'theme' | 'appName' | 'homeLink'>) => {
|
||||
const theme = useContext(NavigationThemeContext);
|
||||
return (
|
||||
<div className="flex h-full gap-4 items-center">
|
||||
{homeLink ? (
|
||||
<NavLink to={homeLink}>
|
||||
<VegaLogo className="h-4 group-[.nav-size-small]:h-3" />
|
||||
</NavLink>
|
||||
) : (
|
||||
<VegaLogo className="h-4 group-[.nav-size-small]:h-3" />
|
||||
)}
|
||||
|
||||
{appName && (
|
||||
<span
|
||||
data-testid="nav-app-name"
|
||||
className={classNames(
|
||||
'group-[.nav-size-small]:text-sm',
|
||||
'font-alpha calt lowercase text-xl tracking-[1px] whitespace-nowrap leading-1',
|
||||
'border-l pl-4',
|
||||
{
|
||||
'border-l-vega-light-200 dark:border-l-vega-dark-200':
|
||||
theme === 'system',
|
||||
'border-l-vega-light-200': theme === 'light',
|
||||
'border-l-vega-dark-200': theme === 'dark' || theme === 'yellow',
|
||||
}
|
||||
)}
|
||||
>
|
||||
{appName}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const Spacer = () => <div className="w-full" aria-hidden="true"></div>;
|
||||
|
||||
export const NavigationItem = ({
|
||||
children,
|
||||
className,
|
||||
hide,
|
||||
hideInDrawer,
|
||||
...props
|
||||
}: ComponentProps<typeof NavigationMenu.Item> & NavigationElementProps) => {
|
||||
const insideDrawer = useContext(DrawerContext);
|
||||
return (
|
||||
<NavigationMenu.Item
|
||||
className={classNames(
|
||||
!insideDrawer && ['h-12 [.navigation-content_&]:h-8 flex items-center'],
|
||||
determineIfHidden({ hide, hideInDrawer }),
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</NavigationMenu.Item>
|
||||
);
|
||||
};
|
||||
export const NavigationList = ({
|
||||
children,
|
||||
className,
|
||||
hide,
|
||||
hideInDrawer,
|
||||
...props
|
||||
}: ComponentProps<typeof NavigationMenu.List> & NavigationElementProps) => (
|
||||
<NavigationMenu.List
|
||||
className={classNames(
|
||||
'flex gap-4 items-center',
|
||||
'[.navigation-content_&]:flex-col [.navigation-content_&]:items-start [.navigation-content_&]:gap-2',
|
||||
'[.drawer-content_&]:flex-col [.drawer-content_&]:items-start [.drawer-content_&]:gap-2 [.drawer-content_&]:mt-2',
|
||||
determineIfHidden({ hide, hideInDrawer }),
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</NavigationMenu.List>
|
||||
);
|
||||
export const NavigationTrigger = ({
|
||||
children,
|
||||
className,
|
||||
isActive = false,
|
||||
hide,
|
||||
hideInDrawer,
|
||||
...props
|
||||
}: ComponentProps<typeof NavigationMenu.Trigger> & {
|
||||
isActive?: boolean;
|
||||
} & NavigationElementProps) => {
|
||||
const theme = useContext(NavigationThemeContext);
|
||||
return (
|
||||
<NavigationMenu.Trigger
|
||||
className={classNames(
|
||||
'h-12 [.drawer-content_&]:h-min flex items-center relative gap-2',
|
||||
{
|
||||
'text-black dark:text-white': isActive && theme === 'system',
|
||||
'text-black': isActive && theme === 'light',
|
||||
'text-white': isActive && theme === 'dark',
|
||||
'text-black [.navigation-content_&]:text-white [.drawer-content_&]:text-white':
|
||||
isActive && theme === 'yellow',
|
||||
},
|
||||
determineIfHidden({ hide, hideInDrawer }),
|
||||
className
|
||||
)}
|
||||
onPointerMove={(e) => e.preventDefault()} // disables hover
|
||||
onPointerLeave={(e) => e.preventDefault()} // disables hover
|
||||
{...props}
|
||||
>
|
||||
<span>{children}</span>
|
||||
<span className="rotate-90 group-data-open/drawer:hidden">
|
||||
<Icon name="arrow-right" size={3} />
|
||||
</span>
|
||||
<div
|
||||
aria-hidden="true"
|
||||
className={classNames(
|
||||
'absolute bottom-0 left-0 w-full h-[2px] [.navigation-content_&]:hidden [.drawer-content_&]:hidden',
|
||||
{ hidden: !isActive },
|
||||
{
|
||||
'bg-vega-yellow-550 dark:bg-vega-yellow-500': theme === 'system',
|
||||
'bg-vega-yellow-550': theme === 'light',
|
||||
'bg-vega-yellow-500': theme === 'dark',
|
||||
'bg-black': theme === 'yellow',
|
||||
}
|
||||
)}
|
||||
></div>
|
||||
</NavigationMenu.Trigger>
|
||||
);
|
||||
};
|
||||
export const NavigationContent = ({
|
||||
children,
|
||||
className,
|
||||
...props
|
||||
}: ComponentProps<typeof NavigationMenu.Content>) => {
|
||||
const theme = useContext(NavigationThemeContext);
|
||||
const insideDrawer = useContext(DrawerContext);
|
||||
|
||||
const content = (
|
||||
<NavigationMenu.Content
|
||||
onPointerLeave={(e) => e.preventDefault()} // disables hover
|
||||
asChild
|
||||
{...props}
|
||||
>
|
||||
<div
|
||||
className={classNames(
|
||||
'navigation-content',
|
||||
'absolute z-20 top-12 w-max',
|
||||
'p-2 mt-1',
|
||||
'text-vega-light-300 dark:text-vega-dark-300',
|
||||
|
||||
'border rounded border-vega-light-200 dark:border-vega-dark-200',
|
||||
'shadow-[8px_8px_16px_0_rgba(0,0,0,0.4)]',
|
||||
{
|
||||
'bg-white dark:bg-black': theme === 'system',
|
||||
'bg-white': theme === 'light',
|
||||
'bg-black': theme === 'dark' || theme === 'yellow',
|
||||
}
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
</NavigationMenu.Content>
|
||||
);
|
||||
|
||||
const list = (
|
||||
<div className={classNames('navigation-content', 'border-none pl-4')}>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
|
||||
return insideDrawer ? list : content;
|
||||
};
|
||||
|
||||
export const NavigationLink = ({
|
||||
children,
|
||||
to,
|
||||
...props
|
||||
}: ComponentProps<typeof Link>) => {
|
||||
const theme = useContext(NavigationThemeContext);
|
||||
const setDrawerOpen = useDrawer((state) => state.setDrawerOpen);
|
||||
return (
|
||||
<NavigationMenu.Link
|
||||
asChild
|
||||
onClick={() => {
|
||||
setDrawerOpen(false);
|
||||
}}
|
||||
>
|
||||
<NavLink
|
||||
to={to}
|
||||
className={classNames(
|
||||
'h-12 [.navigation-content_&]:h-min [.drawer-content_&]:h-min flex items-center relative'
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{({ isActive }) => (
|
||||
<>
|
||||
<span
|
||||
className={classNames({
|
||||
'text-black dark:text-white': isActive && theme === 'system',
|
||||
'text-black': isActive && theme === 'light',
|
||||
'text-white': isActive && theme === 'dark',
|
||||
'text-black [.navigation-content_&]:text-white [.drawer-content_&]:text-white':
|
||||
isActive && theme === 'yellow',
|
||||
})}
|
||||
>
|
||||
{children}
|
||||
</span>
|
||||
<div
|
||||
aria-hidden="true"
|
||||
className={classNames(
|
||||
'absolute bottom-0 left-0 w-full h-[2px] [.navigation-content_&]:hidden [.drawer-content_&]:hidden',
|
||||
{ hidden: !isActive },
|
||||
{
|
||||
'bg-vega-yellow-550 dark:bg-vega-yellow-500':
|
||||
theme === 'system',
|
||||
'bg-vega-yellow-550': theme === 'light',
|
||||
'bg-vega-yellow-500': theme === 'dark',
|
||||
'bg-black': theme === 'yellow',
|
||||
}
|
||||
)}
|
||||
></div>
|
||||
</>
|
||||
)}
|
||||
</NavLink>
|
||||
</NavigationMenu.Link>
|
||||
);
|
||||
};
|
||||
|
||||
const NavigationThemeContext =
|
||||
createContext<NavigationProps['theme']>('system');
|
||||
|
||||
export const Navigation = ({
|
||||
appName,
|
||||
homeLink = '/',
|
||||
children,
|
||||
actions,
|
||||
theme = 'system',
|
||||
breakpoints = [478, 1000],
|
||||
}: NavigationProps) => {
|
||||
const navigationRef = useRef<HTMLElement>(null);
|
||||
const actionsRef = useRef<HTMLDivElement>(null);
|
||||
useResizeObserver(navigationRef.current, (entries) => {
|
||||
if (entries.length === 0 && !navigationRef.current) return;
|
||||
|
||||
const width = entries[0].borderBoxSize[0].inlineSize;
|
||||
navigationRef.current?.classList.remove(
|
||||
NavigationBreakpoint.Full,
|
||||
NavigationBreakpoint.Narrow,
|
||||
NavigationBreakpoint.Small
|
||||
);
|
||||
|
||||
if (width <= breakpoints[0]) {
|
||||
navigationRef.current?.classList.add(NavigationBreakpoint.Small);
|
||||
}
|
||||
if (width > breakpoints[0] && width <= breakpoints[1]) {
|
||||
navigationRef.current?.classList.add(NavigationBreakpoint.Narrow);
|
||||
}
|
||||
if (width > breakpoints[1]) {
|
||||
navigationRef.current?.classList.add(NavigationBreakpoint.Full);
|
||||
}
|
||||
});
|
||||
|
||||
const [drawerOpen, setDrawerOpen] = useDrawer((state) => [
|
||||
state.drawerOpen,
|
||||
state.setDrawerOpen,
|
||||
]);
|
||||
|
||||
const drawerTrigger = (
|
||||
<button
|
||||
className={classNames('px-2', `group-[.nav-size-full]:hidden`, {
|
||||
'z-20': drawerOpen,
|
||||
})}
|
||||
onClick={() => {
|
||||
setDrawerOpen(!drawerOpen);
|
||||
}}
|
||||
>
|
||||
<svg
|
||||
className={classNames('stroke-[1px] transition-transform', {
|
||||
'stroke-black dark:stroke-white': theme === 'system',
|
||||
'stroke-black': theme === 'light' || theme === 'yellow',
|
||||
'stroke-white':
|
||||
theme === 'dark' || (drawerOpen && theme === 'yellow'),
|
||||
})}
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 16 16"
|
||||
>
|
||||
<line
|
||||
x1={0.5}
|
||||
x2={15.5}
|
||||
y1={3.5}
|
||||
y2={3.5}
|
||||
className={classNames('transition-transform duration-75', {
|
||||
'rotate-45 translate-y-[4px] origin-[8px_3.5px]': drawerOpen,
|
||||
})}
|
||||
/>
|
||||
<line
|
||||
x1={0.5}
|
||||
x2={15.5}
|
||||
y1={11.5}
|
||||
y2={11.5}
|
||||
className={classNames('transition-transform duration-75', {
|
||||
'rotate-[-45deg] translate-y-[-4px] origin-[8px_11.5px]':
|
||||
drawerOpen,
|
||||
})}
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
);
|
||||
|
||||
const drawerContent = (
|
||||
<DrawerContext.Provider value={true}>
|
||||
<div
|
||||
className={classNames(
|
||||
'drawer-content',
|
||||
'border-l h-full relative overflow-auto',
|
||||
'px-4 font-alpha',
|
||||
// text
|
||||
{
|
||||
'text-vega-light-300 dark:text-vega-dark-300': theme === 'system',
|
||||
'text-vega-light-300': theme === 'light',
|
||||
'text-vega-dark-300': theme === 'dark' || theme === 'yellow',
|
||||
},
|
||||
// border
|
||||
{
|
||||
'border-l-vega-light-200 dark:border-l-vega-dark-200':
|
||||
theme === 'system',
|
||||
'border-l-vega-light-200': theme === 'light',
|
||||
'border-l-vega-dark-200': theme === 'dark' || theme === 'yellow',
|
||||
},
|
||||
// background
|
||||
{
|
||||
'bg-white dark:bg-black': theme === 'system',
|
||||
'bg-white': theme === 'light',
|
||||
'bg-black': theme === 'dark' || theme === 'yellow',
|
||||
}
|
||||
)}
|
||||
style={{
|
||||
paddingTop: `${
|
||||
navigationRef.current?.getBoundingClientRect().bottom
|
||||
}px`,
|
||||
}}
|
||||
>
|
||||
<div className="flex flex-col gap-2 pr-10">{children}</div>
|
||||
</div>
|
||||
</DrawerContext.Provider>
|
||||
);
|
||||
|
||||
return (
|
||||
<NavigationThemeContext.Provider value={theme}>
|
||||
<NavigationMenu.Root
|
||||
ref={navigationRef}
|
||||
className={classNames(
|
||||
'h-12',
|
||||
'group flex gap-4 items-center',
|
||||
'border-b px-3 relative',
|
||||
// text
|
||||
{
|
||||
'text-black dark:text-white': theme === 'system',
|
||||
'text-black': theme === 'light' || theme === 'yellow',
|
||||
'text-white': theme === 'dark',
|
||||
},
|
||||
// border
|
||||
{
|
||||
'border-b-vega-light-200 dark:border-b-vega-dark-200':
|
||||
theme === 'system',
|
||||
'border-b-vega-light-200': theme === 'light',
|
||||
'border-b-vega-dark-200': theme === 'dark',
|
||||
'border-b-black': theme === 'yellow',
|
||||
},
|
||||
// background
|
||||
{
|
||||
'bg-white dark:bg-black': theme === 'system',
|
||||
'bg-white': theme === 'light',
|
||||
'bg-black': theme === 'dark',
|
||||
'bg-vega-yellow-500': theme === 'yellow',
|
||||
}
|
||||
)}
|
||||
data-testid="navigation"
|
||||
>
|
||||
<Logo appName={appName} theme={theme} homeLink={homeLink} />
|
||||
<div
|
||||
className={classNames(
|
||||
'navbar',
|
||||
'flex gap-4 h-12 items-center font-alpha text-lg calt',
|
||||
{
|
||||
'text-vega-light-300 dark:text-vega-dark-300': theme === 'system',
|
||||
'text-vega-light-300': theme === 'light',
|
||||
'text-vega-dark-300': theme === 'dark',
|
||||
'text-vega-dark-200': theme === 'yellow',
|
||||
}
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
<Spacer />
|
||||
{(actions || children) && (
|
||||
<div ref={actionsRef} className="flex gap-2 items-center">
|
||||
{actions}
|
||||
<Drawer
|
||||
open={drawerOpen}
|
||||
onChange={setDrawerOpen}
|
||||
trigger={drawerTrigger}
|
||||
container={actionsRef.current}
|
||||
>
|
||||
{drawerContent}
|
||||
</Drawer>
|
||||
</div>
|
||||
)}
|
||||
</NavigationMenu.Root>
|
||||
</NavigationThemeContext.Provider>
|
||||
);
|
||||
};
|
||||
@@ -1,5 +1,9 @@
|
||||
export const SunIcon = () => (
|
||||
<svg viewBox="0 0 45 45" className="w-8 h-8">
|
||||
type IconProps = {
|
||||
className?: string;
|
||||
};
|
||||
|
||||
export const SunIcon = ({ className }: IconProps) => (
|
||||
<svg viewBox="0 0 45 45" className={className || 'w-8 h-8'}>
|
||||
<path
|
||||
d="M22.5 27.79a5.29 5.29 0 1 0 0-10.58 5.29 5.29 0 0 0 0 10.58Z"
|
||||
fill="currentColor"
|
||||
@@ -13,8 +17,8 @@ export const SunIcon = () => (
|
||||
</svg>
|
||||
);
|
||||
|
||||
export const MoonIcon = () => (
|
||||
<svg viewBox="0 0 45 45" className="w-8 h-8">
|
||||
export const MoonIcon = ({ className }: IconProps) => (
|
||||
<svg viewBox="0 0 45 45" className={className || 'w-8 h-8'}>
|
||||
<path
|
||||
d="M28.75 11.69A12.39 12.39 0 0 0 22.5 10a12.5 12.5 0 1 0 0 25c2.196 0 4.353-.583 6.25-1.69A12.46 12.46 0 0 0 35 22.5a12.46 12.46 0 0 0-6.25-10.81Zm-6.25 22a11.21 11.21 0 0 1-11.2-11.2 11.21 11.21 0 0 1 11.2-11.2c1.246 0 2.484.209 3.66.62a13.861 13.861 0 0 0-5 10.58 13.861 13.861 0 0 0 5 10.58 11.078 11.078 0 0 1-3.66.63v-.01Z"
|
||||
fill="currentColor"
|
||||
|
||||
@@ -1,13 +1,16 @@
|
||||
import { t } from '@vegaprotocol/i18n';
|
||||
|
||||
export const VegaLogo = () => {
|
||||
type VegaLogoProps = {
|
||||
className?: string;
|
||||
};
|
||||
export const VegaLogo = ({ className }: VegaLogoProps) => {
|
||||
return (
|
||||
<svg
|
||||
aria-label={t('Vega logo')}
|
||||
width="111"
|
||||
height="24"
|
||||
className={className || 'h-6'}
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 111 24"
|
||||
>
|
||||
<path
|
||||
fill="currentColor"
|
||||
|
||||
@@ -25,6 +25,7 @@
|
||||
"@radix-ui/react-dialog": "^1.0.2",
|
||||
"@radix-ui/react-dropdown-menu": "^2.0.2",
|
||||
"@radix-ui/react-icons": "^1.1.1",
|
||||
"@radix-ui/react-navigation-menu": "^1.1.1",
|
||||
"@radix-ui/react-popover": "^1.0.3",
|
||||
"@radix-ui/react-radio-group": "^1.1.1",
|
||||
"@radix-ui/react-select": "^1.2.0",
|
||||
|
||||
@@ -4222,6 +4222,27 @@
|
||||
aria-hidden "^1.1.1"
|
||||
react-remove-scroll "2.5.5"
|
||||
|
||||
"@radix-ui/react-navigation-menu@^1.1.1":
|
||||
version "1.1.1"
|
||||
resolved "https://registry.yarnpkg.com/@radix-ui/react-navigation-menu/-/react-navigation-menu-1.1.1.tgz#84f24d90e6448a0c83d3431c6eefbea73dc7522e"
|
||||
integrity sha512-Khgf+LwqYfUpbFAHcFPDMj6ZrWxnwCgC96liLYwE48x9YJbXGlutOWzZaSzrgl82xS+PwoPLQunfDe/i4ZITRA==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.13.10"
|
||||
"@radix-ui/primitive" "1.0.0"
|
||||
"@radix-ui/react-collection" "1.0.1"
|
||||
"@radix-ui/react-compose-refs" "1.0.0"
|
||||
"@radix-ui/react-context" "1.0.0"
|
||||
"@radix-ui/react-direction" "1.0.0"
|
||||
"@radix-ui/react-dismissable-layer" "1.0.2"
|
||||
"@radix-ui/react-id" "1.0.0"
|
||||
"@radix-ui/react-presence" "1.0.0"
|
||||
"@radix-ui/react-primitive" "1.0.1"
|
||||
"@radix-ui/react-use-callback-ref" "1.0.0"
|
||||
"@radix-ui/react-use-controllable-state" "1.0.0"
|
||||
"@radix-ui/react-use-layout-effect" "1.0.0"
|
||||
"@radix-ui/react-use-previous" "1.0.0"
|
||||
"@radix-ui/react-visually-hidden" "1.0.1"
|
||||
|
||||
"@radix-ui/react-popover@^1.0.3":
|
||||
version "1.0.3"
|
||||
resolved "https://registry.yarnpkg.com/@radix-ui/react-popover/-/react-popover-1.0.3.tgz#65ae2ee1fca2d7fd750308549eb8e0857c6160fe"
|
||||
|
||||
Reference in New Issue
Block a user