Storybook: render off the app's real theme CSS, stop hardcoding story colours, and gate a11y in dark mode too (#7187)

## What

Makes Storybook render components with the same CSS the app gives them.

- The preview loaded the token primitives but **not the editor's
semantic token layer** (`styles/theme.css`), so components styled on
those variables rendered unthemed — three onboarding stories were
importing it by hand to stop their modal surfaces rendering transparent.
It's now loaded in the preview and the workarounds are gone.
- **Portal stories render inside the `.portal-scope` wrapper** PortalApp
mounts, so the portal's scoped reset and typography apply to them
exactly as in the app — and, deliberately, to nothing else.
- The folder stories invented their own hex colours, two of which aren't
values the app's `FOLDER_COLOR_PALETTE` can produce. They now use the
palette, so they can't drift from what a user can actually pick.

Deliberately does **not** load `tailwind.css` — tailwind is on its way
out of the editor, so matching the token layer alone is the target
state.

## Story colours route through the tokens, enforced

Stories were exempt from the `code-colors` lint, and it showed:
hardcoded hexes for surfaces the tokens already name (chat bubbles,
borders, demo backgrounds), `var(--x, #hex)` fallbacks that mask a
renamed token by silently painting the stale colour, and mocked category
accents for which real `--color-cat-*` tokens exist.

- Styling literals now use tokens; the dead fallbacks are stripped.
- The stories exemption is removed from `theme-lint`, so this can't
regress.
- Colours that are **the datum itself** — `ColorInput` values, signature
ink, per-policy accents, brand-mark swatches — stay literal via
`theme-allow-color`, hoisted to named consts so the exemption and its
reason sit together.

A practical side effect: stories styled on tokens actually respond to
the dark-mode toolbar toggle, which is what makes a dark-theme a11y pass
meaningful later.

## The a11y gate now runs dark as well as light

Contrast is most of what axe reports and it is theme-dependent, so a
light-only gate left half the surface unmeasured — and it only becomes
measurable at all once the tokens above actually flip. `SCAN_THEME=dark`
pins the theme for a whole scan run, every a11y task runs both themes,
and each theme has its own baseline:

- **light** re-recorded against the themed rendering (the old baseline
measured colours the app never shows): 831 stories with violations
- **dark** recorded for the first time: 798 stories with violations, 980
story-rule pairs, zero render failures across the full sweep

Verified end to end: dark scans measure against dark surfaces (`#18181b`
vs `#ffffff`), both baselines self-check clean, and a live scan of
stories that changed on main after recording passes both gates.
Nightly's timeout doubles for the second sweep.

## Testing

Typecheck (all variants), ESLint and Prettier pass. Onboarding, folder,
portal and control stories render in the browser scan (39/39) with the
per-story CSS imports removed; every story touched by the colour sweep
renders too (58/58). `task frontend:lint:colors` passes with stories
included.
This commit is contained in:
Reece Browne
2026-08-10 11:06:18 +00:00
committed by GitHub
parent af5f54274d
commit 7bf18cc4c7
23 changed files with 2613 additions and 116 deletions
+8 -2
View File
@@ -211,11 +211,13 @@ tasks:
- npx vitest run --config .storybook/vitest.config.ts {{.CLI_ARGS}}
storybook:a11y:
desc: "a11y regression gate over every story: fail only on NEW axe violations"
desc: "a11y regression gate over every story, light and dark: fail only on NEW axe violations"
deps: [prepare, storybook:browser]
cmds:
- node .storybook/a11y-scan.mjs
- node .storybook/a11y-check.mjs --in .a11y-scan --manifest .a11y-scan/manifest.txt
- SCAN_THEME=dark node .storybook/a11y-scan.mjs
- node .storybook/a11y-check.mjs --in .a11y-scan --manifest .a11y-scan/manifest.txt --baseline .storybook/a11y-baseline.dark.json
storybook:a11y:changed:
desc: "a11y gate over the stories this branch affects (default base origin/main)"
@@ -244,13 +246,17 @@ tasks:
fi
node .storybook/a11y-scan.mjs {{.CHANGED}}
node .storybook/a11y-check.mjs --in .a11y-scan --manifest .a11y-scan/manifest.txt
SCAN_THEME=dark node .storybook/a11y-scan.mjs {{.CHANGED}}
node .storybook/a11y-check.mjs --in .a11y-scan --manifest .a11y-scan/manifest.txt --baseline .storybook/a11y-baseline.dark.json
storybook:a11y:record:
desc: "Re-record the a11y baseline (run after intentionally fixing/adding violations)"
desc: "Re-record both a11y baselines (run after intentionally fixing/adding violations)"
deps: [prepare, storybook:browser]
cmds:
- node .storybook/a11y-scan.mjs
- node .storybook/a11y-check.mjs --in .a11y-scan --manifest .a11y-scan/manifest.txt --record
- SCAN_THEME=dark node .storybook/a11y-scan.mjs
- node .storybook/a11y-check.mjs --in .a11y-scan --manifest .a11y-scan/manifest.txt --record --baseline .storybook/a11y-baseline.dark.json
# ============================================================
# Code quality