Files
HiveGrid-Wireframes/hivegrid-cloud/docs/architecture/storybook.md
T

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`).