Storybook topology
Five Storybook installs across the monorepo, the shared preview contract, and the global CSS/font requirements every preview iframe must satisfy.
The repo has five 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) |
apps/desktop-client/.storybook | Desktop integration and protocol test tooling | No |
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 first-paint behavior and decorators through
@todayai-labs/storybook,
the workspace package that exports common head hooks, manager setup, theme, and
background primitives.
The shared preview package
@todayai-labs/storybook is consumed via subpath exports:
// .storybook/main.ts
import { createRequire } from 'node:module'
import { storybookManagerHead, storybookPreviewHead } from '@todayai-labs/storybook/head'
const require = createRequire(import.meta.url)
const config = {
previewAnnotations: [
require.resolve('@todayai-labs/storybook/theme/preview'),
require.resolve('@todayai-labs/storybook/background/preview'),
],
previewHead: storybookPreviewHead,
managerHead: storybookManagerHead,
}This gives every Storybook the same:
- Preview and manager first-paint colors, including loader/skeleton colors
- A preparing-root patch that keeps the story/docs root measurable while hidden
- Theme toggle (light / dark / system)
- Background palette switcher
Each install also has a one-line .storybook/manager.ts:
import '@todayai-labs/storybook/manager'The manager setup follows the operating-system color scheme at runtime. Its head hook supplies the matching paint before the manager JavaScript starts, so neither the manager nor preview iframe flashes white during boot.
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 provides the Next.js module and runtime mocks. Theme and
background behavior still comes from the shared preview annotations above.
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 writes the product signalsclass="light|dark"anddata-theme="light|dark", plus the browser-paint signaldata-sb-color-scheme="light|dark". An unlayered shared stylesheet maps the latter tocolor-schemeand the default iframe background. Product CSS should treat<html data-theme>as the canonical resolved theme signal and.darkas the Tailwind selector. The default remains light. - 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. |
apps/desktop-client | apps/desktop-client/.storybook/storybook.css | Provides the standalone desktop test-tool shell; the current stories do not depend on Tailwind or product tokens. |
design-system/opal-ui | design-system/opal-ui/.storybook/storybook.css | Imports Tailwind, opal tokens, and Figtree for Opal typography. |
design-system/opal-icons | tailwindcss/index.css | Imports Tailwind's generated CSS directly; shared annotations provide theme and background behavior. |
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 five 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.apps/desktop-clientowns desktop-only integration tools, including the custom-protocol test matrix used by both Storybook and the packaged app's debug-only URL Scheme panel to exercise OS-level application activation.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 dev:client:storybook # apps/desktop-client (port 6008)
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 desktop and 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, register the shared head hooks and
preview annotations from @todayai-labs/storybook, add the shared manager
entry, 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.
Background contract
The shared Background toolbar writes data-sb-background on the preview
iframe's <html> element. Its stylesheet owns the canvas paint and makes the
body transparent only while an explicit background is selected. It does not
write inline body styles.
Under default, Storybook leaves the canvas paint alone and every story must
leave the iframe body transparent (background-color: transparent and
background-image: none). Component-local backgrounds can live on their normal
wrappers. Story-owned full-canvas paint must be applied temporarily to <html>
and cleaned up when the story unmounts. An inline !important body background
or an opaque full-screen wrapper can still cover a toolbar background and is
outside the supported override contract.
Options are default, solid, transparent, grid, wallpaper, sky, and
sky-light. wallpaper, sky, and sky-light are Today-specific extensions
and are intentionally retained alongside the generic options. Their light/dark
variants are selected through data-sb-color-scheme.
Run cd apps/web && pnpm tsx scripts/probe-all-storybooks.mts after changing
shared Storybook scaffolding. Besides the first-paint contracts, the probe
renders every story in all five Storybooks under an explicit default background
and reports any body color, body image, render error, or render timeout by story
ID. Story-level toolbar backgrounds are temporarily removed for the computed
body check, then restored.
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.