Today Platform Web — Dev Docs
ToolchainTesting

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

InstallStories scopeVisual regression?
apps/web/.storybookApp-level integration stories (composed flows, layouts)Yes (CI gate)
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 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:

  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 class="light|dark", data-theme="light|dark", color-scheme, the iframe background, and meta[name="theme-color"]. Component CSS should treat <html data-theme> as the canonical resolved theme signal and .dark as the Tailwind selector.
  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.
design-system/opal-uidesign-system/opal-ui/.storybook/storybook.cssImports Tailwind, opal tokens, and Figtree for Opal typography.
design-system/opal-iconsdesign-system/opal-icons/.storybook/storybook.cssImports Tailwind and the minimal icon preview shell.
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 four 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.
  • 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:buildapps/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 --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 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:

  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, 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.

On this page