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

6.2 KiB

Storybook

A single Storybook instance under apps/storybook/ aggregates component stories from the boilerplate UI packages.

Quickstart

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

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:

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

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