diff --git a/.cursor/rules/storybook.mdc b/.cursor/rules/storybook.mdc index de44b8a..c8dac53 100644 --- a/.cursor/rules/storybook.mdc +++ b/.cursor/rules/storybook.mdc @@ -28,16 +28,17 @@ Do **not** colocate `*.stories.*` next to components. The Storybook config # File naming -- `.stories.js` — matches 69/70 existing files. -- Use `.tsx` only when the story genuinely needs types (rare; prefer JS to - match the codebase convention). +- `.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 (CSF2) +# Default export shape (CSF3) ```javascript import MyComponent from "../../app/components//MyComponent"; +import { CHIP_PALETTE_OPTIONS } from "../../lib/propNormalization"; export default { title: "Components//MyComponent", @@ -51,31 +52,32 @@ export default { }, }, argTypes: { - variant: { + palette: { control: { type: "select" }, - options: ["filled", "outline"], - description: "The variant (Figma prop)", + options: [...CHIP_PALETTE_OPTIONS], + description: "The palette (Figma prop)", }, onClick: { action: "clicked" }, }, }; -export const Default = { args: { variant: "filled" } }; +export const Default = { args: { palette: "default" } }; ``` ## Title hierarchy - Design-system components → `Components//` (e.g. `Components/Controls/Checkbox`). -- Pages → `Pages/` (folder: `stories/pages/`). +- 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 **lowercase** option set, sourced -from the matching `*_OPTIONS` const in `lib/propNormalization.ts`. See -`.cursor/rules/component-props.mdc`. +…) 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 @@ -83,8 +85,12 @@ from the matching `*_OPTIONS` const in `lib/propNormalization.ts`. See - `MessagesProvider` with `messages/en` → access copy via `useMessages()` inside stories exactly like app code. Never hard-code user-facing strings. -- `app/globals.css` + `.font-inter` wrapper → design tokens and fonts are - already present. +- `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`. @@ -92,8 +98,8 @@ 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` and stays compatible with the -Vitest portable-stories runner in `.storybook/vitest.setup.js`. +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"; diff --git a/.storybook/fonts.css b/.storybook/fonts.css index 8cc0b88..56d499b 100644 --- a/.storybook/fonts.css +++ b/.storybook/fonts.css @@ -1,7 +1,13 @@ -@import url("https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap"); +@import url("https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=Bricolage+Grotesque:wght@400;500;700;800&family=Space+Grotesk:wght@400;500;700&display=swap"); :root { --font-inter: "Inter", ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", "Apple Color Emoji", "Segoe UI Emoji"; + --font-bricolage-grotesque: + "Bricolage Grotesque", ui-sans-serif, system-ui, -apple-system, "Segoe UI", + Roboto, "Helvetica Neue", Arial; + --font-space-grotesk: + "Space Grotesk", ui-sans-serif, system-ui, -apple-system, "Segoe UI", + Roboto, "Helvetica Neue", Arial; } diff --git a/.storybook/main.js b/.storybook/main.js index d1941c6..543478b 100644 --- a/.storybook/main.js +++ b/.storybook/main.js @@ -4,32 +4,10 @@ module.exports = { "../stories/**/*.mdx", "../stories/**/*.stories.@(js|jsx|mjs|ts|tsx)", ], - addons: [ - // Removed @storybook/addon-essentials due to version mismatch with Storybook 10.x - // Using individual addons instead. Interaction helpers import from storybook/test - // (bundled with storybook@10); @storybook/addon-interactions was merged into SB 8 core. - "@storybook/addon-a11y", - ], + addons: ["@storybook/addon-docs", "@storybook/addon-a11y"], framework: { name: "@storybook/nextjs", options: {}, }, staticDirs: ["../public"], - - // Webpack configuration to resolve Next.js modules for Next.js 16 compatibility - async webpackFinal(config) { - // Ensure Next.js modules are resolved correctly - config.resolve = config.resolve || {}; - config.resolve.alias = { - ...(config.resolve.alias || {}), - }; - - // Ensure node_modules are resolved - config.resolve.modules = [ - ...(config.resolve.modules || []), - "node_modules", - ]; - - return config; - }, }; diff --git a/.storybook/preview.js b/.storybook/preview.js index 333f813..4b8151d 100644 --- a/.storybook/preview.js +++ b/.storybook/preview.js @@ -1,10 +1,43 @@ import "../app/globals.css"; import "./fonts.css"; +import { MINIMAL_VIEWPORTS } from "storybook/viewport"; +import { AuthModalProvider } from "../app/contexts/AuthModalContext"; import { MessagesProvider } from "../app/contexts/MessagesContext"; +import { CreateFlowProvider } from "../app/(app)/create/context/CreateFlowContext"; import messages from "../messages/en/index"; +/** Figma / Tailwind breakpoints from `app/tailwind.css`. */ +const dsViewports = { + sm: { + name: "sm 430", + styles: { width: "430px", height: "932px" }, + }, + md: { + name: "md 640", + styles: { width: "640px", height: "1024px" }, + }, + lg: { + name: "lg 1024", + styles: { width: "1024px", height: "768px" }, + }, + xl: { + name: "xl 1440", + styles: { width: "1440px", height: "900px" }, + }, + // Aliases used by existing page stories + mobile1: { + name: "sm 430", + styles: { width: "430px", height: "932px" }, + }, + desktop: { + name: "xl 1440", + styles: { width: "1440px", height: "900px" }, + }, +}; + /** @type { import('@storybook/react').Preview } */ const preview = { + tags: ["autodocs"], parameters: { controls: { matchers: { @@ -12,13 +45,32 @@ const preview = { date: /Date$/i, }, }, + backgrounds: { + options: { + dark: { name: "Dark", value: "#000000" }, + light: { name: "Light", value: "#ffffff" }, + }, + }, + viewport: { + options: { + ...MINIMAL_VIEWPORTS, + ...dsViewports, + }, + }, + }, + initialGlobals: { + backgrounds: { value: "dark" }, }, decorators: [ (Story) => ( -
- -
+ + +
+ +
+
+
), ], diff --git a/.storybook/vitest.setup.js b/.storybook/vitest.setup.js deleted file mode 100644 index 9cef2b3..0000000 --- a/.storybook/vitest.setup.js +++ /dev/null @@ -1,7 +0,0 @@ -import * as a11yAddonAnnotations from "@storybook/addon-a11y/preview"; -import { setProjectAnnotations } from "@storybook/nextjs-vite"; -import * as projectAnnotations from "./preview"; - -// This is an important step to apply the right configuration when testing your stories. -// More info at: https://storybook.js.org/docs/api/portable-stories/portable-stories-vitest#setprojectannotations -setProjectAnnotations([a11yAddonAnnotations, projectAnnotations]); diff --git a/knip.json b/knip.json index 248e7bb..7dfd369 100644 --- a/knip.json +++ b/knip.json @@ -25,7 +25,6 @@ "@types/mdx", "eslint-config-next", "typescript-eslint", - "@storybook/nextjs-vite", "@eslint/js", "@next/eslint-plugin-next", "eslint-plugin-react", diff --git a/package-lock.json b/package-lock.json index 0a9eef7..92f64fd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -30,6 +30,7 @@ "@lhci/cli": "^0.15.1", "@playwright/test": "^1.55.0", "@storybook/addon-a11y": "^10.2.0", + "@storybook/addon-docs": "^10.2.0", "@storybook/nextjs": "^10.2.0", "@storybook/react": "^10.2.0", "@svgr/webpack": "^8.1.0", @@ -5911,6 +5912,35 @@ "storybook": "^10.4.1" } }, + "node_modules/@storybook/addon-docs": { + "version": "10.4.1", + "resolved": "https://registry.npmjs.org/@storybook/addon-docs/-/addon-docs-10.4.1.tgz", + "integrity": "sha512-IYqUdjoZe4VO2LFZlKL/gwy7DsQSWCq6hX+zc1MBmZo04yycDASk1tte57n9pdlW3ajw9yYMF/+lVBi+xQjyvw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@mdx-js/react": "^3.0.0", + "@storybook/csf-plugin": "10.4.1", + "@storybook/icons": "^2.0.2", + "@storybook/react-dom-shim": "10.4.1", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "ts-dedent": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/storybook" + }, + "peerDependencies": { + "@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "storybook": "^10.4.1" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/@storybook/builder-webpack5": { "version": "10.4.1", "resolved": "https://registry.npmjs.org/@storybook/builder-webpack5/-/builder-webpack5-10.4.1.tgz", @@ -6017,6 +6047,41 @@ "storybook": "^10.4.1" } }, + "node_modules/@storybook/csf-plugin": { + "version": "10.4.1", + "resolved": "https://registry.npmjs.org/@storybook/csf-plugin/-/csf-plugin-10.4.1.tgz", + "integrity": "sha512-WdPepGBxDGOUDjYd8KxMtcf+us/2PAcnBczl77XtrnxxHNs0jWesxKkiJ9yiuGrge4BPhDeAj6rxjbBoaHxLBA==", + "dev": true, + "license": "MIT", + "dependencies": { + "unplugin": "^2.3.5" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/storybook" + }, + "peerDependencies": { + "esbuild": "*", + "rollup": "*", + "storybook": "^10.4.1", + "vite": "*", + "webpack": "*" + }, + "peerDependenciesMeta": { + "esbuild": { + "optional": true + }, + "rollup": { + "optional": true + }, + "vite": { + "optional": true + }, + "webpack": { + "optional": true + } + } + }, "node_modules/@storybook/global": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/@storybook/global/-/global-5.0.0.tgz", @@ -6192,7 +6257,7 @@ "webpack": ">= 4" } }, - "node_modules/@storybook/react/node_modules/@storybook/react-dom-shim": { + "node_modules/@storybook/react-dom-shim": { "version": "10.4.1", "resolved": "https://registry.npmjs.org/@storybook/react-dom-shim/-/react-dom-shim-10.4.1.tgz", "integrity": "sha512-6QFqfDNH4DMrt7yHKRfpqRopsVUc/Az+sXIdJ39IetYnHUxL3nW4NVaPc6uy/8Qi8urzUyEXL/nn7cpSIP2aPQ==", @@ -23811,6 +23876,22 @@ "node": ">= 0.8" } }, + "node_modules/unplugin": { + "version": "2.3.11", + "resolved": "https://registry.npmjs.org/unplugin/-/unplugin-2.3.11.tgz", + "integrity": "sha512-5uKD0nqiYVzlmCRs01Fhs2BdkEgBS3SAVP6ndrBsuK42iC2+JHyxM05Rm9G8+5mkmRtzMZGY8Ct5+mliZxU/Ww==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/remapping": "^2.3.5", + "acorn": "^8.15.0", + "picomatch": "^4.0.3", + "webpack-virtual-modules": "^0.6.2" + }, + "engines": { + "node": ">=18.12.0" + } + }, "node_modules/unrs-resolver": { "version": "1.12.2", "resolved": "https://registry.npmjs.org/unrs-resolver/-/unrs-resolver-1.12.2.tgz", diff --git a/package.json b/package.json index 3969b96..dcb7d88 100644 --- a/package.json +++ b/package.json @@ -17,9 +17,7 @@ "lint": "eslint . --ext .js,.jsx,.ts,.tsx --max-warnings 9999", "postinstall": "npm rebuild lightningcss && prisma generate", "storybook": "storybook dev -p 6006", - "storybook:github": "STORYBOOK_BASE_PATH=true storybook dev -p 6006", "storybook:build": "storybook build", - "storybook:build:github": "STORYBOOK_BASE_PATH=true storybook build", "knip": "knip --include files,exports --exclude duplicates", "test": "vitest run --coverage", "test:watch": "vitest", @@ -69,6 +67,7 @@ "@lhci/cli": "^0.15.1", "@playwright/test": "^1.55.0", "@storybook/addon-a11y": "^10.2.0", + "@storybook/addon-docs": "^10.2.0", "@storybook/nextjs": "^10.2.0", "@storybook/react": "^10.2.0", "@svgr/webpack": "^8.1.0", diff --git a/stories/buttons/Button.stories.js b/stories/buttons/Button.stories.js index ba0ced7..8af8e3d 100644 --- a/stories/buttons/Button.stories.js +++ b/stories/buttons/Button.stories.js @@ -1,4 +1,9 @@ import Button from "../../app/components/buttons/Button"; +import { + BUTTON_PALETTE_OPTIONS, + BUTTON_TYPE_OPTIONS, + SIZE_OPTIONS, +} from "../../lib/propNormalization"; export default { title: "Components/Buttons/Button", @@ -15,17 +20,17 @@ export default { argTypes: { buttonType: { control: { type: "select" }, - options: ["filled", "outline", "ghost", "danger"], + options: [...BUTTON_TYPE_OPTIONS], description: "The button type (Figma prop)", }, palette: { control: { type: "select" }, - options: ["default", "inverse"], + options: [...BUTTON_PALETTE_OPTIONS], description: "The button palette (Figma prop)", }, size: { control: { type: "select" }, - options: ["xsmall", "small", "medium", "large", "xlarge"], + options: [...SIZE_OPTIONS], description: "The size of the button", }, disabled: { diff --git a/stories/buttons/InlineTextButton.stories.js b/stories/buttons/InlineTextButton.stories.js index ce7294c..ad11a40 100644 --- a/stories/buttons/InlineTextButton.stories.js +++ b/stories/buttons/InlineTextButton.stories.js @@ -33,7 +33,7 @@ export const Default = { export const InParagraph = { render: () => ( -

