Skip to content

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

  1. Given the repository, when the developer runs the workbench script, then Storybook starts locally and its sidebar lists Button, Input, Dialog and Tooltip.
  2. 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.
  3. Given any story, when the browser is put into prefers-color-scheme: dark (OS setting or DevTools media emulation), then the component reflects the _osDark token values (e.g. surface, text.strong), proving the dark layer is present in the preview. (There is no bespoke light/dark toolbar toggle — _osDark is media-query-driven, not class-driven; research.md V2.)
  4. Given a component with variants (e.g. Button's variant/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.
  5. Given a story file, when it is written, then it imports the finished component from shared/ui and nothing from @base-ui/react or styled-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

  1. Given the app, when it is built and run, then there is no /ui route and no features/ui-gallery/ folder (page, route table, styles, test all removed).
  2. Given main.tsx, when it assembles the router, then it no longer mounts the ui-gallery route table.
  3. 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 /ui stopgap has been retired — true of the repository at merge, not an intention.

Requirements ​

The workbench

  • R1 — Storybook (the @storybook/react-vite framework, its Vite builder) is added as a dev dependency of apps/web, with a storybook script 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.8 ran storybook build on 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/ (a main and a preview). The preview loads Panda's generated CSS and token layer (by importing src/index.css) so components render with the same styling the app gives them; the _osDark layer renders under prefers-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, the button--* 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 own vite build.
  • R3 — One colocated story file per component — Button.stories.tsx, Input.stories.tsx, Dialog.stories.tsx, Tooltip.stories.tsx — beside its component in apps/web/src/shared/ui/. Each exposes the component's documented variants/states (mirroring what /ui shows today) via controls and/or separate stories, imports the finished component, and imports nothing from @base-ui/react or styled-system.

Fitting the existing enforcement

  • R4 — Biome's useFilenamingConvention (which reads the filename segment before the first dot, so Button.stories.tsx is checked as Button) is given a scoped apps/web/**/*.stories.tsx override with filenameCases: ["export", "camelCase", "PascalCase"], placed after the __tests__ override, exactly as that override admits PascalCase test files. Confirmed — research.md V3: the committed config rejects Button.stories.tsx; this override makes biome check pass, and — since overrides are per-rule — the styled-system import ban still binds, so a story cannot import styled-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 includes src/**/*.tsx), and Vitest does not pick the stories up as test files (its include is *.test.ts(x) only). Confirmed — research.md V4: the Storybook build loads the app vite.config.ts, so the compiler runs in Storybook too (useMemoCache in the built story chunk) and storybook build still passes — so panicThreshold protects the Storybook build as well; no viteFinal is 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 from main.tsx. Any test that asserts the /ui route (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 /ui gallery has been retired. Any other doc line pointing at /ui as the surface (e.g. in react.md) is updated to match. Both must compile under pnpm 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 storybook boots a local Storybook whose sidebar lists Button, Input, Dialog and Tooltip, each rendering with its Panda styling applied (no unstyled output).
  • SC2 — Each of the four components has a colocated *.stories.tsx exposing its documented variants and states; every story imports the finished component from shared/ui and nothing from @base-ui/react or styled-system.
  • SC3 — A story renders correctly under both prefers-color-scheme settings (toggled via the OS or DevTools media emulation), with the _osDark values applied in dark — verified on a token whose value differs between schemes, e.g. surface.
  • SC4 — features/ui-gallery/ and the /ui route no longer exist; main.tsx no longer mounts them; the app builds and runs without them.
  • SC5 — design-system.md (and any other doc that named /ui as the surface) states Storybook is the catalogue home and /ui is 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:contract and pnpm docs:build are all green, and pnpm --filter @gw2priory/web storybook runs.

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/ui components 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 _osDark token layer in the preview.

Assumptions ​

  • The stack is as pinned: apps/web on 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 in research.md/plan.md.
  • shared/ui exports the four closed components with the props APIs spec 019 defined; the /ui gallery 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 devDependencies only and is not part of vite 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.

CriterionTest
P1 #1, #2, #4 · SC1Manual storybook dev boot (T1) — sidebar lists the four, Button styled, controls sweep; the build-CSS proof (research V2)
P1 #3 · SC3Manual (T2) — DevTools prefers-color-scheme: dark flips surface/text in a story
P1 #5 · SC2Human review of the four story files' imports + pnpm lint (the styled-system ban on stories)
P2 #1, #2 · SC4grep finds no ui-gallery/uiGalleryRoutes; pipeline green without the folder (T3)
P2 #3 · SC5Human review of design-system.md/react.md; pnpm docs:build green
SC6Human review — no build-storybook script, no CI workflow, no host; only storybook dev
SC7The full command set at Step 5: pnpm typecheck, test, lint, build, verify:contract, docs:build, and storybook boots