--- description: Storybook story conventions — location, naming, titles, decorators globs: stories/**/*.{js,jsx,ts,tsx,mdx},.storybook/**/*.{js,ts} alwaysApply: false --- # Where stories live All stories live in the top-level `stories/` folder. Two layout rules: - **Design-system components** mirror `app/components/`. A component at `app/components//` gets `stories//.stories.js`. - **Create-flow material** has two carve-outs: - `stories/create-flow/` — shared create-flow pieces that aren't in `app/components/` (e.g. composed wizard fragments). - `stories/pages/` — integration stories that exercise an entire `app/(app)/create/screens/<...>` screen as it appears in the wizard. | Source | Story location | | --------------------------------- | --------------------------------------- | | `app/components/controls/Chip` | `stories/controls/Chip.stories.js` | | `app/components/buttons/Button` | `stories/buttons/Button.stories.js` | | `app/(app)/create/screens/.../FooScreen`| `stories/pages/FooPage.stories.js` | | Shared create-flow fragment | `stories/create-flow/.stories.js` | Do **not** colocate `*.stories.*` next to components. The Storybook config (`.storybook/main.js`) only globs `stories/**`. # File naming - `.stories.js` — default; keep `.js` unless the story genuinely needs types. - Use `.tsx` only when the story uses `Meta` / `StoryObj` (rare). - Variants get a suffix: `Button.visual.stories.js`, `Footer.responsive.stories.js`. # Default export shape (CSF3) ```javascript import MyComponent from "../../app/components//MyComponent"; import { CHIP_PALETTE_OPTIONS } from "../../lib/propNormalization"; export default { title: "Components//MyComponent", component: MyComponent, parameters: { layout: "centered", docs: { description: { component: "Short description of what the component is for.", }, }, }, argTypes: { palette: { control: { type: "select" }, options: [...CHIP_PALETTE_OPTIONS], description: "The palette (Figma prop)", }, onClick: { action: "clicked" }, }, }; export const Default = { args: { palette: "default" } }; ``` ## Title hierarchy - Design-system components → `Components//` (e.g. `Components/Controls/Checkbox`). - Create-flow screens → `Pages/Create Flow/` (folder: `stories/pages/`). - Create flow shared pieces → `Create Flow/`. ## `argTypes` For every Figma enum prop (`variant`, `size`, `state`, `mode`, `palette`, …) expose a `select` control listing the option set, sourced from the matching `*_OPTIONS` const in `lib/propNormalization.ts`. See `.cursor/rules/component-props.mdc`. Spread with `[...FOO_OPTIONS]` so the control receives a mutable array. # Rely on the global preview — don't re-wrap `.storybook/preview.js` already provides: - `MessagesProvider` with `messages/en` → access copy via `useMessages()` inside stories exactly like app code. Never hard-code user-facing strings. - `AuthModalProvider` and `CreateFlowProvider` (same stack as `tests/utils/test-utils.tsx`) so `Top` and create-flow screens can mount. - `app/globals.css` + Inter / Bricolage Grotesque / Space Grotesk CSS variables + `.font-inter` wrapper. - Dark canvas default and Figma breakpoints (`sm` 430, `md` 640, `lg` 1024, `xl` 1440). Do **not** add your own `MessagesProvider`, font wrapper, or token setup in a story. If you need a new global, update `preview.js`. # Interaction tests (`play`) Use `storybook/test` for interaction assertions — not `@testing-library/*` directly. This matches `Checkbox.stories.js`. Storybook is documentation; Vitest component tests remain the source of truth. ```javascript import { within, userEvent, expect } from "storybook/test"; export const Interactive = { play: async ({ canvasElement }) => { const canvas = within(canvasElement); await userEvent.click(canvas.getByRole("checkbox")); expect(canvas.getByRole("checkbox")).toHaveAttribute("aria-checked", "true"); }, }; ``` # Coverage expectation Every new component in `app/components/**` ships with a story. Screens in `app/(app)/create/screens/**` ship with a `stories/pages/Page.stories.js` entry. A new component without a story is considered incomplete.