AGENTS.md@apps/storybook · git:20261020.8d47d7a · 2026-10-20 · sha256 31c1a7abe9a88e57
AGENTS.md@apps/storybook git:20261020.8d47d7aA
Immutable. This exact content never changes and is served at /api/v1/blob/31c1a7abe9a88e57.
# Agent Instructions — `apps/storybook` The workbench for the login surface: one `@storybook/web-components-vite` instance hosting `@zitadel/components`' atoms and orchestrators, the orchestrators driven by `@zitadel/api-mock`. Console UI iterates on the console dev server instead (ADR 055). Commands and coverage: [`README.md`](README.md). The static build (`dist/`) is the hosted cloud's `storybook` service at `/storybook/`: production from `main`, a preview on every branch. Vercel builds it with `turbo run build` (repo-root [`vercel.json`](../../vercel.json)). ## Hard rules - One instance, one renderer: no Storybook Composition, second Storybook or React renderer. - One story file per atom (`src/<id>.stories.ts`), exporting one `Default` under `Atoms/<Name>`. States are controls (`args`/`argTypes`, e.g. `previewState`), never extra stories or "matrix" grids. - Visuals come from `@zitadel/design-tokens`; an atom's CSS is `packages/components/src/atoms/zl-<id>.css`. Stories never restyle atoms. - Add a `play` only when no lower layer can prove the behaviour; atoms prove theirs in `packages/components` specs. This app gates render and a11y. - Only orchestrator stories give the one MSW worker handlers (`parameters.msw`), and they are tagged `no-test` to keep the network out of the test run. - The build is relocatable: address files relative to the page or Vite's base, never from `/`. The MSW worker follows the base; scoped to `/`, it would intercept the console's and login UI's requests. ## Orchestrator stories `<zitadel-login>` renders whatever the flow engine returns; here that engine is `@zitadel/api-mock`. Group controls with `argTypes.<name>.table.category`: **Component** for host props (`purpose`, `variant`, `theme`, `previewState`), **Flow engine** for mock overlays applied in `beforeEach` (`branding`, `sso`, `passkey`). - The orchestrator ignores the document's `data-theme`: it paints from the flow's branding (`theme-controller.ts`). A branding with one palette side forces that side, so the `theme` control toggles only with two-sided presets. - `layout` comes from the backend and has no element prop. `split` renders only through the legacy `*-template` presets, so never add a `layout` control. - `branding` presets (`branding-presets.ts`) are fixtures applied through `applyBranding`; `none` leaves the design-system defaults. - `sso` providers derive from `SHIPPED_BRAND_ICON_NAMES` (`orchestrator-shared.ts`), so a glyph added in `@zitadel/components` shows up on its own. Never hard-code a provider list. - `sso` and `passkey` stay off by default, so the mock mirrors the shipped flow and the api-mock conformance spec stays green. - Keep controls scalar (enums, booleans): args round-trip through the URL, and object controls break shareable links. - Never remove the story-id-and-args key on the mock-backed subtree in `.storybook/preview.ts`: without it, a flow driven to `done` leaks into the next story. - `passkey-login` stories stub `navigator.credentials` with `freezePasskeyCeremony()` in `beforeEach`. Keep that stub in the workbench; the login element gets no "skip the ceremony" prop. - Not storied on purpose: `passkey-setup` / `passkey-upsell` (legacy; the default flow no longer routes through them) and `sso-redirect` (it navigates away). - A `play` that fills the password uses `PASSWORD_FIELD` (`x-auth-methods#password`), never `"password"`. - After changing a control or preset, load the stories and read the resolved state (host `data-theme`, provider buttons, the step) across branding × theme × sso. Do not infer it from the code. ## Recipe: add an atom (Figma → Storybook) Each step's package has its own `AGENTS.md`; follow it. 1. **Design.** Pull the frame with the Figma MCP (`get_design_context`, `get_screenshot`); note states, the WAI-ARIA pattern and tokens. A missing value is a new token in `@zitadel/design-tokens`. 2. **Icon**, if new: `packages/components/src/atoms/zl-icon.ts` (`IconName`, `SHIPPED_ICON_NAMES`, `ICON_NODES`). 3. **CSS**: `src/atoms/zl-<id>.css` beside the atom, `:host`/slot rules first, then the painted `.zr-<id>` surface. 4. **Atom**: `src/atoms/zl-<id>.ts`. An atom that holds a value extends `FormAtom` (`src/internal/form-atom.ts`); never hand-roll `formAssociated`. Import the CSS with `?inline`, export a manifest, export the atom from `src/atoms/index.ts`, add the manifest to `src/manifests.ts` and the public API to `src/index.ts`. 5. **Tests**, at the lowest layer that proves the property: `zl-<id>.spec.ts` (jsdom: markup, ARIA), `zl-<id>.browser.spec.ts` (Chromium: form, keyboard, focus) only if needed, the tag in `src/manifests.spec.ts` and its events in `src/atoms/event-contract.spec.ts`. 6. **Story**: `src/<id>.stories.ts`, one `Default` under `Atoms/<Name>`. 7. **Verify** with `moon run storybook:dev` (HMR from source), then `moon run components:test storybook:typecheck storybook:test`. While iterating, `pnpm --filter @zitadel/components test:unit` is the fast lane. ## Local checks ```sh moon run storybook:typecheck storybook:build storybook:test ``` `storybook:test` runs real Chromium and re-bundles its dependencies on every start (`optimizeDeps.force`), so every run starts cold. Never pipe it through `tail`, and clear strays before a retry; see [`packages/components/AGENTS.md`](../../packages/components/AGENTS.md#the-browser-projects-cold-start).