Spec 022 — Storybook design-system workbench
Status: implemented Branch: 022-storybook
Status is set by the human, never by the agent. It moves draft → approved → implemented.
Problem
Spec 019 shipped four closed design-system components (Button, Input, Dialog, Tooltip) and rendered them from a hand-maintained /ui route (features/ui-gallery/), whose own header comment marks it a stopgap "until Storybook lands." design-system.md names Storybook as the intended catalogue home in two places. As more components land, that stopgap stops paying for itself: it is a single page a developer edits by hand, with no per-component isolation, no interactive controls to sweep a prop across its values, and no way to view a component on its own while iterating on it. There is no dedicated surface for building and reviewing a component in isolation — which is exactly what a growing design system needs.
User stories
Ordered by priority. Each story must be independently testable and shippable — if only P1 ships, there is still something usable.
P1 — A local Storybook renders the four components, correctly styled, for iteration
As the developer, I want to run a component workbench locally that renders each design-system component in isolation with its Panda styling applied, so that I can build and review a component on its own — sweeping its variants and states — instead of hand-editing one shared gallery page.
Independent test: with only this story implemented, pnpm --filter @gw2priory/web storybook boots and serves a Storybook whose sidebar lists the four components; each renders with Panda tokens applied (not unstyled) and can be viewed in both the light and _osDark colour schemes. The /ui route still exists at this point — retiring it is P2.
Acceptance scenarios
- Given the repository, when the developer runs the workbench script, then Storybook starts locally and its sidebar lists
Button,Input,DialogandTooltip. - Given a component's story, when it renders in the preview, then its Panda recipe styling is applied — the same tokens the app resolves, no unstyled fallback — because the generated CSS and token layer are wired into the preview.
- Given any story, when the browser is put into
prefers-color-scheme: dark(OS setting or DevTools media emulation), then the component reflects the_osDarktoken values (e.g.surface,text.strong), proving the dark layer is present in the preview. (There is no bespoke light/dark toolbar toggle —_osDarkis media-query-driven, not class-driven; research.md V2.) - Given a component with variants (e.g.
Button'svariant/size/disabled), when its story is opened, then those variants are reachable — as interactive controls, separate stories, or both — so a prop can be swept without editing code. - Given a story file, when it is written, then it imports the finished component from
shared/uiand nothing from@base-ui/reactorstyled-system, honouring the closed-component boundary (spec 019 R2).
P2 — The /ui stopgap is retired
As the developer, I want the interim /ui gallery removed once Storybook covers the same four components, so there is one component surface rather than two that drift apart.
Independent test: with this story added, features/ui-gallery/ no longer exists, the /ui route is gone from the router, and the app builds and runs without it; Storybook is the only component-catalogue surface.
Acceptance scenarios
- Given the app, when it is built and run, then there is no
/uiroute and nofeatures/ui-gallery/folder (page, route table, styles, test all removed). - Given
main.tsx, when it assembles the router, then it no longer mounts the ui-gallery route table. - Given
design-system.md(and any other doc that pointed at/ui), when it is read, then it states Storybook is the component catalogue and the/uistopgap has been retired — true of the repository at merge, not an intention.
Requirements
The workbench
- R1 — Storybook (the
@storybook/react-viteframework, its Vite builder) is added as a dev dependency ofapps/web, with astorybookscript that starts it locally. It is a local developer tool only: no CI job builds or deploys it, no static host, no visual-regression runner. Confirmed — research.md V1:storybook@10.5.8+@storybook/react-vite@10.5.8ranstorybook buildon this app's Vite 8.1.5 (Rolldown/Oxc) with no peer conflict; the plan pins the exact version. - R2 — Storybook config lives in
apps/web/.storybook/(amainand apreview). The preview loads Panda's generated CSS and token layer (by importingsrc/index.css) so components render with the same styling the app gives them; the_osDarklayer renders underprefers-color-scheme: dark(OS/DevTools emulation), with no class-based theme toggle added. Confirmed — research.md V2: the built preview CSS carries the--colors-*tokens, thebutton--*recipe classes, and the@media (prefers-color-scheme: dark)block. F1: this needs the PostCSS config in array syntax, which stays backward-compatible with the app's ownvite build. - R3 — One colocated story file per component —
Button.stories.tsx,Input.stories.tsx,Dialog.stories.tsx,Tooltip.stories.tsx— beside its component inapps/web/src/shared/ui/. Each exposes the component's documented variants/states (mirroring what/uishows today) via controls and/or separate stories, imports the finished component, and imports nothing from@base-ui/reactorstyled-system.
Fitting the existing enforcement
- R4 — Biome's
useFilenamingConvention(which reads the filename segment before the first dot, soButton.stories.tsxis checked asButton) is given a scopedapps/web/**/*.stories.tsxoverride withfilenameCases: ["export", "camelCase", "PascalCase"], placed after the__tests__override, exactly as that override admits PascalCase test files. Confirmed — research.md V3: the committed config rejectsButton.stories.tsx; this override makesbiome checkpass, and — since overrides are per-rule — thestyled-systemimport ban still binds, so a story cannot importstyled-system(layout uses plain elements/decorators, per R3). - R5 — Adding Storybook and the story files leaves the app's own pipeline green: the React Compiler build (
pnpm build,panicThreshold: 'all_errors') still passes, the story files typecheck (tsconfig includessrc/**/*.tsx), and Vitest does not pick the stories up as test files (itsincludeis*.test.ts(x)only). Confirmed — research.md V4: the Storybook build loads the appvite.config.ts, so the compiler runs in Storybook too (useMemoCachein the built story chunk) andstorybook buildstill passes — sopanicThresholdprotects the Storybook build as well; noviteFinalis needed to strip the compiler.
Retiring the stopgap
- R6 —
features/ui-gallery/(page,routes.tsx,styles.ts, test) is deleted and its route table is removed frommain.tsx. Any test that asserts the/uiroute (e.g.App.test.tsx) is updated so the suite stays green. - R7 —
design-system.md's "Interim surface" note is rewritten: Storybook is the catalogue home and the/uigallery has been retired. Any other doc line pointing at/uias the surface (e.g. inreact.md) is updated to match. Both must compile underpnpm docs:build.
HTTP contract
No endpoint is added, removed or changed by this spec, and apps/api/openapi.json is untouched. The work is confined to apps/web tooling and docs/; this feature has no HTTP surface. pnpm verify:contract must stay green throughout.
Success criteria
Measurable and technology-agnostic — outcomes, not implementation.
- SC1 —
pnpm --filter @gw2priory/web storybookboots a local Storybook whose sidebar listsButton,Input,DialogandTooltip, each rendering with its Panda styling applied (no unstyled output). - SC2 — Each of the four components has a colocated
*.stories.tsxexposing its documented variants and states; every story imports the finished component fromshared/uiand nothing from@base-ui/reactorstyled-system. - SC3 — A story renders correctly under both
prefers-color-schemesettings (toggled via the OS or DevTools media emulation), with the_osDarkvalues applied in dark — verified on a token whose value differs between schemes, e.g.surface. - SC4 —
features/ui-gallery/and the/uiroute no longer exist;main.tsxno longer mounts them; the app builds and runs without them. - SC5 —
design-system.md(and any other doc that named/uias the surface) states Storybook is the catalogue home and/uiis retired. True of the repository at merge. - SC6 — Storybook is local-only: no CI workflow builds, tests or deploys it, and no hosting is added.
- SC7 —
pnpm typecheck,pnpm test,pnpm lint,pnpm build,pnpm verify:contractandpnpm docs:buildare all green, andpnpm --filter @gw2priory/web storybookruns.
Out of scope
- Building Storybook in CI, hosting a static Storybook, or any visual-regression tooling (Chromatic, the Storybook test-runner) — the workbench is local-only (SC6). These are a later rung if they earn their place.
- Interaction / play-function tests. Behaviour and accessibility of the four components are already covered by their Vitest tests (spec 019 R14); stories are a visual/iteration surface, not a second test suite.
- Stories for
shared/uicomponents that are not design-system vocabulary (Coins,ConnectAccountPrompt,ErrorBoundary) — a component earns a story when it needs one. - Any new component, MDX documentation pages, or a design-token docs page.
- Wiring the four components into features, or any change to their APIs or recipes (spec 019 territory).
- Dark-mode work beyond exercising the existing
_osDarktoken layer in the preview.
Assumptions
- The stack is as pinned:
apps/webon Vite 8 (Rolldown/Oxc), Panda 1.11.x via@pandacss/dev/postcss, React 19.2 with the React Compiler (@rolldown/plugin-babel), Base UI 1.6.0, Vitest 4. The Storybook version is the one that supports this Vite setup, pinned inresearch.md/plan.md. shared/uiexports the four closed components with the props APIs spec 019 defined; the/uigallery renders exactly those four today and is their only render surface.- Panda's token/CSS output is produced by
panda codegen+ the PostCSS pipeline (react.md,stack.md); wiring that output into the Storybook preview is the integration this spec proves. - Storybook adds no runtime dependency to the shipped app — it is
devDependenciesonly and is not part ofvite build.
Traceability
Each acceptance scenario and success criterion must map to a named test. Filled in during implementation. Some criteria are surface/tooling outcomes verified by running a command or by human review rather than by a guard test — noted as such, in the 019 style.
| Criterion | Test |
|---|---|
| P1 #1, #2, #4 · SC1 | Manual storybook dev boot (T1) — sidebar lists the four, Button styled, controls sweep; the build-CSS proof (research V2) |
| P1 #3 · SC3 | Manual (T2) — DevTools prefers-color-scheme: dark flips surface/text in a story |
| P1 #5 · SC2 | Human review of the four story files' imports + pnpm lint (the styled-system ban on stories) |
| P2 #1, #2 · SC4 | grep finds no ui-gallery/uiGalleryRoutes; pipeline green without the folder (T3) |
| P2 #3 · SC5 | Human review of design-system.md/react.md; pnpm docs:build green |
| SC6 | Human review — no build-storybook script, no CI workflow, no host; only storybook dev |
| SC7 | The full command set at Step 5: pnpm typecheck, test, lint, build, verify:contract, docs:build, and storybook boots |