Today Platform Web — Dev Docs
ToolchainTesting

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):

InstallStories scopeVisual regression?
apps/web/.storybookApp-level integration stories (composed flows, layouts)Yes (CI gate)
apps/desktop-client/.storybookDesktop integration and protocol test toolingNo
design-system/opal-ui/.storybookOpal flow primitivesNo
design-system/opal-icons/.storybookOpal icon libraryNo
design-system/tangible-ui/.storybookTangible primitives and composed surfacesNo

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:

  1. Tailwind preflight is present. Components are written assuming @import 'tailwindcss' has run. This is not optional: native element defaults for button, input, body, headings, borders, and box sizing affect component geometry.
  2. 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.css plus any temporary compatibility tokens they still consume.
  3. Theme is represented on the iframe <html>. The shared Storybook theme decorator writes the product signals class="light|dark" and data-theme="light|dark", plus the browser-paint signal data-sb-color-scheme="light|dark". An unlayered shared stylesheet maps the latter to color-scheme and the default iframe background. Product CSS should treat <html data-theme> as the canonical resolved theme signal and .dark as the Tailwind selector. The default remains light.
  4. Font utilities have real font faces. Token packages define family names such as font-flex, font-sentient, font-handwriting, and font-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.css or package-specific storybook.css plus a matching staticDirs /fonts mapping.
  5. 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:

InstallPreview CSSNotes
apps/webapps/web/src/app/globals.css + apps/web/.storybook/fonts.cssMirrors Next.js app globals and next/font/local registrations.
apps/desktop-clientapps/desktop-client/.storybook/storybook.cssProvides the standalone desktop test-tool shell; the current stories do not depend on Tailwind or product tokens.
design-system/opal-uidesign-system/opal-ui/.storybook/storybook.cssImports Tailwind, opal tokens, and Figtree for Opal typography.
design-system/opal-iconstailwindcss/index.cssImports Tailwind's generated CSS directly; shared annotations provide theme and background behavior.
design-system/tangible-uidesign-system/tangible-ui/.storybook/storybook.cssImports 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>/src

If 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-ui is the canonical reference for Opal flow primitives (onboarding shells, narrative marks, chat atoms, etc.).
  • design-system/tangible-ui is the canonical reference for Tangible surfaces and primitives.
  • apps/web mounts 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-client owns 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-icons is 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:

  1. Builds apps/web Storybook (pnpm storybook:build → apps/web/storybook-static/)
  2. Boots Playwright against the static build
  3. Runs *.visual.spec.ts co-located with each story
  4. 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 snapshots

The 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:

  1. The package is a deployable app with composed-layer stories that don't make sense at the primitive level
  2. 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.

On this page