Storybook topology
Four Storybook installs across the monorepo, the shared preview contract, and the global CSS/font requirements every preview iframe must satisfy.
The repo has four Storybook installations, all on Storybook 10 (catalog):
| Install | Stories scope | Visual regression? |
|---|---|---|
apps/web/.storybook | App-level integration stories (composed flows, layouts) | Yes (CI gate) |
design-system/opal-ui/.storybook | Opal flow primitives | No |
design-system/opal-icons/.storybook | Opal icon library | No |
design-system/tangible-ui/.storybook | Tangible primitives and composed surfaces | No |
Each Storybook is independent — separate config, separate build output,
separate dev server. They share styling and decorators through
@todayai-labs/storybook,
the workspace package that exports the common preview + theme + background
primitives.
The shared preview package
@todayai-labs/storybook is consumed via subpath exports:
// .storybook/preview.tsx
export { default } from '@todayai-labs/storybook/preview'This gives every Storybook the same:
- Theme toggle (light / dark / system)
- Background palette switcher
The shared preview package does not own product CSS. Each Storybook preview iframe must still import the global CSS that the stories assume exists.
When a story needs Next.js mocks (router, navigation, Image component),
apps/web imports a separate preset:
// apps/web/.storybook/main.ts
import { withNextMocks } from '@todayai-labs/storybook/next/config'
const config: StorybookConfig = withNextMocks({
// ...
})withNextMocks wires @storybook/addon-themes and friends without each app
having to repeat the configuration.
Preview CSS contract
Every Storybook iframe is a separate document. The app root and Next.js layout
do not run there, so a working app is not proof that Storybook has the same
CSS environment. Each .storybook/preview.tsx must import exactly one preview
CSS entry that satisfies this contract:
- Tailwind preflight is present. Components are written assuming
@import 'tailwindcss'has run. This is not optional: native element defaults forbutton,input,body, headings, borders, and box sizing affect component geometry. - The relevant token package is imported. Opal-track components need
@todayai-labs/opal-tokens/globals.css; Tangible components need their own@todayai-labs/tangible-ui/tokens/globals.cssplus any temporary compatibility tokens they still consume. - Theme is represented on the iframe
<html>. The shared Storybook theme decorator writesclass="light|dark",data-theme="light|dark",color-scheme, the iframe background, andmeta[name="theme-color"]. Component CSS should treat<html data-theme>as the canonical resolved theme signal and.darkas the Tailwind selector. - Font utilities have real font faces. Token packages define family
names such as
font-flex,font-sentient,font-handwriting, andfont-seed; they intentionally do not ship every@font-face. Storybook must register the same font files the app runtime registers, usually with a local.storybook/fonts.cssor package-specificstorybook.cssplus a matchingstaticDirs/fontsmapping. - The body shell is explicit. If stories rely on app-level body
font-family, smoothing, selection, focus, or root background rules, put those rules in the preview CSS entry. Do not rely on Storybook manager CSS leaking into the iframe.
Current preview CSS entries:
| Install | Preview CSS | Notes |
|---|---|---|
apps/web | apps/web/src/app/globals.css + apps/web/.storybook/fonts.css | Mirrors Next.js app globals and next/font/local registrations. |
design-system/opal-ui | design-system/opal-ui/.storybook/storybook.css | Imports Tailwind, opal tokens, and Figtree for Opal typography. |
design-system/opal-icons | design-system/opal-icons/.storybook/storybook.css | Imports Tailwind and the minimal icon preview shell. |
design-system/tangible-ui | design-system/tangible-ui/.storybook/storybook.css | Imports Tailwind, tangible tokens, and temporary opal font utility compatibility until Tangible owns its full typography tokens. |
When adding a new Storybook or moving components between packages, audit the component classes before opening the PR:
rg -n "font-(flex|sentient|handwriting|seed|today-glyphic)|dark:|data-theme" <package>/srcIf the package uses a font-* utility, the Storybook preview must provide both
the Tailwind token that creates the utility and the @font-face that resolves
the family name.
Why four instead of one
Each Storybook is scoped to a publishing audience:
design-system/opal-uiis the canonical reference for Opal flow primitives (onboarding shells, narrative marks, chat atoms, etc.).design-system/tangible-uiis the canonical reference for Tangible surfaces and primitives.apps/webmounts both libraries and adds app-shaped stories — a composed onboarding flow, a chat layout. This is where visual regression happens, because this is where layout matters.design-system/opal-iconsis isolated so icon updates can be reviewed without loading the heavier product component libraries.
Consolidating them into one would mean either losing the publishing-audience boundary or losing visual regression's tight scope (snapshot diffs covering everything).
Visual regression scope
Only apps/web runs visual regression. The CI Visual Regression check:
- Builds
apps/webStorybook (pnpm storybook:build→apps/web/storybook-static/) - Boots Playwright against the static build
- Runs
*.visual.spec.tsco-located with each story - Compares against committed PNG snapshots (Git LFS)
See Playwright tiers → Visual regression for the full config.
design-system/opal-ui and design-system/tangible-ui Storybooks don't have
visual regression yet because their stories are atomic — the layout that goes
wrong is composed-layer behavior, which is what apps/web catches. Atom-level
visual regression is queued as tier-3 improvement.
Commands
# Run a specific Storybook locally
pnpm --filter @todayai-labs/web storybook # apps/web
pnpm --filter @todayai-labs/opal-ui storybook # design-system/opal-ui
pnpm --filter @todayai-labs/opal-icons storybook # design-system/opal-icons
pnpm --filter @todayai-labs/tangible-ui storybook # design-system/tangible-ui
# Build static Storybook
pnpm storybook:build # apps/web (CI uses this)
# Run visual regression locally
pnpm test:visual # diff against committed snapshots
pnpm test:visual:update # regenerate snapshotsThe web Storybook is the slowest (~30 s cold start, fast HMR after) because of Agentation and react-scan dev decorators. The two design-system Storybooks are noticeably snappier.
Adding a Storybook to a new package
Most new packages should not add a Storybook. Components belong in
design-system/opal-ui or design-system/tangible-ui where they get one of
the existing Storybooks "for free." A new Storybook is justified only when:
- The package is a deployable app with composed-layer stories that don't make sense at the primitive level
- The package is a candidate for visual regression of its own
If you do add one, copy the structure from design-system/opal-ui/.storybook
or design-system/tangible-ui/.storybook, import the shared preview from
@todayai-labs/storybook, add a preview CSS entry that satisfies the contract
above, and add Moon tasks (storybook and build-storybook) to the new
project's moon.yml.
Playwright tiers
Five Playwright configs (visual / smoke / full / preview / local) split by what they target and when CI runs them.
MSW handlers and fixtures
MSW (Mock Service Worker) is the mock-mode runtime — handlers + fixtures live under `apps/web/src/lib/msw/` and are also the source of truth for Storybook stories that need network responses.