diff --git a/.changeset/pink-candles-whisper.md b/.changeset/pink-candles-whisper.md new file mode 100644 index 0000000000..0174a37376 --- /dev/null +++ b/.changeset/pink-candles-whisper.md @@ -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 diff --git a/apps/minifront/src/components/syncing-dialog/index.tsx b/apps/minifront/src/components/syncing-dialog/index.tsx index a8f4835d21..5b5739701e 100644 --- a/apps/minifront/src/components/syncing-dialog/index.tsx +++ b/apps/minifront/src/components/syncing-dialog/index.tsx @@ -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): 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, diff --git a/apps/minifront/src/components/syncing-dialog/sync-animation.tsx b/apps/minifront/src/components/syncing-dialog/sync-animation.tsx index 2c565554c0..c62bca1114 100644 --- a/apps/minifront/src/components/syncing-dialog/sync-animation.tsx +++ b/apps/minifront/src/components/syncing-dialog/sync-animation.tsx @@ -1,4 +1,4 @@ -import illustration from './illustration.svg'; +import illustration from './illustration.svg?url'; const FakeButtons = () => (
diff --git a/apps/minifront/src/components/v2/dashboard-layout/index.tsx b/apps/minifront/src/components/v2/dashboard-layout/index.tsx index 9ac58869cd..5f94517895 100644 --- a/apps/minifront/src/components/v2/dashboard-layout/index.tsx +++ b/apps/minifront/src/components/v2/dashboard-layout/index.tsx @@ -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)]: , - [v2PathPrefix(PagePath.TRANSACTIONS)]: , + [getV2Link(PagePath.DASHBOARD)]: , + [getV2Link(PagePath.TRANSACTIONS)]: , }; 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 = () => { @@ -31,12 +29,12 @@ export const DashboardLayout = () => { navigate(value)} options={TABS_OPTIONS} actionType='accent' diff --git a/apps/minifront/src/components/v2/get-v2-link.ts b/apps/minifront/src/components/v2/get-v2-link.ts new file mode 100644 index 0000000000..6947745339 --- /dev/null +++ b/apps/minifront/src/components/v2/get-v2-link.ts @@ -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}`; diff --git a/apps/minifront/src/components/v2/header/desktop-nav.tsx b/apps/minifront/src/components/v2/header/desktop-nav.tsx new file mode 100644 index 0000000000..8c4d9cfb52 --- /dev/null +++ b/apps/minifront/src/components/v2/header/desktop-nav.tsx @@ -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 ( + + ); +}; diff --git a/apps/minifront/src/components/v2/header/index.tsx b/apps/minifront/src/components/v2/header/index.tsx new file mode 100644 index 0000000000..87013fdfc4 --- /dev/null +++ b/apps/minifront/src/components/v2/header/index.tsx @@ -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 ( +
+ + + + + +
+ + +
+
+ +
+
+
+ ); +}; diff --git a/apps/minifront/src/components/v2/header/links.ts b/apps/minifront/src/components/v2/header/links.ts new file mode 100644 index 0000000000..8774308d56 --- /dev/null +++ b/apps/minifront/src/components/v2/header/links.ts @@ -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 }, +]; diff --git a/apps/minifront/src/components/v2/header/logo.svg b/apps/minifront/src/components/v2/header/logo.svg new file mode 100644 index 0000000000..23cebc2ae7 --- /dev/null +++ b/apps/minifront/src/components/v2/header/logo.svg @@ -0,0 +1,3 @@ + + + diff --git a/apps/minifront/src/components/v2/header/logo.tsx b/apps/minifront/src/components/v2/header/logo.tsx new file mode 100644 index 0000000000..3b1b46c3eb --- /dev/null +++ b/apps/minifront/src/components/v2/header/logo.tsx @@ -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 ( + + + + ); +}; diff --git a/apps/minifront/src/components/v2/header/mobile-nav.tsx b/apps/minifront/src/components/v2/header/mobile-nav.tsx new file mode 100644 index 0000000000..53781128b3 --- /dev/null +++ b/apps/minifront/src/components/v2/header/mobile-nav.tsx @@ -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 ( + setIsOpen(false)}> + + +
+ + + +
+ {HEADER_LINKS.map(link => ( + onNavigate(link.value)} + /> + ))} +
+
+
+
+
+ ); +}; diff --git a/apps/minifront/src/components/v2/header/provider-popover.tsx b/apps/minifront/src/components/v2/header/provider-popover.tsx new file mode 100644 index 0000000000..82f3de4e91 --- /dev/null +++ b/apps/minifront/src/components/v2/header/provider-popover.tsx @@ -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 : ( + {manifest?.name} + ); + return () => element; + }, [manifest]); + + const disconnect = () => { + void penumbra.disconnect().then(() => window.location.reload()); + }; + + return ( + + + + + + {manifest ? ( +
+ + {manifest.name} v{manifest.version} + + {manifest.description} +
+ ) : ( + Loading provider manifest... + )} +
+ +
+
+
+ ); +}; diff --git a/apps/minifront/src/components/v2/header/status-popover.tsx b/apps/minifront/src/components/v2/header/status-popover.tsx new file mode 100644 index 0000000000..5decc3448b --- /dev/null +++ b/apps/minifront/src/components/v2/header/status-popover.tsx @@ -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 Block Sync Error; + } + + if (status.percentSyncedNumber === 1) { + return Blocks Synced; + } + + return Block Syncing; + }, [status]); + + return ( + + + + + {status?.isCatchingUp !== undefined && ( + + +
+
+ Status + {pill} + {!!status.error && String(status.error)} +
+
+ Block Height + + {status.latestKnownBlockHeight !== status.fullSyncHeight + ? `${status.fullSyncHeight} of ${status.latestKnownBlockHeight}` + : `${status.latestKnownBlockHeight}`} + +
+
+
+
+ )} +
+ ); +}; diff --git a/apps/minifront/src/components/v2/header/sync-bar.tsx b/apps/minifront/src/components/v2/header/sync-bar.tsx new file mode 100644 index 0000000000..37138332b1 --- /dev/null +++ b/apps/minifront/src/components/v2/header/sync-bar.tsx @@ -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 ( +
+ {status?.isCatchingUp === undefined ? ( + + ) : ( + + )} +
+ ); +}; diff --git a/apps/minifront/src/components/v2/layout.tsx b/apps/minifront/src/components/v2/layout.tsx index f76520945b..45ab15ac59 100644 --- a/apps/minifront/src/components/v2/layout.tsx +++ b/apps/minifront/src/components/v2/layout.tsx @@ -3,10 +3,16 @@ import { HeadTag } from '../metadata/head-tag'; import { Outlet } from 'react-router-dom'; import { Toaster } from '@penumbra-zone/ui/components/ui/toaster'; import { SyncingDialog } from '../syncing-dialog'; +import { Header } from './header'; +import { SyncBar } from './header/sync-bar.tsx'; export const Layout = () => ( + + +
+ diff --git a/apps/minifront/src/components/v2/transfer-layout/index.tsx b/apps/minifront/src/components/v2/transfer-layout/index.tsx index 02930474cf..b590c94994 100644 --- a/apps/minifront/src/components/v2/transfer-layout/index.tsx +++ b/apps/minifront/src/components/v2/transfer-layout/index.tsx @@ -5,13 +5,11 @@ import { Tabs } from '@penumbra-zone/ui/Tabs'; import { usePagePath } from '../../../fetchers/page-path'; import { PagePath } from '../../metadata/paths'; 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 TABS_OPTIONS = [ - { label: 'Send', value: v2PathPrefix(PagePath.SEND) }, - { label: 'Receive', value: v2PathPrefix(PagePath.RECEIVE) }, + { label: 'Send', value: getV2Link(PagePath.SEND) }, + { label: 'Receive', value: getV2Link(PagePath.RECEIVE) }, ]; export const TransferLayout = () => { @@ -26,7 +24,7 @@ export const TransferLayout = () => { navigate(value)} options={TABS_OPTIONS} actionType='accent' diff --git a/apps/minifront/src/prax.ts b/apps/minifront/src/prax.ts index 9157cf02e5..25edba2bb9 100644 --- a/apps/minifront/src/prax.ts +++ b/apps/minifront/src/prax.ts @@ -1,5 +1,6 @@ -import { createPenumbraClient } from '@penumbra-zone/client'; +import { createPenumbraClient, PenumbraManifest } from '@penumbra-zone/client'; import { assertProviderConnected, assertProviderManifest } from '@penumbra-zone/client/assert'; +import { useEffect, useState } from 'react'; const prax_id = 'lkpmkhpnhknhmibgnmmhdhgdilepfghe'; const prax_origin = new URL(`chrome-extension://${prax_id}`).origin; @@ -13,3 +14,16 @@ export const isPraxInstalled = () => () => true, () => false, ); + +export const usePraxManifest = (): PenumbraManifest | undefined => { + const [manifest, setManifest] = useState(); + + useEffect(() => { + setManifest(penumbra.manifest); + penumbra.onConnectionStateChange(() => { + setManifest(penumbra.manifest); + }); + }, []); + + return manifest; +}; diff --git a/apps/minifront/src/state/status.ts b/apps/minifront/src/state/status.ts index 8b4597362c..56c66b4881 100644 --- a/apps/minifront/src/state/status.ts +++ b/apps/minifront/src/state/status.ts @@ -1,4 +1,4 @@ -import { ZQueryState } from '@penumbra-zone/zquery/src/types'; +import { AbridgedZQueryState, ZQueryState } from '@penumbra-zone/zquery/src/types'; import { SliceCreator, useStore } from '.'; import { createZQuery } from '@penumbra-zone/zquery'; import { getStatusStream } from '../fetchers/status'; @@ -37,3 +37,55 @@ export interface StatusSlice { export const createStatusSlice = (): SliceCreator => () => ({ status, }); + +// Copies the logic from the view service's `status` method. +export const statusSelector = ( + zQueryState: AbridgedZQueryState, +): + | { + /** + * - `undefined` when not loaded + * `true` if the sync is behind 10 blocks the current state of blockchain, or if not synced at all + * `false` otherwise + */ + isCatchingUp: undefined; + } + | { + isCatchingUp: boolean; + isUpdating: boolean; + fullSyncHeight: bigint; + latestKnownBlockHeight?: bigint; + percentSynced?: string; + percentSyncedNumber: number; + error: unknown; + } => { + if (!zQueryState.data?.fullSyncHeight) { + return { isCatchingUp: undefined }; + } else { + const { fullSyncHeight, latestKnownBlockHeight } = zQueryState.data; + const isCatchingUp = !latestKnownBlockHeight || latestKnownBlockHeight - fullSyncHeight > 10; + const isUpdating = Boolean( + latestKnownBlockHeight && + latestKnownBlockHeight !== fullSyncHeight && + latestKnownBlockHeight - fullSyncHeight <= 10, + ); + + let percentSyncedNumber = 0; + if (latestKnownBlockHeight) { + percentSyncedNumber = Number(fullSyncHeight) / Number(latestKnownBlockHeight); + if (percentSyncedNumber > 1) { + percentSyncedNumber = 1; + } + } + + return { + error: zQueryState.error, + isCatchingUp, + isUpdating, + fullSyncHeight, + latestKnownBlockHeight, + percentSyncedNumber, + percentSynced: `${percentSyncedNumber * 100}%`, + }; + } +}; diff --git a/apps/minifront/src/svg.d.ts b/apps/minifront/src/svg.d.ts new file mode 100644 index 0000000000..2854559d04 --- /dev/null +++ b/apps/minifront/src/svg.d.ts @@ -0,0 +1,9 @@ +declare module '*.svg' { + import type { FunctionComponent, ComponentProps } from 'react'; + + const ReactComponent: FunctionComponent & { title?: string }> & { + $$typeof: symbol; + }; + + export default ReactComponent; +} diff --git a/apps/minifront/vite.config.ts b/apps/minifront/vite.config.ts index 195904f20a..3931713d39 100644 --- a/apps/minifront/vite.config.ts +++ b/apps/minifront/vite.config.ts @@ -5,12 +5,24 @@ import react from '@vitejs/plugin-react-swc'; import basicSsl from '@vitejs/plugin-basic-ssl'; import { commitInfoPlugin } from './src/utils/commit-info-vite-plugin'; import polyfillNode from 'vite-plugin-node-stdlib-browser'; +import svgr from 'vite-plugin-svgr'; export default defineConfig(({ mode }) => { return { define: { 'globalThis.__DEV__': mode !== 'production' }, clearScreen: false, base: './', - plugins: [polyfillNode(), react(), basicSsl(), commitInfoPlugin()], + plugins: [ + polyfillNode(), + react(), + basicSsl(), + commitInfoPlugin(), + svgr({ + include: '**/*.svg', + svgrOptions: { + exportType: 'default', + }, + }), + ], }; }); diff --git a/package.json b/package.json index 33b4375df3..31f8b5eac8 100644 --- a/package.json +++ b/package.json @@ -82,6 +82,7 @@ "typescript-eslint": "^7.16.0", "vite": "^5.2.11", "vite-plugin-node-stdlib-browser": "^0.2.1", + "vite-plugin-svgr": "^4.2.0", "vite-plugin-top-level-await": "^1.4.1", "vite-plugin-wasm": "^3.3.0", "vitest": "^1.6.0" diff --git a/packages/tailwind-config/index.js b/packages/tailwind-config/index.js index 8ae049341b..e78a25dddb 100644 --- a/packages/tailwind-config/index.js +++ b/packages/tailwind-config/index.js @@ -1,6 +1,9 @@ import plugin from 'tailwindcss/plugin'; import tailwindCssAnimatePlugin from 'tailwindcss-animate'; +// TODO: Replace the theme to v2 instead of partially using it +import { tailwindConfig as v2TailwindConfig } from '@penumbra-zone/ui/tailwind'; + /** @type {import('tailwindcss').Config} */ export default { content: [ @@ -27,6 +30,7 @@ export default { mono: ['Iosevka Term', 'monospace'], }, colors: { + v2: v2TailwindConfig.theme.extend.colors, border: { DEFAULT: 'hsl(var(--border))', secondary: 'var(--border-secondary)', diff --git a/packages/tailwind-config/package.json b/packages/tailwind-config/package.json index 6bb832bf97..39a53868e1 100644 --- a/packages/tailwind-config/package.json +++ b/packages/tailwind-config/package.json @@ -5,6 +5,7 @@ "license": "(MIT OR Apache-2.0)", "main": "index.js", "dependencies": { + "@penumbra-zone/ui": "workspace:*", "tailwindcss": "^3.4.3", "tailwindcss-animate": "^1.0.7" } diff --git a/packages/ui/.storybook/preview.jsx b/packages/ui/.storybook/preview.jsx index b17745781c..f000fcc922 100644 --- a/packages/ui/.storybook/preview.jsx +++ b/packages/ui/.storybook/preview.jsx @@ -28,14 +28,16 @@ const DensityWrapper = ({ children, showDensityControl }) => { > {showDensityControl && ( - + + + )} {children} diff --git a/packages/ui/package.json b/packages/ui/package.json index dd4d05b876..b210cf2a30 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -30,6 +30,7 @@ "./lib/toast/*": "./lib/toast/*.ts", "./lib/toast/transaction-toast": "./lib/toast/transaction-toast.tsx", "./lib/utils": "./lib/utils.ts", + "./tailwind": "./src/tailwindConfig.ts", "./postcss.config.js": "./postcss.config.js", "./styles/*": "./styles/*", "./hooks/*": "./src/hooks/*/index.ts", @@ -37,6 +38,10 @@ }, "publishConfig": { "exports": { + "./tailwind": { + "types": "./dist/src/tailwindConfig.d.ts", + "default": "./dist/src/tailwindConfig.js" + }, "./*": { "types": "./dist/src/*/index.d.ts", "default": "./dist/src/*/index.js" diff --git a/packages/ui/src/Button/index.tsx b/packages/ui/src/Button/index.tsx index b115f1b8b0..300533064b 100644 --- a/packages/ui/src/Button/index.tsx +++ b/packages/ui/src/Button/index.tsx @@ -1,4 +1,4 @@ -import { forwardRef, MouseEventHandler } from 'react'; +import { FC, forwardRef, MouseEventHandler } from 'react'; import styled, { css, DefaultTheme } from 'styled-components'; import { asTransientProps } from '../utils/asTransientProps'; import { Priority, focusOutline, overlays, buttonBase } from '../utils/button'; @@ -142,7 +142,7 @@ interface IconOnlyProps { * * ``` */ - icon: LucideIcon; + icon: LucideIcon | FC; } interface RegularProps { @@ -158,7 +158,7 @@ interface RegularProps { * * ``` */ - icon?: LucideIcon; + icon?: LucideIcon | FC; } export type ButtonProps = BaseButtonProps & (IconOnlyProps | RegularProps) & MotionProp; diff --git a/packages/ui/src/Dialog/index.tsx b/packages/ui/src/Dialog/index.tsx index 26e01f70ed..a939467d29 100644 --- a/packages/ui/src/Dialog/index.tsx +++ b/packages/ui/src/Dialog/index.tsx @@ -16,7 +16,7 @@ const Overlay = styled(RadixDialog.Overlay)` background-color: ${props => props.theme.color.other.overlay}; position: fixed; inset: 0; - z-index: ${props => props.theme.zIndex.dialogOverlay}; + z-index: auto; `; const FullHeightWrapper = styled.div` @@ -43,8 +43,8 @@ const FullHeightWrapper = styled.div` const DialogContent = styled.div` position: fixed; inset: 0; - z-index: ${props => props.theme.zIndex.dialogContent}; pointer-events: none; + z-index: auto; `; const DialogContentCard = styled(motion.div)` @@ -208,6 +208,23 @@ export const Dialog = ({ children, onClose, isOpen }: DialogProps) => { ); }; +export interface DialogEmptyContentProps { + children?: ReactNode; +} + +const EmptyContent = ({ children }: DialogEmptyContentProps) => { + return ( + + + + + {children} + + + ); +}; +Dialog.EmptyContent = EmptyContent; + /** Internal use only. */ const DialogContext = createContext<{ showCloseButton: boolean }>({ showCloseButton: true, @@ -236,49 +253,43 @@ const Content = ({ const { showCloseButton } = useContext(DialogContext); return ( - - + + + + - - - - - - - - - - - - - {title} - - + + + + + + + {title} + + - {showCloseButton && ( - - - - - - )} - + {showCloseButton && ( + + + + + + )} + - {children} + {children} - {buttonGroupProps && } - - - + {buttonGroupProps && } + + + - - - - - - + + + + ); }; Dialog.Content = Content; diff --git a/packages/ui/src/DropdownMenu/CheckboxItem.tsx b/packages/ui/src/DropdownMenu/CheckboxItem.tsx index 85969990df..6dbd6d1bef 100644 --- a/packages/ui/src/DropdownMenu/CheckboxItem.tsx +++ b/packages/ui/src/DropdownMenu/CheckboxItem.tsx @@ -6,7 +6,7 @@ import { ReactNode } from 'react'; import { Check } from 'lucide-react'; import { asTransientProps } from '../utils/asTransientProps.ts'; import { Text } from '../Text'; -import { DropdownMenuItemBase, MenuItem } from './shared.ts'; +import { DropdownMenuItemBase, MenuItem } from '../utils/menuItem.ts'; export interface DropdownMenuCheckboxItemProps extends DropdownMenuItemBase { children?: ReactNode; diff --git a/packages/ui/src/DropdownMenu/Item.tsx b/packages/ui/src/DropdownMenu/Item.tsx index 915e012bc6..c11385b1fa 100644 --- a/packages/ui/src/DropdownMenu/Item.tsx +++ b/packages/ui/src/DropdownMenu/Item.tsx @@ -2,7 +2,7 @@ import type { ReactNode } from 'react'; import { Item as RadixDropdownMenuItem } from '@radix-ui/react-dropdown-menu'; import { asTransientProps } from '../utils/asTransientProps.ts'; import { Text } from '../Text'; -import { DropdownMenuItemBase, MenuItem } from './shared.ts'; +import { DropdownMenuItemBase, MenuItem } from '../utils/menuItem.ts'; export interface DropdownMenuItemProps extends DropdownMenuItemBase { children?: ReactNode; diff --git a/packages/ui/src/DropdownMenu/RadioItem.tsx b/packages/ui/src/DropdownMenu/RadioItem.tsx index 6708900231..63633d4977 100644 --- a/packages/ui/src/DropdownMenu/RadioItem.tsx +++ b/packages/ui/src/DropdownMenu/RadioItem.tsx @@ -6,7 +6,7 @@ import { ReactNode } from 'react'; import { Check } from 'lucide-react'; import { asTransientProps } from '../utils/asTransientProps.ts'; import { Text } from '../Text'; -import { DropdownMenuItemBase, MenuItem } from './shared.ts'; +import { DropdownMenuItemBase, MenuItem } from '../utils/menuItem.ts'; export interface DropdownMenuRadioItemProps extends DropdownMenuItemBase { children?: ReactNode; diff --git a/packages/ui/src/MenuItem/index.stories.tsx b/packages/ui/src/MenuItem/index.stories.tsx new file mode 100644 index 0000000000..e9647c1917 --- /dev/null +++ b/packages/ui/src/MenuItem/index.stories.tsx @@ -0,0 +1,31 @@ +import type { Meta, StoryObj } from '@storybook/react'; + +import { MenuItem } from '.'; +import { ArrowLeftRight, Check, Copy } from 'lucide-react'; + +const meta: Meta = { + component: MenuItem, + tags: ['autodocs', '!dev'], + argTypes: { + icon: { + control: 'select', + options: ['None', 'Copy', 'Check', 'ArrowLeftRight'], + mapping: { None: undefined, Copy, Check, ArrowLeftRight }, + }, + label: { + type: 'string', + }, + onClick: { control: false }, + }, +}; +export default meta; + +type Story = StoryObj; + +export const Basic: Story = { + args: { + label: 'Menu Item', + icon: Check, + disabled: false, + }, +}; diff --git a/packages/ui/src/MenuItem/index.tsx b/packages/ui/src/MenuItem/index.tsx new file mode 100644 index 0000000000..419ad1d50f --- /dev/null +++ b/packages/ui/src/MenuItem/index.tsx @@ -0,0 +1,48 @@ +import type { LucideIcon } from 'lucide-react'; +import type { FC, MouseEventHandler } from 'react'; +import { MenuItem as SharedMenuItem, DropdownMenuItemBase } from '../utils/menuItem'; +import { Text } from '../Text'; +import styled from 'styled-components'; +import { asTransientProps } from '../utils/asTransientProps.ts'; + +const IconAdornment = styled.i` + display: flex; + align-items: center; + justify-content: center; + padding: ${props => props.theme.spacing(1)}; + width: ${props => props.theme.spacing(6)}; + height: ${props => props.theme.spacing(6)}; +`; + +export interface MenuItemProps extends DropdownMenuItemBase { + label: string; + icon?: LucideIcon | FC; + onClick?: MouseEventHandler; +} + +/** + * A button generally used in menus or selectable lists + */ +export const MenuItem = ({ + actionType = 'default', + icon: IconComponent, + label, + onClick, + disabled, +}: MenuItemProps) => { + return ( + + {IconComponent && ( + + + + )} + {label} + + ); +}; diff --git a/packages/ui/src/PenumbraUIProvider/theme.ts b/packages/ui/src/PenumbraUIProvider/theme.ts index 5786cfe7b5..706c0dda1f 100644 --- a/packages/ui/src/PenumbraUIProvider/theme.ts +++ b/packages/ui/src/PenumbraUIProvider/theme.ts @@ -258,8 +258,6 @@ export const theme = { spacing, zIndex: { disabledOverlay: 10, - dialogOverlay: 1000, - dialogContent: 1001, }, } as const; diff --git a/packages/ui/src/Pill/index.stories.tsx b/packages/ui/src/Pill/index.stories.tsx index af9aec0b75..0b5d3c8ad4 100644 --- a/packages/ui/src/Pill/index.stories.tsx +++ b/packages/ui/src/Pill/index.stories.tsx @@ -12,7 +12,7 @@ type Story = StoryObj; export const Basic: Story = { args: { - children: 'Pill', + children: 'Label', priority: 'primary', }, }; diff --git a/packages/ui/src/Pill/index.tsx b/packages/ui/src/Pill/index.tsx index ef4873f4ee..1f02fae387 100644 --- a/packages/ui/src/Pill/index.tsx +++ b/packages/ui/src/Pill/index.tsx @@ -1,46 +1,94 @@ -import styled from 'styled-components'; +import styled, { DefaultTheme } from 'styled-components'; import { asTransientProps } from '../utils/asTransientProps'; import { ReactNode } from 'react'; -import { button } from '../utils/typography'; +import { body, technical, detail, detailTechnical } from '../utils/typography'; import { Density } from '../types/Density'; import { useDensity } from '../hooks/useDensity'; type Priority = 'primary' | 'secondary'; +type Context = + | 'default' + | 'technical-default' + | 'technical-success' + | 'technical-caution' + | 'technical-destructive'; -const TEN_PERCENT_OPACITY_IN_HEX = '1a'; +const getFont = (context: Context, density: Density) => { + if (context === 'default') { + return density === 'sparse' ? body : detail; + } + return density === 'sparse' ? technical : detailTechnical; +}; + +const getXPadding = (priority: Priority, density: Density) => { + let padding = density === 'sparse' ? 3 : 2; + if (priority === 'secondary') { + padding = padding - 0.5; + } + return padding; +}; + +const getBackgroundColor = (theme: DefaultTheme, priority: Priority, context: Context) => { + if (priority === 'secondary') { + return 'transparent'; + } + const colorMap: Record = { + default: theme.color.other.tonalFill10, + 'technical-default': theme.color.other.tonalFill10, + 'technical-success': theme.color.secondary.light, + 'technical-caution': theme.color.caution.light, + 'technical-destructive': theme.color.destructive.light, + }; + return colorMap[context]; +}; + +const getColor = (theme: DefaultTheme, priority: Priority, context: Context) => { + if (priority === 'primary') { + return context === 'default' || context === 'technical-default' + ? theme.color.text.primary + : theme.color.secondary.dark; + } -const Root = styled.span<{ $density: Density; $priority: Priority }>` - ${button} + const colorMap: Record = { + default: theme.color.text.primary, + 'technical-default': theme.color.text.primary, + 'technical-success': theme.color.secondary.light, + 'technical-caution': theme.color.caution.light, + 'technical-destructive': theme.color.destructive.light, + }; + return colorMap[context]; +}; +const Root = styled.span<{ $density: Density; $priority: Priority; $context: Context }>` box-sizing: border-box; - border: 2px dashed - ${props => - props.$priority === 'secondary' ? props.theme.color.other.tonalStroke : 'transparent'}; + + border: ${props => + props.$priority === 'secondary' ? `2px dashed ${props.theme.color.other.tonalStroke}` : 'none'}; border-radius: ${props => props.theme.borderRadius.full}; display: inline-block; max-width: 100%; width: max-content; - padding-top: ${props => props.theme.spacing(props.$density === 'sparse' ? 2 : 1)}; - padding-bottom: ${props => props.theme.spacing(props.$density === 'sparse' ? 2 : 1)}; + padding-top: ${props => props.theme.spacing(props.$priority === 'secondary' ? 0.5 : 1)}; + padding-bottom: ${props => props.theme.spacing(props.$priority === 'secondary' ? 0.5 : 1)}; - padding-left: ${props => props.theme.spacing(props.$density === 'sparse' ? 4 : 2)}; - padding-right: ${props => props.theme.spacing(props.$density === 'sparse' ? 4 : 2)}; + padding-left: ${props => props.theme.spacing(getXPadding(props.$priority, props.$density))}; + padding-right: ${props => props.theme.spacing(getXPadding(props.$priority, props.$density))}; - background-color: ${props => - props.$priority === 'primary' - ? props.theme.color.text.primary + TEN_PERCENT_OPACITY_IN_HEX - : 'transparent'}; + ${props => getFont(props.$context, props.$density)}; + color: ${props => getColor(props.theme, props.$priority, props.$context)}; + background-color: ${props => getBackgroundColor(props.theme, props.$priority, props.$context)}; `; export interface PillProps { children: ReactNode; priority?: Priority; + context?: Context; } -export const Pill = ({ children, priority = 'primary' }: PillProps) => { +export const Pill = ({ children, priority = 'primary', context = 'default' }: PillProps) => { const density = useDensity(); - return {children}; + return {children}; }; diff --git a/packages/ui/src/Progress/index.stories.tsx b/packages/ui/src/Progress/index.stories.tsx new file mode 100644 index 0000000000..de42b78acd --- /dev/null +++ b/packages/ui/src/Progress/index.stories.tsx @@ -0,0 +1,19 @@ +import type { Meta, StoryObj } from '@storybook/react'; + +import { Progress } from '.'; + +const meta: Meta = { + component: Progress, + tags: ['autodocs', '!dev'], +}; +export default meta; + +type Story = StoryObj; + +export const Basic: Story = { + args: { + value: 0.3, + loading: false, + error: false, + }, +}; diff --git a/packages/ui/src/Progress/index.tsx b/packages/ui/src/Progress/index.tsx new file mode 100644 index 0000000000..00f37a3d06 --- /dev/null +++ b/packages/ui/src/Progress/index.tsx @@ -0,0 +1,84 @@ +import * as ProgressPrimitive from '@radix-ui/react-progress'; +import styled, { type DefaultTheme, keyframes } from 'styled-components'; + +export const infiniteLoading = keyframes` + from { + left: -20%; + } + to { + left: 100%; + } +`; + +const Root = styled(ProgressPrimitive.Root)` + position: relative; + width: 100%; + height: ${props => props.theme.spacing(1)}; + background-color: ${props => props.theme.color.other.tonalFill5}; + transition: background-color 0.15s; + overflow: hidden; +`; + +export const getIndicatorColor = (theme: DefaultTheme, value: number, error: boolean): string => { + if (error) { + return theme.color.destructive.light; + } + + if (value === 1) { + return theme.color.secondary.light; + } + + return theme.color.caution.light; +}; + +const Indicator = styled.div<{ $value: number; $error: boolean }>` + height: 100%; + width: 100%; + background-color: ${props => getIndicatorColor(props.theme, props.$value, props.$error)}; + transition: + transform 0.5s cubic-bezier(0.65, 0, 0.35, 1), + background-color 0.15s; + overflow: hidden; +`; + +const Loading = styled.div` + position: absolute; + top: 0; + left: -20%; + width: 20%; + height: 100%; + filter: blur(2px); + background: linear-gradient( + 90deg, + rgba(255, 255, 255, 0) 0%, + #fff 50%, + rgba(255, 255, 255, 0) 100% + ); + animation: ${infiniteLoading} 1s linear infinite; +`; + +export interface ProgressProps { + /** Percentage value from 0 to 1 */ + value: number; + /** Displays the skeleton-like moving shade */ + loading?: boolean; + /** Renders red indicator while the progress continues */ + error?: boolean; +} + +/** + * Progress bar with loading and error states + */ +export const Progress = ({ value, loading, error = false }: ProgressProps) => ( + + + + {loading && } + + + +); diff --git a/packages/ui/src/Tabs/index.stories.tsx b/packages/ui/src/Tabs/index.stories.tsx index 055c3d0d4c..83bc0e3ec1 100644 --- a/packages/ui/src/Tabs/index.stories.tsx +++ b/packages/ui/src/Tabs/index.stories.tsx @@ -5,7 +5,7 @@ import { Tabs } from '.'; const meta: Meta = { component: Tabs, - tags: ['autodocs', '!dev'], + tags: ['autodocs', '!dev', 'density'], argTypes: { value: { control: false }, options: { control: false }, diff --git a/packages/ui/src/Tabs/index.tsx b/packages/ui/src/Tabs/index.tsx index c422354311..c187cc93fd 100644 --- a/packages/ui/src/Tabs/index.tsx +++ b/packages/ui/src/Tabs/index.tsx @@ -1,18 +1,37 @@ -import styled, { DefaultTheme } from 'styled-components'; -import { tab } from '../utils/typography'; +import styled, { css, DefaultTheme } from 'styled-components'; +import { tab, tabSmall } from '../utils/typography'; import { motion } from 'framer-motion'; import { useId } from 'react'; import { buttonBase, overlays } from '../utils/button'; import * as RadixTabs from '@radix-ui/react-tabs'; import { ActionType } from '../utils/ActionType'; +import { useDensity } from '../hooks/useDensity'; +import { Density } from '../types/Density.ts'; -const Root = styled.div` - height: 52px; - padding: ${props => props.theme.spacing(1)}; +const sparse = css` + ${tab}; + flex-grow: 1; + flex-shrink: 1; + flex-basis: 0; /** Ensure equal widths */ + + padding: ${props => props.theme.spacing(2)}; +`; + +const compact = css` + ${tabSmall}; + + padding: ${props => props.theme.spacing(1)} ${props => props.theme.spacing(2)}; +`; + +const Root = styled.div<{ + $density: Density; +}>` display: flex; align-items: stretch; box-sizing: border-box; + gap: ${props => props.theme.spacing(4)}; + height: ${props => (props.$density === 'sparse' ? 44 : 28)}px; `; type LimitedActionType = Exclude; @@ -33,12 +52,11 @@ const Tab = styled.button<{ $actionType: LimitedActionType; $getFocusOutlineColor: (theme: DefaultTheme) => string; $getBorderRadius: (theme: DefaultTheme) => string; + $density: Density; }>` - ${buttonBase} + ${buttonBase}; - flex-grow: 1; - flex-shrink: 1; - flex-basis: 0; /** Ensure equal widths */ + height: 100%; color: ${props => { switch (props.$actionType) { @@ -53,9 +71,10 @@ const Tab = styled.button<{ position: relative; white-space: nowrap; - ${tab} ${overlays} + ${props => (props.$density === 'sparse' ? sparse : compact)} + &:focus-within { outline: none; } @@ -115,11 +134,12 @@ export interface TabsProps { */ export const Tabs = ({ value, onChange, options, actionType = 'default' }: TabsProps) => { const layoutId = useId(); + const density = useDensity(); return ( - + {options.map(option => ( onChange(option.value)} disabled={option.disabled} + $density={density} $actionType={actionType} $getFocusOutlineColor={theme => theme.color.action[outlineColorByActionType[actionType]] diff --git a/packages/ui/src/tailwindConfig.ts b/packages/ui/src/tailwindConfig.ts index f2aec2152d..84dfbbb491 100644 --- a/packages/ui/src/tailwindConfig.ts +++ b/packages/ui/src/tailwindConfig.ts @@ -1,17 +1,15 @@ -import { Config } from 'tailwindcss'; import { theme } from './PenumbraUIProvider/theme'; -import { RecursiveKeyValuePair, ResolvableTo } from 'tailwindcss/types/config'; /** * For consumers using Tailwind, this file exports a Tailwind config based on * the Penumbra UI theme values. */ -export const tailwindConfig: Config = { +export const tailwindConfig = { content: [], theme: { extend: { borderRadius: theme.borderRadius, - colors: theme.color as unknown as ResolvableTo, + colors: theme.color, fontFamily: theme.font, fontSize: theme.fontSize, lineHeight: theme.lineHeight, @@ -27,4 +25,4 @@ export const tailwindConfig: Config = { // Penumbra UI's. }, }, -}; +} as const; diff --git a/packages/ui/src/utils/button.ts b/packages/ui/src/utils/button.ts index 5775c3cb77..9ba3bbf9d4 100644 --- a/packages/ui/src/utils/button.ts +++ b/packages/ui/src/utils/button.ts @@ -62,6 +62,7 @@ export const focusOutline = css<{ /** Adds overlays to a button for when it's hovered, active, or disabled. */ export const overlays = css<{ $getBorderRadius: (theme: DefaultTheme) => string; + $getFocusOutlineColor: (theme: DefaultTheme) => string; }>` position: relative; @@ -72,17 +73,25 @@ export const overlays = css<{ inset: 0; z-index: 1; - transition: background-color 0.15s; + transition: + background-color 0.15s, + outline 0.15s; } - &:hover::before { - background-color: ${props => props.theme.color.action.hoverOverlay}; + @media (hover: hover) { + &:hover::before { + background-color: ${props => props.theme.color.action.hoverOverlay}; + } } &:active::before { background-color: ${props => props.theme.color.action.activeOverlay}; } + &:focus::before { + outline: 2px solid ${props => props.$getFocusOutlineColor(props.theme)}; + } + &:disabled::before { background-color: ${props => props.theme.color.action.disabledOverlay}; cursor: not-allowed; diff --git a/packages/ui/src/DropdownMenu/shared.ts b/packages/ui/src/utils/menuItem.ts similarity index 86% rename from packages/ui/src/DropdownMenu/shared.ts rename to packages/ui/src/utils/menuItem.ts index 239a6db0f6..83792d23d3 100644 --- a/packages/ui/src/DropdownMenu/shared.ts +++ b/packages/ui/src/utils/menuItem.ts @@ -1,9 +1,5 @@ import styled from 'styled-components'; -import { - ActionType, - getColorByActionType, - getOutlineColorByActionType, -} from '../utils/ActionType.ts'; +import { ActionType, getColorByActionType, getOutlineColorByActionType } from './ActionType.ts'; export interface DropdownMenuItemBase { actionType?: ActionType; @@ -36,7 +32,8 @@ export const MenuItem = styled.div` outline: 2px solid ${props => getOutlineColorByActionType(props.theme, props.$actionType)}; } - &[aria-disabled='true'] { + &[aria-disabled='true'], + &:disabled { color: ${props => props.theme.color.text.muted}; } @@ -44,4 +41,8 @@ export const MenuItem = styled.div` &[role='menuitem'] { padding-left: ${props => props.theme.spacing(9)}; } + + & > span { + color: inherit; + } `; diff --git a/packages/ui/src/utils/typography.ts b/packages/ui/src/utils/typography.ts index 8519c0c975..1165babd1c 100644 --- a/packages/ui/src/utils/typography.ts +++ b/packages/ui/src/utils/typography.ts @@ -113,6 +113,15 @@ export const tab = css` line-height: ${props => props.theme.lineHeight.textLg}; `; +export const tabSmall = css` + ${base} + + font-family: ${props => props.theme.font.default}; + font-size: ${props => props.theme.fontSize.textSm}; + font-weight: 500; + line-height: ${props => props.theme.lineHeight.textSm}; +`; + export const tableItem = css` ${base} @@ -135,9 +144,9 @@ export const technical = css` ${base} font-family: ${props => props.theme.font.mono}; - font-size: ${props => props.theme.fontSize.textSm}; + font-size: ${props => props.theme.fontSize.textBase}; font-weight: 500; - line-height: ${props => props.theme.lineHeight.textSm}; + line-height: ${props => props.theme.lineHeight.textBase}; `; export const xxl = css` diff --git a/packages/ui/vite.config.ts b/packages/ui/vite.config.ts index 8112b4a222..d4085881a6 100644 --- a/packages/ui/vite.config.ts +++ b/packages/ui/vite.config.ts @@ -25,6 +25,14 @@ const getAllUIComponents = (): Record => { ); }; +/** Extends the `getAllUIComponents` function to add support for other useful files */ +const getAllEntries = (): Record => { + return { + 'src/tailwindConfig': join(__dirname, 'src', 'tailwindConfig.ts'), + ...getAllUIComponents(), + }; +}; + export default defineConfig({ plugins: [ dts(), @@ -43,7 +51,7 @@ export default defineConfig({ build: { emptyOutDir: true, lib: { - entry: getAllUIComponents(), + entry: getAllEntries(), formats: ['es'], name: '@penumbra-zone/ui', }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c4cf3178d2..51369f5039 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -146,6 +146,9 @@ importers: vite-plugin-node-stdlib-browser: specifier: ^0.2.1 version: 0.2.1(node-stdlib-browser@1.2.0)(rollup@4.18.1)(vite@5.3.3(@types/node@20.14.10)(terser@5.31.1)) + vite-plugin-svgr: + specifier: ^4.2.0 + version: 4.2.0(rollup@4.18.1)(typescript@5.5.3)(vite@5.3.3(@types/node@20.14.10)(terser@5.31.1)) vite-plugin-top-level-await: specifier: ^1.4.1 version: 1.4.1(@swc/helpers@0.5.11)(rollup@4.18.1)(vite@5.3.3(@types/node@20.14.10)(terser@5.31.1)) @@ -550,6 +553,9 @@ importers: packages/tailwind-config: dependencies: + '@penumbra-zone/ui': + specifier: workspace:* + version: link:../ui tailwindcss: specifier: ^3.4.3 version: 3.4.4(ts-node@10.9.2(@swc/core@1.6.13(@swc/helpers@0.5.11))(@types/node@20.14.10)(typescript@5.5.3)) @@ -779,7 +785,7 @@ importers: version: 8.1.11(@types/react-dom@18.3.0)(@types/react@18.3.3)(prettier@3.3.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@storybook/addon-interactions': specifier: ^8.1.1 - version: 8.1.11(vitest@1.6.0(@types/node@20.14.10)(@vitest/browser@1.6.0(playwright@1.45.1)(vitest@1.6.0))(jsdom@24.1.0(bufferutil@4.0.8)(utf-8-validate@5.0.10))(terser@5.31.1)) + version: 8.1.11(vitest@1.6.0(@types/node@20.14.10)(@vitest/browser@1.6.0)(jsdom@24.1.0(bufferutil@4.0.8)(utf-8-validate@5.0.10))(terser@5.31.1)) '@storybook/addon-links': specifier: ^8.1.1 version: 8.1.11(react@18.3.1) @@ -4610,6 +4616,74 @@ packages: '@storybook/types@8.1.11': resolution: {integrity: sha512-k9N5iRuY2+t7lVRL6xeu6diNsxO3YI3lS4Juv3RZ2K4QsE/b3yG5ElfJB8DjHDSHwRH4ORyrU71KkOCUVfvtnw==} + '@svgr/babel-plugin-add-jsx-attribute@8.0.0': + resolution: {integrity: sha512-b9MIk7yhdS1pMCZM8VeNfUlSKVRhsHZNMl5O9SfaX0l0t5wjdgu4IDzGB8bpnGBBOjGST3rRFVsaaEtI4W6f7g==} + engines: {node: '>=14'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@svgr/babel-plugin-remove-jsx-attribute@8.0.0': + resolution: {integrity: sha512-BcCkm/STipKvbCl6b7QFrMh/vx00vIP63k2eM66MfHJzPr6O2U0jYEViXkHJWqXqQYjdeA9cuCl5KWmlwjDvbA==} + engines: {node: '>=14'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@svgr/babel-plugin-remove-jsx-empty-expression@8.0.0': + resolution: {integrity: sha512-5BcGCBfBxB5+XSDSWnhTThfI9jcO5f0Ai2V24gZpG+wXF14BzwxxdDb4g6trdOux0rhibGs385BeFMSmxtS3uA==} + engines: {node: '>=14'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@svgr/babel-plugin-replace-jsx-attribute-value@8.0.0': + resolution: {integrity: sha512-KVQ+PtIjb1BuYT3ht8M5KbzWBhdAjjUPdlMtpuw/VjT8coTrItWX6Qafl9+ji831JaJcu6PJNKCV0bp01lBNzQ==} + engines: {node: '>=14'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@svgr/babel-plugin-svg-dynamic-title@8.0.0': + resolution: {integrity: sha512-omNiKqwjNmOQJ2v6ge4SErBbkooV2aAWwaPFs2vUY7p7GhVkzRkJ00kILXQvRhA6miHnNpXv7MRnnSjdRjK8og==} + engines: {node: '>=14'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@svgr/babel-plugin-svg-em-dimensions@8.0.0': + resolution: {integrity: sha512-mURHYnu6Iw3UBTbhGwE/vsngtCIbHE43xCRK7kCw4t01xyGqb2Pd+WXekRRoFOBIY29ZoOhUCTEweDMdrjfi9g==} + engines: {node: '>=14'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@svgr/babel-plugin-transform-react-native-svg@8.1.0': + resolution: {integrity: sha512-Tx8T58CHo+7nwJ+EhUwx3LfdNSG9R2OKfaIXXs5soiy5HtgoAEkDay9LIimLOcG8dJQH1wPZp/cnAv6S9CrR1Q==} + engines: {node: '>=14'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@svgr/babel-plugin-transform-svg-component@8.0.0': + resolution: {integrity: sha512-DFx8xa3cZXTdb/k3kfPeaixecQLgKh5NVBMwD0AQxOzcZawK4oo1Jh9LbrcACUivsCA7TLG8eeWgrDXjTMhRmw==} + engines: {node: '>=12'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@svgr/babel-preset@8.1.0': + resolution: {integrity: sha512-7EYDbHE7MxHpv4sxvnVPngw5fuR6pw79SkcrILHJ/iMpuKySNCl5W1qcwPEpU+LgyRXOaAFgH0KhwD18wwg6ug==} + engines: {node: '>=14'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@svgr/core@8.1.0': + resolution: {integrity: sha512-8QqtOQT5ACVlmsvKOJNEaWmRPmcojMOzCz4Hs2BGG/toAp/K38LcsMRyLp349glq5AzJbCEeimEoxaX6v/fLrA==} + engines: {node: '>=14'} + + '@svgr/hast-util-to-babel-ast@8.0.0': + resolution: {integrity: sha512-EbDKwO9GpfWP4jN9sGdYwPBU0kdomaPIL2Eu4YwmgP+sJeXT+L7bMwJUBnhzfH8Q2qMBqZ4fJwpCyYsAN3mt2Q==} + engines: {node: '>=14'} + + '@svgr/plugin-jsx@8.1.0': + resolution: {integrity: sha512-0xiIyBsLlr8quN+WyuxooNW9RJ0Dpr8uOnH/xrCVO8GLUcwHISwj1AG0k+LFzteTkAA0GbX0kj9q6Dk70PTiPA==} + engines: {node: '>=14'} + peerDependencies: + '@svgr/core': '*' + '@swc/core-darwin-arm64@1.6.13': resolution: {integrity: sha512-SOF4buAis72K22BGJ3N8y88mLNfxLNprTuJUpzikyMGrvkuBFNcxYtMhmomO0XHsgLDzOJ+hWzcgjRNzjMsUcQ==} engines: {node: '>=10'} @@ -5898,6 +5972,10 @@ packages: resolution: {integrity: sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==} engines: {node: '>=6'} + camelcase@6.3.0: + resolution: {integrity: sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==} + engines: {node: '>=10'} + camelize@1.0.1: resolution: {integrity: sha512-dU+Tx2fsypxTgtLoE36npi3UqcjSSMNYfkqgmoEhtZrraP5VWq0K7FkWVTYa8eMPtnU/G2txVsfdCJTn9uzpuQ==} @@ -6149,6 +6227,15 @@ packages: resolution: {integrity: sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA==} engines: {node: '>=10'} + cosmiconfig@8.3.6: + resolution: {integrity: sha512-kcZ6+W5QzcJ3P1Mt+83OUv/oHFqZHIx8DuxG6eZ5RGMERoLqp4BuGjhHLYGK+Kf5XVkQvqBSmAy/nGWN3qDgEA==} + engines: {node: '>=14'} + peerDependencies: + typescript: 5.5.3 + peerDependenciesMeta: + typescript: + optional: true + cosmiconfig@9.0.0: resolution: {integrity: sha512-itvL5h8RETACmOTFc4UfIyB2RfEHi71Ax6E/PivVxq9NseKbOWpeyHEOIbmAw1rs8Ak0VursQNww7lf7YtUwzg==} engines: {node: '>=14'} @@ -6517,6 +6604,9 @@ packages: dot-case@2.1.1: resolution: {integrity: sha512-HnM6ZlFqcajLsyudHq7LeeLDr2rFAVYtDv/hV5qchQEidSck8j9OPUsXY9KwJv/lHMtYlX4DjRQqwFYa+0r8Ug==} + dot-case@3.0.4: + resolution: {integrity: sha512-Kv5nKlh6yRrdrGvxeJ2e5y2eRUpkUosIW4A2AS38zwSz27zu7ufDwQPi5Jhs3XAlGNetl3bmnGhQsMtkKJnj3w==} + dotenv-expand@10.0.0: resolution: {integrity: sha512-GopVGCpVS1UKH75VKHGuQFqS1Gusej0z4FyQkPdwjil2gNIv+LNsqBlboOzpJFZKVT95GkCyWJbBSdFEFUWI2A==} engines: {node: '>=12'} @@ -7950,6 +8040,9 @@ packages: lower-case@1.1.4: resolution: {integrity: sha512-2Fgx1Ycm599x+WGpIYwJOvsjmXFzTSc34IwDWALRA/8AopUKAVPwfJ+h5+f85BCp0PWmmJcWzEpxOpoXycMpdA==} + lower-case@2.0.2: + resolution: {integrity: sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg==} + lru-cache@10.4.3: resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} @@ -8199,6 +8292,9 @@ packages: no-case@2.3.2: resolution: {integrity: sha512-rmTZ9kz+f3rCvK2TD1Ue/oZlns7OGoIWP4fc3llxxRXlOkHKoWPPWJOfFYpITabSow43QJbRIoHQXtt10VldyQ==} + no-case@3.0.4: + resolution: {integrity: sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg==} + nock@13.5.4: resolution: {integrity: sha512-yAyTfdeNJGGBFxWdzSKCBYxs5FxLbCg5X5Q4ets974hcQzG1+qCxvIyOo4j2Ry6MUlhWVMX4OoYDefAIIwupjw==} engines: {node: '>= 10.13'} @@ -9333,6 +9429,9 @@ packages: snake-case@2.1.0: resolution: {integrity: sha512-FMR5YoPFwOLuh4rRz92dywJjyKYZNLpMn1R5ujVpIYkbA9p01fq8RMg0FkO4M+Yobt4MjHeLTJVm5xFFBHSV2Q==} + snake-case@3.0.4: + resolution: {integrity: sha512-LAOh4z89bGQvl9pFfNF8V146i7o7/CqFPbqzYgP+yYzDIDeS9HaNFtXABamRW+AQzEVODcvE79ljJ+8a9YSdMg==} + socks-proxy-agent@8.0.4: resolution: {integrity: sha512-GNAq/eg8Udq2x0eNiFkr9gRg5bA7PXEWagQdeRX4cPSG+X/8V38v637gim9bjFptMk1QWsCTr0ttrJEiXbNnRw==} engines: {node: '>= 14'} @@ -9563,6 +9662,9 @@ packages: resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} engines: {node: '>= 0.4'} + svg-parser@2.0.4: + resolution: {integrity: sha512-e4hG1hRwoOdRb37cIMSgzNsxyzKfayW6VOflrwvR+/bzrkyxY/31WkbgnQpgtrNp1SdpJvpUAGTa/ZoiPNDuRQ==} + swap-case@1.1.2: resolution: {integrity: sha512-BAmWG6/bx8syfc6qXPprof3Mn5vQgf5dwdUNJhsNqU9WdPt5P+ES/wQ5bxfijy8zwZgZZHslC3iAsxsuQMCzJQ==} @@ -10167,6 +10269,11 @@ packages: node-stdlib-browser: ^1.2.0 vite: ^2.0.0 || ^3.0.0 || ^4.0.0 + vite-plugin-svgr@4.2.0: + resolution: {integrity: sha512-SC7+FfVtNQk7So0XMjrrtLAbEC8qjFPifyD7+fs/E6aaNdVde6umlVVh0QuwDLdOMu7vp5RiGFsB70nj5yo0XA==} + peerDependencies: + vite: ^2.6.0 || 3 || 4 || 5 + vite-plugin-top-level-await@1.4.1: resolution: {integrity: sha512-hogbZ6yT7+AqBaV6lK9JRNvJDn4/IJvHLu6ET06arNfo0t2IsyCaon7el9Xa8OumH+ESuq//SDf8xscZFE0rWw==} peerDependencies: @@ -16171,11 +16278,11 @@ snapshots: dependencies: '@storybook/global': 5.0.0 - '@storybook/addon-interactions@8.1.11(vitest@1.6.0(@types/node@20.14.10)(@vitest/browser@1.6.0(playwright@1.45.1)(vitest@1.6.0))(jsdom@24.1.0(bufferutil@4.0.8)(utf-8-validate@5.0.10))(terser@5.31.1))': + '@storybook/addon-interactions@8.1.11(vitest@1.6.0(@types/node@20.14.10)(@vitest/browser@1.6.0)(jsdom@24.1.0(bufferutil@4.0.8)(utf-8-validate@5.0.10))(terser@5.31.1))': dependencies: '@storybook/global': 5.0.0 '@storybook/instrumenter': 8.1.11 - '@storybook/test': 8.1.11(vitest@1.6.0(@types/node@20.14.10)(@vitest/browser@1.6.0(playwright@1.45.1)(vitest@1.6.0))(jsdom@24.1.0(bufferutil@4.0.8)(utf-8-validate@5.0.10))(terser@5.31.1)) + '@storybook/test': 8.1.11(vitest@1.6.0(@types/node@20.14.10)(@vitest/browser@1.6.0)(jsdom@24.1.0(bufferutil@4.0.8)(utf-8-validate@5.0.10))(terser@5.31.1)) '@storybook/types': 8.1.11 polished: 4.3.1 ts-dedent: 2.2.0 @@ -16979,14 +17086,14 @@ snapshots: - prettier - supports-color - '@storybook/test@8.1.11(vitest@1.6.0(@types/node@20.14.10)(@vitest/browser@1.6.0(playwright@1.45.1)(vitest@1.6.0))(jsdom@24.1.0(bufferutil@4.0.8)(utf-8-validate@5.0.10))(terser@5.31.1))': + '@storybook/test@8.1.11(vitest@1.6.0(@types/node@20.14.10)(@vitest/browser@1.6.0)(jsdom@24.1.0(bufferutil@4.0.8)(utf-8-validate@5.0.10))(terser@5.31.1))': dependencies: '@storybook/client-logger': 8.1.11 '@storybook/core-events': 8.1.11 '@storybook/instrumenter': 8.1.11 '@storybook/preview-api': 8.1.11 '@testing-library/dom': 10.1.0 - '@testing-library/jest-dom': 6.4.5(vitest@1.6.0(@types/node@20.14.10)(@vitest/browser@1.6.0(playwright@1.45.1)(vitest@1.6.0))(jsdom@24.1.0(bufferutil@4.0.8)(utf-8-validate@5.0.10))(terser@5.31.1)) + '@testing-library/jest-dom': 6.4.5(vitest@1.6.0(@types/node@20.14.10)(@vitest/browser@1.6.0)(jsdom@24.1.0(bufferutil@4.0.8)(utf-8-validate@5.0.10))(terser@5.31.1)) '@testing-library/user-event': 14.5.2(@testing-library/dom@10.1.0) '@vitest/expect': 1.6.0 '@vitest/spy': 1.6.0 @@ -17020,6 +17127,76 @@ snapshots: '@types/express': 4.17.21 file-system-cache: 2.3.0 + '@svgr/babel-plugin-add-jsx-attribute@8.0.0(@babel/core@7.24.7)': + dependencies: + '@babel/core': 7.24.7 + + '@svgr/babel-plugin-remove-jsx-attribute@8.0.0(@babel/core@7.24.7)': + dependencies: + '@babel/core': 7.24.7 + + '@svgr/babel-plugin-remove-jsx-empty-expression@8.0.0(@babel/core@7.24.7)': + dependencies: + '@babel/core': 7.24.7 + + '@svgr/babel-plugin-replace-jsx-attribute-value@8.0.0(@babel/core@7.24.7)': + dependencies: + '@babel/core': 7.24.7 + + '@svgr/babel-plugin-svg-dynamic-title@8.0.0(@babel/core@7.24.7)': + dependencies: + '@babel/core': 7.24.7 + + '@svgr/babel-plugin-svg-em-dimensions@8.0.0(@babel/core@7.24.7)': + dependencies: + '@babel/core': 7.24.7 + + '@svgr/babel-plugin-transform-react-native-svg@8.1.0(@babel/core@7.24.7)': + dependencies: + '@babel/core': 7.24.7 + + '@svgr/babel-plugin-transform-svg-component@8.0.0(@babel/core@7.24.7)': + dependencies: + '@babel/core': 7.24.7 + + '@svgr/babel-preset@8.1.0(@babel/core@7.24.7)': + dependencies: + '@babel/core': 7.24.7 + '@svgr/babel-plugin-add-jsx-attribute': 8.0.0(@babel/core@7.24.7) + '@svgr/babel-plugin-remove-jsx-attribute': 8.0.0(@babel/core@7.24.7) + '@svgr/babel-plugin-remove-jsx-empty-expression': 8.0.0(@babel/core@7.24.7) + '@svgr/babel-plugin-replace-jsx-attribute-value': 8.0.0(@babel/core@7.24.7) + '@svgr/babel-plugin-svg-dynamic-title': 8.0.0(@babel/core@7.24.7) + '@svgr/babel-plugin-svg-em-dimensions': 8.0.0(@babel/core@7.24.7) + '@svgr/babel-plugin-transform-react-native-svg': 8.1.0(@babel/core@7.24.7) + '@svgr/babel-plugin-transform-svg-component': 8.0.0(@babel/core@7.24.7) + + '@svgr/core@8.1.0(typescript@5.5.3)': + dependencies: + '@babel/core': 7.24.7 + '@svgr/babel-preset': 8.1.0(@babel/core@7.24.7) + camelcase: 6.3.0 + cosmiconfig: 8.3.6(typescript@5.5.3) + snake-case: 3.0.4 + transitivePeerDependencies: + - supports-color + - typescript + + '@svgr/hast-util-to-babel-ast@8.0.0': + dependencies: + '@babel/types': 7.24.7 + entities: 4.5.0 + + '@svgr/plugin-jsx@8.1.0(@svgr/core@8.1.0(typescript@5.5.3))': + dependencies: + '@babel/core': 7.24.7 + '@svgr/babel-preset': 8.1.0(@babel/core@7.24.7) + '@svgr/core': 8.1.0(typescript@5.5.3) + '@svgr/hast-util-to-babel-ast': 8.0.0 + svg-parser: 2.0.4 + transitivePeerDependencies: + - supports-color + '@swc/core-darwin-arm64@1.6.13': optional: true @@ -17199,7 +17376,7 @@ snapshots: lz-string: 1.5.0 pretty-format: 27.5.1 - '@testing-library/jest-dom@6.4.5(vitest@1.6.0(@types/node@20.14.10)(@vitest/browser@1.6.0(playwright@1.45.1)(vitest@1.6.0))(jsdom@24.1.0(bufferutil@4.0.8)(utf-8-validate@5.0.10))(terser@5.31.1))': + '@testing-library/jest-dom@6.4.5(vitest@1.6.0(@types/node@20.14.10)(@vitest/browser@1.6.0)(jsdom@24.1.0(bufferutil@4.0.8)(utf-8-validate@5.0.10))(terser@5.31.1))': dependencies: '@adobe/css-tools': 4.4.0 '@babel/runtime': 7.24.7 @@ -18791,6 +18968,8 @@ snapshots: camelcase@5.3.1: {} + camelcase@6.3.0: {} + camelize@1.0.1: {} caniuse-lite@1.0.30001640: {} @@ -19049,6 +19228,15 @@ snapshots: path-type: 4.0.0 yaml: 1.10.2 + cosmiconfig@8.3.6(typescript@5.5.3): + dependencies: + import-fresh: 3.3.0 + js-yaml: 4.1.0 + parse-json: 5.2.0 + path-type: 4.0.0 + optionalDependencies: + typescript: 5.5.3 + cosmiconfig@9.0.0(typescript@5.5.3): dependencies: env-paths: 2.2.1 @@ -19462,6 +19650,11 @@ snapshots: dependencies: no-case: 2.3.2 + dot-case@3.0.4: + dependencies: + no-case: 3.0.4 + tslib: 2.6.3 + dotenv-expand@10.0.0: {} dotenv@16.0.3: {} @@ -21224,6 +21417,10 @@ snapshots: lower-case@1.1.4: {} + lower-case@2.0.2: + dependencies: + tslib: 2.6.3 + lru-cache@10.4.3: {} lru-cache@4.1.5: @@ -21428,6 +21625,11 @@ snapshots: dependencies: lower-case: 1.1.4 + no-case@3.0.4: + dependencies: + lower-case: 2.0.2 + tslib: 2.6.3 + nock@13.5.4: dependencies: debug: 4.3.5 @@ -22805,6 +23007,11 @@ snapshots: dependencies: no-case: 2.3.2 + snake-case@3.0.4: + dependencies: + dot-case: 3.0.4 + tslib: 2.6.3 + socks-proxy-agent@8.0.4: dependencies: agent-base: 7.1.1 @@ -23062,6 +23269,8 @@ snapshots: supports-preserve-symlinks-flag@1.0.0: {} + svg-parser@2.0.4: {} + swap-case@1.1.2: dependencies: lower-case: 1.1.4 @@ -23689,6 +23898,17 @@ snapshots: transitivePeerDependencies: - rollup + vite-plugin-svgr@4.2.0(rollup@4.18.1)(typescript@5.5.3)(vite@5.3.3(@types/node@20.14.10)(terser@5.31.1)): + dependencies: + '@rollup/pluginutils': 5.1.0(rollup@4.18.1) + '@svgr/core': 8.1.0(typescript@5.5.3) + '@svgr/plugin-jsx': 8.1.0(@svgr/core@8.1.0(typescript@5.5.3)) + vite: 5.3.3(@types/node@20.14.10)(terser@5.31.1) + transitivePeerDependencies: + - rollup + - supports-color + - typescript + vite-plugin-top-level-await@1.4.1(@swc/helpers@0.5.11)(rollup@4.18.1)(vite@5.3.3(@types/node@20.14.10)(terser@5.31.1)): dependencies: '@rollup/plugin-virtual': 3.0.2(rollup@4.18.1)