+

Share a bit more detail so the group can weigh in. You can always{" "} {}}>expand this later{" "} if you need more room. diff --git a/stories/buttons/Vertical.stories.js b/stories/buttons/Vertical.stories.js index ea8451b..7024433 100644 --- a/stories/buttons/Vertical.stories.js +++ b/stories/buttons/Vertical.stories.js @@ -37,10 +37,10 @@ export const Default = { - + Number @@ -54,10 +54,10 @@ export const Disabled = { - + Number diff --git a/stories/controls/AddCustomField.stories.js b/stories/controls/AddCustomField.stories.js index 1d80189..bfa477d 100644 --- a/stories/controls/AddCustomField.stories.js +++ b/stories/controls/AddCustomField.stories.js @@ -1,7 +1,5 @@ import React, { useState } from "react"; import AddCustomField from "../../app/components/controls/AddCustomField"; -import { MessagesProvider } from "../../app/contexts/MessagesContext"; -import messages from "../../messages/en/index"; /** Figma: Add Custom Field — node `20235:12994` (Community Rule System). */ export default { @@ -9,11 +7,9 @@ export default { component: AddCustomField, decorators: [ (Story) => ( - -

- -
- +
+ +
), ], }; diff --git a/stories/controls/Chip.stories.js b/stories/controls/Chip.stories.js index 0370303..6f3b6a0 100644 --- a/stories/controls/Chip.stories.js +++ b/stories/controls/Chip.stories.js @@ -1,4 +1,9 @@ import Chip from "../../app/components/controls/Chip"; +import { + CHIP_PALETTE_OPTIONS, + CHIP_SIZE_OPTIONS, + CHIP_STATE_OPTIONS, +} from "../../lib/propNormalization"; export default { title: "Components/Controls/Chip", @@ -13,17 +18,17 @@ export default { }, state: { control: "select", - options: ["unselected", "selected", "disabled", "custom"], + options: [...CHIP_STATE_OPTIONS], description: "Visual state of the chip", }, palette: { control: "select", - options: ["default", "inverse"], + options: [...CHIP_PALETTE_OPTIONS], description: "Color palette of the chip", }, size: { control: "select", - options: ["s", "m"], + options: [...CHIP_SIZE_OPTIONS], description: "Size of the chip", }, disabled: { diff --git a/stories/controls/SelectInput.stories.js b/stories/controls/SelectInput.stories.js index ccd56da..cd5549b 100644 --- a/stories/controls/SelectInput.stories.js +++ b/stories/controls/SelectInput.stories.js @@ -44,43 +44,46 @@ const Template = (args) => { }; // Default story -export const Default = Template.bind({}); -Default.args = { - label: "Default Select Input", - placeholder: "Choose an option", - state: "default", +export const Default = { + args: { + label: "Default Select Input", + placeholder: "Choose an option", + state: "default", + }, + render: Template, +}; // States +export const Active = { + args: { + label: "Active State", + placeholder: "Choose an option", + state: "active", + }, + render: Template, }; - -// States -export const Active = Template.bind({}); -Active.args = { - label: "Active State", - placeholder: "Choose an option", - state: "active", +export const Focus = { + args: { + label: "Focus State", + placeholder: "Choose an option", + state: "focus", + }, + render: Template, }; - -export const Focus = Template.bind({}); -Focus.args = { - label: "Focus State", - placeholder: "Choose an option", - state: "focus", +export const Error = { + args: { + label: "Error State", + placeholder: "Choose an option", + error: true, + }, + render: Template, }; - -export const Error = Template.bind({}); -Error.args = { - label: "Error State", - placeholder: "Choose an option", - error: true, -}; - -export const Disabled = Template.bind({}); -Disabled.args = { - label: "Disabled State", - placeholder: "Choose an option", - disabled: true, -}; - -// Interactive example +export const Disabled = { + args: { + label: "Disabled State", + placeholder: "Choose an option", + disabled: true, + }, + render: Template, +}; // Interactive example export const Interactive = (args) => { const [value, setValue] = useState(""); diff --git a/stories/controls/Switch.stories.js b/stories/controls/Switch.stories.js index 8e91793..180c6e3 100644 --- a/stories/controls/Switch.stories.js +++ b/stories/controls/Switch.stories.js @@ -39,32 +39,36 @@ export default { const Template = (args) => ; -export const Default = Template.bind({}); -Default.args = { - propSwitch: false, - text: "Switch label", +export const Default = { + args: { + propSwitch: false, + text: "Switch label", + }, + render: Template, }; - -export const Checked = Template.bind({}); -Checked.args = { - propSwitch: true, - text: "Switch label", +export const Checked = { + args: { + propSwitch: true, + text: "Switch label", + }, + render: Template, }; - -export const Focus = Template.bind({}); -Focus.args = { - propSwitch: false, - state: "focus", - text: "Switch label", +export const Focus = { + args: { + propSwitch: false, + state: "focus", + text: "Switch label", + }, + render: Template, }; - -export const FocusChecked = Template.bind({}); -FocusChecked.args = { - propSwitch: true, - state: "focus", - text: "Switch label", +export const FocusChecked = { + args: { + propSwitch: true, + state: "focus", + text: "Switch label", + }, + render: Template, }; - export const States = () => (
diff --git a/stories/controls/TextArea.stories.js b/stories/controls/TextArea.stories.js index 9f20188..48bc61e 100644 --- a/stories/controls/TextArea.stories.js +++ b/stories/controls/TextArea.stories.js @@ -1,5 +1,11 @@ import React from "react"; import TextArea from "../../app/components/controls/TextArea"; +import { + INPUT_STATE_OPTIONS, + LABEL_VARIANT_OPTIONS, + SMALL_MEDIUM_LARGE_OPTIONS, + TEXT_AREA_APPEARANCE_OPTIONS, +} from "../../lib/propNormalization"; export default { title: "Components/Controls/TextArea", @@ -10,15 +16,19 @@ export default { argTypes: { size: { control: { type: "select" }, - options: ["small", "medium", "large"], + options: [...SMALL_MEDIUM_LARGE_OPTIONS], }, labelVariant: { control: { type: "select" }, - options: ["default", "horizontal"], + options: [...LABEL_VARIANT_OPTIONS], + }, + appearance: { + control: { type: "select" }, + options: [...TEXT_AREA_APPEARANCE_OPTIONS], }, state: { control: { type: "select" }, - options: ["default", "active", "hover", "focus", "error"], + options: [...INPUT_STATE_OPTIONS], }, disabled: { control: { type: "boolean" }, @@ -31,64 +41,71 @@ export default { const Template = (args) =>