From 9dd7295cd2aeccf6cc271459c8abd4ca80a9460e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=A8=8B=E5=BA=8F=E5=91=98=E9=98=BF=E6=B1=9F=28Relakkes?= =?UTF-8?q?=29?= Date: Sun, 26 Jul 2026 16:03:44 +0800 Subject: [PATCH] feat(desktop): add a dev-only component gallery Renders every `components/ui` primitive under each of the three themes at `/gallery.html`, reachable under `bun run dev` with no backend. Unit tests assert structure and ARIA. They cannot tell whether a token resolves to a readable color, whether an overlay lands above the thing it is meant to cover, or whether an entrance animation actually plays. Three defects surfaced here that had a full green suite behind them: - `Button` and `IconButton` swallowed refs, so `Tooltip` had nothing to measure and `Dropdown` could not return focus. Console warning only. - The light theme's warning badge sat at 2.66:1. - `Dropdown`'s Escape closed the dialog behind it as well. Vite's build input is `index.html` alone, so this never enters the production bundle. --- desktop/gallery.html | 12 ++ desktop/src/dev/ComponentGallery.tsx | 300 +++++++++++++++++++++++++++ desktop/src/dev/gallery.tsx | 9 + 3 files changed, 321 insertions(+) create mode 100644 desktop/gallery.html create mode 100644 desktop/src/dev/ComponentGallery.tsx create mode 100644 desktop/src/dev/gallery.tsx diff --git a/desktop/gallery.html b/desktop/gallery.html new file mode 100644 index 00000000..87e6706a --- /dev/null +++ b/desktop/gallery.html @@ -0,0 +1,12 @@ + + + + + + components/ui gallery + + +
+ + + diff --git a/desktop/src/dev/ComponentGallery.tsx b/desktop/src/dev/ComponentGallery.tsx new file mode 100644 index 00000000..f85f95fb --- /dev/null +++ b/desktop/src/dev/ComponentGallery.tsx @@ -0,0 +1,300 @@ +import { useEffect, useState } from 'react' + +import { Badge, StatusDot, type Tone } from '@/components/ui/Badge' +import { Button, type ButtonSize, type ButtonVariant } from '@/components/ui/Button' +import { Card } from '@/components/ui/Card' +import { Checkbox } from '@/components/ui/Checkbox' +import { Dropdown } from '@/components/ui/Dropdown' +import { EmptyState } from '@/components/ui/EmptyState' +import { ErrorState } from '@/components/ui/ErrorState' +import { IconButton, type IconButtonSize, type IconButtonTone } from '@/components/ui/IconButton' +import { Input } from '@/components/ui/Input' +import { LoadingState } from '@/components/ui/LoadingState' +import { MobileBottomSheet } from '@/components/ui/MobileBottomSheet' +import { Modal } from '@/components/ui/Modal' +import { Progress } from '@/components/ui/Progress' +import { SearchField } from '@/components/ui/SearchField' +import { SegmentedControl } from '@/components/ui/SegmentedControl' +import { SelectField } from '@/components/ui/SelectField' +import { SkeletonCards, SkeletonRows } from '@/components/ui/Skeleton' +import { Spinner } from '@/components/ui/Spinner' +import { Switch } from '@/components/ui/Switch' +import { TextArea } from '@/components/ui/TextArea' +import { Tooltip } from '@/components/ui/Tooltip' + +/** + * A dev-only page rendering every `components/ui` primitive under each theme. + * + * Unit tests assert structure and ARIA; they cannot tell whether a token + * resolves to a readable color, whether an overlay lands above the thing it is + * supposed to cover, or whether an entrance animation actually plays. This page + * is where those get checked by eye, and it takes no backend to open. + * + * Reachable at `/gallery.html` under `bun run dev` only — it is not referenced + * from the app and never enters the production bundle. + */ + +const THEMES = ['white', 'light', 'dark'] as const +const TONES: Tone[] = ['neutral', 'brand', 'success', 'warning', 'danger', 'info'] +const VARIANTS: ButtonVariant[] = ['primary', 'secondary', 'tonal', 'ghost', 'danger', 'danger-outline', 'link', 'inverse'] +const SIZES: ButtonSize[] = ['xs', 'sm', 'base', 'md', 'lg'] +const ICON_SIZES: IconButtonSize[] = ['2xs', 'xs', 'sm', 'md', 'lg', 'xl', '2xl'] +const ICON_TONES: IconButtonTone[] = ['default', 'secondary', 'muted', 'brand', 'danger'] + +function Section({ title, note, children }: { title: string; note?: string; children: React.ReactNode }) { + return ( +
+
+

{title}

+ {note &&

{note}

} +
+ {children} +
+ ) +} + +export function ComponentGallery() { + const [theme, setTheme] = useState<(typeof THEMES)[number]>('white') + const [modalOpen, setModalOpen] = useState(false) + const [sheetOpen, setSheetOpen] = useState(false) + const [search, setSearch] = useState('') + const [segment, setSegment] = useState('all') + const [checked, setChecked] = useState(true) + const [switched, setSwitched] = useState(true) + const [model, setModel] = useState('sonnet') + const [transport, setTransport] = useState('stdio') + + useEffect(() => { + document.documentElement.setAttribute('data-theme', theme) + document.documentElement.style.colorScheme = theme === 'dark' ? 'dark' : 'light' + }, [theme]) + + return ( +
+
+
+

components/ui gallery

+ ({ value, label: value }))} + value={theme} + onChange={setTheme} + label="Theme" + data-testid="theme-switch" + /> +
+
+ +
+
+ {VARIANTS.map((variant) => ( +
+ {variant} + {SIZES.map((size) => ( + + ))} + + +
+ ))} +
+
+ +
+
+ {ICON_TONES.map((tone) => ( +
+ {tone} + {ICON_SIZES.map((size) => ( + + ))} + + + + +
+ ))} +
+ states + + + + + + +
+ {/* `solid` has to stay legible over arbitrary content — this strip + stands in for a user-supplied image behind a remove badge. */} +
+ solid over image + + +
+
+ on sidebar + {/* The sidebar hover token differs from surface-hover in all three + themes; hover these against the sidebar fill to check it. */} + {ICON_TONES.map((tone) => ( + + ))} +
+
+
+ +
+
+ {TONES.map((tone) => {tone})} +
+
+ {TONES.map((tone) => {tone})} +
+
+ {TONES.map((tone) => {tone})} +
+
+ {TONES.map((tone) => ( + + + {tone} + + ))} +
+
+ +
+
+ + + + + + +