Skip to content

Commit

Permalink
feat(minifront, ui): #1732: v2 header (#1734)
Browse files Browse the repository at this point in the history
* feat(minifront): #1732: add support for react-svgr vite plugin

* refactor(minifront): #1732: externalize `getV2Link` function

* feat(minifront): #1732: implement basic header with prax wallet info

* feat(ui): #1732: fix button and dialog styles

* feat(minifront): #1732: implement basic mobile nav dialog

* feat(ui): #1732: update Tabs component to support compact version

* feat(ui): #1732: apply v2 colors to tailwind theme

* feat(UI): #1732: Implement MenuItem component

* feat(minifront): #1732: use MenuItem to create mobile nav

* feat(ui): #1732: add Progress UI component

* feat(minifront): #1732: display sync bar on the top

* feat(ui): #1732: update Pill component to support context prop

* feat(minifront): #1732: apply updated Pill to the status popover

* fix: prettier

* chore: changeset

* fix: update paths after rebase

* fix: syncpack

* fix: ts

* fix(minifront): #1732: fix after the review

* fix(ui): externalize `getAllEntries` function

* fix(minifront): #1732: refactor the manifest usage
  • Loading branch information
VanishMax authored Aug 31, 2024
1 parent a3bef37 commit 74e89e0
Show file tree
Hide file tree
Showing 45 changed files with 1,031 additions and 162 deletions.
22 changes: 22 additions & 0 deletions .changeset/pink-candles-whisper.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
---
'@repo/tailwind-config': minor
'minifront': minor
'@penumbra-zone/ui': minor
---

UI:

- Add new `Progress` component
- Add `MenuItem` component that shares the styles with `DropdownMenu.Item`
- Update the `Pill` component to support `context` prop
- Update the `Tabs` component to support the `compact` density
- Allow passing custom icons to the `Button`
- Fix `density` tag in Storybook

Tailwind Config:

- Add support for v2 colors with v2 prefix like `bg-v2-secondary-dark`

Minifront:

- Add top navigation to the v2 minifront with sync bar and prax connection infos
34 changes: 1 addition & 33 deletions apps/minifront/src/components/syncing-dialog/index.tsx
Original file line number Diff line number Diff line change
@@ -1,42 +1,10 @@
import { Dialog } from '@penumbra-zone/ui/Dialog';
import { Status, useStatus } from '../../state/status';
import { AbridgedZQueryState } from '@penumbra-zone/zquery/src/types';
import { statusSelector, useStatus } from '../../state/status';
import { SyncAnimation } from './sync-animation';
import { Text } from '@penumbra-zone/ui/Text';
import { useEffect, useState } from 'react';
import { useSyncProgress } from '@penumbra-zone/ui/components/ui/block-sync-status';

type StatusSelector =
| {
isCatchingUp: false;
}
| {
isCatchingUp: boolean;
fullSyncHeight: bigint;
latestKnownBlockHeight?: bigint;
percentSynced?: string;
};

// Copies the logic from the view service's `status` method.
const statusSelector = (zQueryState: AbridgedZQueryState<Status>): StatusSelector => {
if (!zQueryState.data?.fullSyncHeight) {
return { isCatchingUp: false };
} else {
const { fullSyncHeight, latestKnownBlockHeight } = zQueryState.data;
const isCatchingUp = !latestKnownBlockHeight || latestKnownBlockHeight > fullSyncHeight;

let percentSynced: string | undefined;
if (latestKnownBlockHeight) {
const percentSyncedNumber = Math.round(
(Number(fullSyncHeight) / Number(latestKnownBlockHeight)) * 100,
);
percentSynced = `${percentSyncedNumber}%`;
}

return { isCatchingUp, fullSyncHeight, latestKnownBlockHeight, percentSynced };
}
};

