182 lines
6.2 KiB
Markdown
182 lines
6.2 KiB
Markdown
# Storybook
|
|
|
|
A single Storybook instance under `apps/storybook/` aggregates component
|
|
stories from the boilerplate UI packages.
|
|
|
|
## Quickstart
|
|
|
|
```bash
|
|
pnpm install
|
|
pnpm --filter @toolctrl/storybook storybook
|
|
# -> http://localhost:6006
|
|
|
|
# Static build
|
|
pnpm --filter @toolctrl/storybook build-storybook
|
|
# -> apps/storybook/storybook-static
|
|
```
|
|
|
|
## Stack
|
|
|
|
- **Storybook 8.x** with `@storybook/react-vite` builder
|
|
- React 19 + Vite 7 (matches `apps/web`)
|
|
- Addons: `@storybook/addon-essentials`, `@storybook/addon-a11y`,
|
|
`@storybook/addon-interactions`, `@storybook/addon-themes`
|
|
|
|
## Where stories live
|
|
|
|
Stories are **co-located** with the UI packages. The `main.ts` glob picks
|
|
them up automatically:
|
|
|
|
```
|
|
packages/ui/src/stories/*.stories.tsx
|
|
packages/ui-app/src/stories/*.stories.tsx
|
|
packages/ui-subframe/src/stories/*.stories.tsx
|
|
packages/ui-mock/src/stories/*.stories.tsx
|
|
```
|
|
|
|
> **Why `packages/ui/` only today?** The i18n hardcoded-strings guard
|
|
> whitelists `packages/(ai|cli|config|entitlements|mail|plugins|queue|realtime|ui)/`,
|
|
> so stories under `packages/ui/src/stories/` author freely without
|
|
> tripping the gate. Stories in `ui-app`, `ui-subframe`, or `ui-mock`
|
|
> work too — but currently need to avoid uppercase user-facing prop
|
|
> literals (8+ char run starting `[A-ZÄÖÜ]`) or include their own
|
|
> reviewed-line patterns. A follow-up will widen the guard.
|
|
|
|
## Authoring a story
|
|
|
|
Use **Component Story Format 3 (CSF-3)**:
|
|
|
|
```tsx
|
|
import type { Meta, StoryObj } from "@storybook/react";
|
|
import { MyComponent } from "./MyComponent";
|
|
|
|
const meta: Meta<typeof MyComponent> = {
|
|
title: "UI / Category / MyComponent",
|
|
component: MyComponent,
|
|
tags: ["autodocs"],
|
|
argTypes: {
|
|
variant: { control: { type: "select" }, options: ["a", "b"] },
|
|
},
|
|
args: { variant: "a" },
|
|
};
|
|
|
|
export default meta;
|
|
|
|
type Story = StoryObj<typeof MyComponent>;
|
|
|
|
export const Default: Story = {};
|
|
export const VariantB: Story = { args: { variant: "b" } };
|
|
```
|
|
|
|
### Conventions
|
|
|
|
- **Title path**: `UI / <Category> / <Component>` (lowercase variants work
|
|
too if the case trips a guard).
|
|
- **Tags**: include `"autodocs"` to get an auto-generated docs page.
|
|
- **Render functions** when you need state or composition. Prefer plain
|
|
`args` for simple variants.
|
|
- **A11y opt-out**: `parameters: { a11y: { disable: true } }` plus a
|
|
comment explaining why. Default policy is "fail on a11y violations".
|
|
|
|
## Theming
|
|
|
|
`apps/storybook/.storybook/preview.tsx` wraps every story in a theme
|
|
decorator from `@storybook/addon-themes`. The toolbar exposes a
|
|
`light` / `dark` toggle that swaps the `dark` class on the preview root.
|
|
|
|
Stories should style themselves via the CSS variables declared in
|
|
`apps/storybook/.storybook/preview.css` (`--color-bg`, `--color-fg`,
|
|
`--color-primary`, …) so they flip automatically. See the **Theme
|
|
Guide** docs page inside Storybook for the full token table.
|
|
|
|
## A11y addon
|
|
|
|
`@storybook/addon-a11y` runs axe-core in the story panel. The
|
|
`preview.tsx` defaults enable the WCAG-style `color-contrast` rule.
|
|
Violations show up in the **Accessibility** panel.
|
|
|
|
## Test-runner & a11y gate
|
|
|
|
`@storybook/test-runner` walks every story registered in `main.ts` and
|
|
runs the `postVisit` hook in `.storybook/test-runner.ts` against each
|
|
render. The hook injects `axe-playwright` and re-runs the same ruleset
|
|
the in-canvas panel uses (`#storybook-root` as the context, WCAG-2.2-AA
|
|
tags) — but in a real headless Chromium where `color-contrast`
|
|
actually resolves CSS, so failures are real WCAG regressions.
|
|
|
|
Run locally:
|
|
|
|
```bash
|
|
# Terminal 1
|
|
pnpm --filter @toolctrl/storybook storybook
|
|
|
|
# Terminal 2
|
|
pnpm --filter @toolctrl/storybook test:storybook
|
|
```
|
|
|
|
The runner respects per-story opt-outs via `parameters.a11y.disable =
|
|
true` (must come with a justifying code comment).
|
|
|
|
## CI
|
|
|
|
`.github/workflows/storybook.yml` and `.gitea/workflows/storybook.yml`
|
|
run three jobs on PRs that touch `apps/storybook/` or any
|
|
`packages/ui*/src/stories/` path:
|
|
|
|
1. **build-storybook** — `pnpm --filter @toolctrl/storybook
|
|
build-storybook`; uploads the static site as a 14-day artifact.
|
|
2. **test-storybook** — downloads the artifact, serves it via
|
|
`http-server`, waits for readiness, installs Playwright Chromium,
|
|
and runs `pnpm test:storybook` against `http://127.0.0.1:6006`.
|
|
Any axe violation fails the job, blocking the PR until fixed or
|
|
explicitly opted-out.
|
|
3. **visual-regression** — Lost-Pixel diff (see below). Self-skips
|
|
until a baseline is committed.
|
|
|
|
The main CI Quality-Gates job is unchanged.
|
|
|
|
## Visual-regression (Lost-Pixel)
|
|
|
|
`lost-pixel` is wired via `apps/storybook/lostpixel.config.ts`. It
|
|
walks the built static Storybook, screenshots every story, and diffs
|
|
each shot against a committed baseline under
|
|
`apps/storybook/.lostpixel/baseline/`. A pixel diff above the 0.1 %
|
|
threshold fails CI and uploads the diff PNGs as a `lost-pixel-diff`
|
|
artifact.
|
|
|
|
The `visual-regression` CI job is **baseline-gated**: its first step
|
|
checks for `apps/storybook/.lostpixel/baseline/*.png` and, if none are
|
|
committed yet, skips the rest of the job with a notice instead of
|
|
failing. This keeps the job green until an operator opts in.
|
|
|
|
**Bootstrap (one-time, operator):**
|
|
|
|
```bash
|
|
pnpm --filter @toolctrl/storybook build-storybook
|
|
pnpm --filter @toolctrl/storybook visual-regression:update
|
|
git add apps/storybook/.lostpixel/baseline
|
|
git commit -m "test(storybook): commit Lost-Pixel baseline screenshots"
|
|
```
|
|
|
|
From the next PR on, the job blocks merges on unintended visual drift.
|
|
`.lostpixel/current/` and `.lostpixel/difference/` are per-run output
|
|
and git-ignored.
|
|
|
|
Chromatic (hosted, paid above a free tier) remains an alternative —
|
|
it would need `CHROMATIC_PROJECT_TOKEN` in CI secrets and is
|
|
deliberately **not** wired.
|
|
|
|
## Troubleshooting
|
|
|
|
- **Story missing?** Confirm the path matches the glob in
|
|
`apps/storybook/.storybook/main.ts`.
|
|
- **Story render error?** Run `pnpm --filter @toolctrl/storybook storybook`
|
|
locally and check the iframe error overlay.
|
|
- **A11y violation that should be allowed?** Use
|
|
`parameters.a11y.config.rules` to disable a specific axe rule, or
|
|
`parameters.a11y.disable = true` if a story is intentionally
|
|
decorative.
|
|
- **i18n guard flags story content?** Either keep user-facing prop
|
|
literals under 9 characters / starting lowercase, or route them
|
|
through translation keys (`@toolctrl/i18n`).
|