export const SyncingDialog = () => {
const status = useStatus({
select: statusSelector,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import illustration from './illustration.svg';
import illustration from './illustration.svg?url';

const FakeButtons = () => (
<div className='ml-6 mt-6 flex gap-1'>
Expand Down
16 changes: 7 additions & 9 deletions apps/minifront/src/components/v2/dashboard-layout/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,18 +7,16 @@ import { PagePath } from '../../metadata/paths';
import { AssetsCardTitle } from './assets-card-title';
import { TransactionsCardTitle } from './transactions-card-title';
import { motion } from 'framer-motion';

/** @todo: Remove this function and its uses after we switch to v2 layout */
const v2PathPrefix = (path: string) => `/v2${path}`;
import { getV2Link } from '../get-v2-link.ts';

const CARD_TITLE_BY_PATH = {
[v2PathPrefix(PagePath.DASHBOARD)]: <AssetsCardTitle />,
[v2PathPrefix(PagePath.TRANSACTIONS)]: <TransactionsCardTitle />,
[getV2Link(PagePath.DASHBOARD)]: <AssetsCardTitle />,
[getV2Link(PagePath.TRANSACTIONS)]: <TransactionsCardTitle />,
};

const TABS_OPTIONS = [
{ label: 'Assets', value: v2PathPrefix(PagePath.DASHBOARD) },
{ label: 'Transactions', value: v2PathPrefix(PagePath.TRANSACTIONS) },
{ label: 'Assets', value: getV2Link(PagePath.DASHBOARD) },
{ label: 'Transactions', value: getV2Link(PagePath.TRANSACTIONS) },
];

export const DashboardLayout = () => {
Expand All @@ -31,12 +29,12 @@ export const DashboardLayout = () => {

<Grid tablet={8} desktop={6} xl={4}>
<Card
title={CARD_TITLE_BY_PATH[v2PathPrefix(pagePath)]}
title={CARD_TITLE_BY_PATH[getV2Link(pagePath)]}
motion={{ layout: true, layoutId: 'main' }}
>
<motion.div layout>
<Tabs
value={v2PathPrefix(pagePath)}
value={getV2Link(pagePath)}
onChange={value => navigate(value)}
options={TABS_OPTIONS}
actionType='accent'
Expand Down
4 changes: 4 additions & 0 deletions apps/minifront/src/components/v2/get-v2-link.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import type { PagePath } from '../metadata/paths.ts';

/** @todo: Remove this function and its uses after we switch to v2 layout */
export const getV2Link = (path: PagePath) => `/v2${path}`;
24 changes: 24 additions & 0 deletions apps/minifront/src/components/v2/header/desktop-nav.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { useNavigate } from 'react-router-dom';
import { Tabs } from '@penumbra-zone/ui/Tabs';
import { Density } from '@penumbra-zone/ui/Density';
import { getV2Link } from '../get-v2-link.ts';
import { usePagePath } from '../../../fetchers/page-path.ts';
import { HEADER_LINKS } from './links.ts';

export const DesktopNav = () => {
const pagePath = usePagePath();
const navigate = useNavigate();

return (
<nav className='hidden rounded-full bg-v2-other-tonalFill5 px-4 py-1 backdrop-blur-xl lg:flex'>
<Density compact>
<Tabs
value={getV2Link(pagePath)}
onChange={value => navigate(value)}
options={HEADER_LINKS}
actionType='accent'
/>
</Density>
</nav>
);
};
26 changes: 26 additions & 0 deletions apps/minifront/src/components/v2/header/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { Density } from '@penumbra-zone/ui/Density';
import { HeaderLogo } from './logo.tsx';
import { ProviderPopover } from './provider-popover.tsx';
import { StatusPopover } from './status-popover.tsx';
import { MobileNav } from './mobile-nav.tsx';
import { DesktopNav } from './desktop-nav.tsx';

export const Header = () => {
return (
<header className='flex items-center justify-between py-5'>
<HeaderLogo />

<DesktopNav />

<Density compact>
<div className='hidden gap-2 lg:flex'>
<StatusPopover />
<ProviderPopover />
</div>
<div className='block lg:hidden'>
<MobileNav />
</div>
</Density>
</header>
);
};
11 changes: 11 additions & 0 deletions apps/minifront/src/components/v2/header/links.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { Shield, MoonStar, ArrowLeftRight, ArrowUpFromDot, Coins } from 'lucide-react';
import { getV2Link } from '../get-v2-link.ts';
import { PagePath } from '../../metadata/paths.ts';

export const HEADER_LINKS = [
{ label: 'Dashboard', value: getV2Link(PagePath.DASHBOARD), icon: Coins },
{ label: 'Shield', value: getV2Link(PagePath.IBC), icon: Shield },
{ label: 'Transfer', value: getV2Link(PagePath.SEND), icon: ArrowUpFromDot },
{ label: 'Swap', value: getV2Link(PagePath.SWAP), icon: ArrowLeftRight },
{ label: 'Stake', value: getV2Link(PagePath.STAKING), icon: MoonStar },
];
3 changes: 3 additions & 0 deletions apps/minifront/src/components/v2/header/logo.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
12 changes: 12 additions & 0 deletions apps/minifront/src/components/v2/header/logo.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { Link } from 'react-router-dom';
import { PagePath } from '../../metadata/paths.ts';
import { getV2Link } from '../get-v2-link.ts';
import PenumbraLogo from './logo.svg';

export const HeaderLogo = () => {
return (
<Link className='flex h-8 items-center' to={getV2Link(PagePath.INDEX)}>
<PenumbraLogo />
</Link>
);
};
57 changes: 57 additions & 0 deletions apps/minifront/src/components/v2/header/mobile-nav.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { Menu, X } from 'lucide-react';
import { Button } from '@penumbra-zone/ui/Button';
import { Dialog } from '@penumbra-zone/ui/Dialog';
import { Display } from '@penumbra-zone/ui/Display';
import { MenuItem } from '@penumbra-zone/ui/MenuItem';
import { StatusPopover } from './status-popover.tsx';
import { ProviderPopover } from './provider-popover.tsx';
import { HeaderLogo } from './logo.tsx';
import { useState } from 'react';
import { HEADER_LINKS } from './links.ts';
import { useNavigate } from 'react-router-dom';

export const MobileNav = () => {
const navigate = useNavigate();
const [isOpen, setIsOpen] = useState(false);

const onNavigate = (link: string) => {
navigate(link);
setIsOpen(false);
};

return (
<Dialog isOpen={isOpen} onClose={() => setIsOpen(false)}>
<Button iconOnly icon={Menu} onClick={() => setIsOpen(true)}>
Menu
</Button>
<Dialog.EmptyContent>
<div className='pointer-events-auto h-full overflow-hidden bg-black'>
<Display>
<nav className='flex items-center justify-between py-5'>
<HeaderLogo />

<div className='flex gap-2'>
<StatusPopover />
<ProviderPopover />
<Button iconOnly icon={X} onClick={() => setIsOpen(false)}>
Close
</Button>
</div>
</nav>

<div className='flex flex-col gap-4'>
{HEADER_LINKS.map(link => (
<MenuItem
key={link.value}
label={link.label}
icon={link.icon}
onClick={() => onNavigate(link.value)}
/>
))}
</div>
</Display>
</div>
</Dialog.EmptyContent>
</Dialog>
);
};
50 changes: 50 additions & 0 deletions apps/minifront/src/components/v2/header/provider-popover.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { useMemo } from 'react';
import { Link2Off } from 'lucide-react';
import { Popover } from '@penumbra-zone/ui/Popover';
import { Button } from '@penumbra-zone/ui/Button';
import { Text } from '@penumbra-zone/ui/Text';
import { penumbra, usePraxManifest } from '../../../prax.ts';

export const ProviderPopover = () => {
const manifest = usePraxManifest();

const icon = useMemo(() => {
const icons = manifest?.icons;
const blob = icons?.['32'] ?? icons?.['128'];
const element = !blob ? null : (
<img src={URL.createObjectURL(blob)} alt={manifest?.name} className='size-4' />
);
return () => element;
}, [manifest]);

const disconnect = () => {
void penumbra.disconnect().then(() => window.location.reload());
};

return (
<Popover>
<Popover.Trigger>
<Button icon={icon} iconOnly>
{manifest?.name ?? ''}
</Button>
</Popover.Trigger>
<Popover.Content align='end' side='bottom'>
{manifest ? (
<div className='flex flex-col gap-2'>
<Text body>
{manifest.name} v{manifest.version}
</Text>
<Text small>{manifest.description}</Text>
</div>
) : (
<Text body>Loading provider manifest...</Text>
)}
<div className='mt-4'>
<Button icon={Link2Off} onClick={disconnect}>
Disconnect
</Button>
</div>
</Popover.Content>
</Popover>
);
};
63 changes: 63 additions & 0 deletions apps/minifront/src/components/v2/header/status-popover.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import { Blocks } from 'lucide-react';
import { Popover } from '@penumbra-zone/ui/Popover';
import { Button } from '@penumbra-zone/ui/Button';
import { Density } from '@penumbra-zone/ui/Density';
import { Pill } from '@penumbra-zone/ui/Pill';
import { Text } from '@penumbra-zone/ui/Text';
import { statusSelector, useStatus } from '../../../state/status.ts';
import { useMemo } from 'react';

export const StatusPopover = () => {
const status = useStatus({
select: statusSelector,
});

// a ReactNode displaying the sync status in form of a pill
const pill = useMemo(() => {
// isCatchingUp is undefined when the status is not yet loaded
if (status?.isCatchingUp === undefined) {
return null;
}

if (status.error) {
return <Pill context='technical-destructive'>Block Sync Error</Pill>;
}

if (status.percentSyncedNumber === 1) {
return <Pill context='technical-success'>Blocks Synced</Pill>;
}

return <Pill context='technical-caution'>Block Syncing</Pill>;
}, [status]);

return (
<Popover>
<Popover.Trigger>
<Button icon={Blocks} iconOnly>
Status
</Button>
</Popover.Trigger>
{status?.isCatchingUp !== undefined && (
<Popover.Content align='end' side='bottom'>
<Density compact>
<div className='flex flex-col gap-4'>
<div className='flex flex-col gap-2'>
<Text technical>Status</Text>
{pill}
{!!status.error && String(status.error)}
</div>
<div className='flex flex-col gap-2'>
<Text technical>Block Height</Text>
<Pill context='technical-default'>
{status.latestKnownBlockHeight !== status.fullSyncHeight
? `${status.fullSyncHeight} of ${status.latestKnownBlockHeight}`
: `${status.latestKnownBlockHeight}`}
</Pill>
</div>
</div>
</Density>
</Popover.Content>
)}
</Popover>
);
};
22 changes: 22 additions & 0 deletions apps/minifront/src/components/v2/header/sync-bar.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { statusSelector, useStatus } from '../../../state/status.ts';
import { Progress } from '@penumbra-zone/ui/Progress';

export const SyncBar = () => {
const status = useStatus({
select: statusSelector,
});

return (
<div className='fixed left-0 top-0 h-1 w-full'>
{status?.isCatchingUp === undefined ? (
<Progress value={0} loading />
) : (
<Progress
value={status.percentSyncedNumber}
loading={status.isUpdating}
error={Boolean(status.error)}
/>
)}
</div>
);
};
Loading

0 comments on commit 74e89e0

Please sign in to comment.