Merge pull request 'Catch up Storybook preview, coverage, and story hygiene' (#67) from adilallo/maintenance/StorybookCatchup into main

Reviewed-on: #67
This commit was merged in pull request #67.
This commit is contained in:
2026-08-18 01:32:09 +00:00
44 changed files with 1215 additions and 932 deletions
+22 -16
View File
@@ -28,16 +28,17 @@ Do **not** colocate `*.stories.*` next to components. The Storybook config
# File naming # File naming
- `<ComponentName>.stories.js` — matches 69/70 existing files. - `<ComponentName>.stories.js` — default; keep `.js` unless the story
- Use `.tsx` only when the story genuinely needs types (rare; prefer JS to genuinely needs types.
match the codebase convention). - Use `.tsx` only when the story uses `Meta` / `StoryObj` (rare).
- Variants get a suffix: `Button.visual.stories.js`, - Variants get a suffix: `Button.visual.stories.js`,
`Footer.responsive.stories.js`. `Footer.responsive.stories.js`.
# Default export shape (CSF2) # Default export shape (CSF3)
```javascript ```javascript
import MyComponent from "../../app/components/<area>/MyComponent"; import MyComponent from "../../app/components/<area>/MyComponent";
import { CHIP_PALETTE_OPTIONS } from "../../lib/propNormalization";
export default { export default {
title: "Components/<SubFolder>/MyComponent", title: "Components/<SubFolder>/MyComponent",
@@ -51,31 +52,32 @@ export default {
}, },
}, },
argTypes: { argTypes: {
variant: { palette: {
control: { type: "select" }, control: { type: "select" },
options: ["filled", "outline"], options: [...CHIP_PALETTE_OPTIONS],
description: "The variant (Figma prop)", description: "The palette (Figma prop)",
}, },
onClick: { action: "clicked" }, onClick: { action: "clicked" },
}, },
}; };
export const Default = { args: { variant: "filled" } }; export const Default = { args: { palette: "default" } };
``` ```
## Title hierarchy ## Title hierarchy
- Design-system components → `Components/<SubFolder>/<Name>` (e.g. - Design-system components → `Components/<SubFolder>/<Name>` (e.g.
`Components/Controls/Checkbox`). `Components/Controls/Checkbox`).
- Pages → `Pages/<PageName>` (folder: `stories/pages/`). - Create-flow screens → `Pages/Create Flow/<Step>` (folder: `stories/pages/`).
- Create flow shared pieces → `Create Flow/<Name>`. - Create flow shared pieces → `Create Flow/<Name>`.
## `argTypes` ## `argTypes`
For every Figma enum prop (`variant`, `size`, `state`, `mode`, `palette`, For every Figma enum prop (`variant`, `size`, `state`, `mode`, `palette`,
…) expose a `select` control listing the **lowercase** option set, sourced …) expose a `select` control listing the option set, sourced from the matching
from the matching `*_OPTIONS` const in `lib/propNormalization.ts`. See `*_OPTIONS` const in `lib/propNormalization.ts`. See
`.cursor/rules/component-props.mdc`. `.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 # 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()` - `MessagesProvider` with `messages/en` → access copy via `useMessages()`
inside stories exactly like app code. Never hard-code user-facing strings. inside stories exactly like app code. Never hard-code user-facing strings.
- `app/globals.css` + `.font-inter` wrapper → design tokens and fonts are - `AuthModalProvider` and `CreateFlowProvider` (same stack as
already present. `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 Do **not** add your own `MessagesProvider`, font wrapper, or token setup in a
story. If you need a new global, update `preview.js`. 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`) # Interaction tests (`play`)
Use `storybook/test` for interaction assertions — not `@testing-library/*` Use `storybook/test` for interaction assertions — not `@testing-library/*`
directly. This matches `Checkbox.stories.js` and stays compatible with the directly. This matches `Checkbox.stories.js`. Storybook is documentation;
Vitest portable-stories runner in `.storybook/vitest.setup.js`. Vitest component tests remain the source of truth.
```javascript ```javascript
import { within, userEvent, expect } from "storybook/test"; import { within, userEvent, expect } from "storybook/test";
+7 -1
View File
@@ -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 { :root {
--font-inter: --font-inter:
"Inter", ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, "Inter", ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto,
"Helvetica Neue", Arial, "Noto Sans", "Apple Color Emoji", "Segoe UI Emoji"; "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;
} }
+1 -23
View File
@@ -4,32 +4,10 @@ module.exports = {
"../stories/**/*.mdx", "../stories/**/*.mdx",
"../stories/**/*.stories.@(js|jsx|mjs|ts|tsx)", "../stories/**/*.stories.@(js|jsx|mjs|ts|tsx)",
], ],
addons: [ addons: ["@storybook/addon-docs", "@storybook/addon-a11y"],
// 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",
],
framework: { framework: {
name: "@storybook/nextjs", name: "@storybook/nextjs",
options: {}, options: {},
}, },
staticDirs: ["../public"], 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;
},
}; };
+52
View File
@@ -1,10 +1,43 @@
import "../app/globals.css"; import "../app/globals.css";
import "./fonts.css"; import "./fonts.css";
import { MINIMAL_VIEWPORTS } from "storybook/viewport";
import { AuthModalProvider } from "../app/contexts/AuthModalContext";
import { MessagesProvider } from "../app/contexts/MessagesContext"; import { MessagesProvider } from "../app/contexts/MessagesContext";
import { CreateFlowProvider } from "../app/(app)/create/context/CreateFlowContext";
import messages from "../messages/en/index"; 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 } */ /** @type { import('@storybook/react').Preview } */
const preview = { const preview = {
tags: ["autodocs"],
parameters: { parameters: {
controls: { controls: {
matchers: { matchers: {
@@ -12,13 +45,32 @@ const preview = {
date: /Date$/i, 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: [ decorators: [
(Story) => ( (Story) => (
<MessagesProvider messages={messages}> <MessagesProvider messages={messages}>
<AuthModalProvider>
<CreateFlowProvider>
<div className="font-inter"> <div className="font-inter">
<Story /> <Story />
</div> </div>
</CreateFlowProvider>
</AuthModalProvider>
</MessagesProvider> </MessagesProvider>
), ),
], ],
-7
View File
@@ -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]);
-1
View File
@@ -25,7 +25,6 @@
"@types/mdx", "@types/mdx",
"eslint-config-next", "eslint-config-next",
"typescript-eslint", "typescript-eslint",
"@storybook/nextjs-vite",
"@eslint/js", "@eslint/js",
"@next/eslint-plugin-next", "@next/eslint-plugin-next",
"eslint-plugin-react", "eslint-plugin-react",
+82 -1
View File
@@ -30,6 +30,7 @@
"@lhci/cli": "^0.15.1", "@lhci/cli": "^0.15.1",
"@playwright/test": "^1.55.0", "@playwright/test": "^1.55.0",
"@storybook/addon-a11y": "^10.2.0", "@storybook/addon-a11y": "^10.2.0",
"@storybook/addon-docs": "^10.2.0",
"@storybook/nextjs": "^10.2.0", "@storybook/nextjs": "^10.2.0",
"@storybook/react": "^10.2.0", "@storybook/react": "^10.2.0",
"@svgr/webpack": "^8.1.0", "@svgr/webpack": "^8.1.0",
@@ -5911,6 +5912,35 @@
"storybook": "^10.4.1" "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": { "node_modules/@storybook/builder-webpack5": {
"version": "10.4.1", "version": "10.4.1",
"resolved": "https://registry.npmjs.org/@storybook/builder-webpack5/-/builder-webpack5-10.4.1.tgz", "resolved": "https://registry.npmjs.org/@storybook/builder-webpack5/-/builder-webpack5-10.4.1.tgz",
@@ -6017,6 +6047,41 @@
"storybook": "^10.4.1" "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": { "node_modules/@storybook/global": {
"version": "5.0.0", "version": "5.0.0",
"resolved": "https://registry.npmjs.org/@storybook/global/-/global-5.0.0.tgz", "resolved": "https://registry.npmjs.org/@storybook/global/-/global-5.0.0.tgz",
@@ -6192,7 +6257,7 @@
"webpack": ">= 4" "webpack": ">= 4"
} }
}, },
"node_modules/@storybook/react/node_modules/@storybook/react-dom-shim": { "node_modules/@storybook/react-dom-shim": {
"version": "10.4.1", "version": "10.4.1",
"resolved": "https://registry.npmjs.org/@storybook/react-dom-shim/-/react-dom-shim-10.4.1.tgz", "resolved": "https://registry.npmjs.org/@storybook/react-dom-shim/-/react-dom-shim-10.4.1.tgz",
"integrity": "sha512-6QFqfDNH4DMrt7yHKRfpqRopsVUc/Az+sXIdJ39IetYnHUxL3nW4NVaPc6uy/8Qi8urzUyEXL/nn7cpSIP2aPQ==", "integrity": "sha512-6QFqfDNH4DMrt7yHKRfpqRopsVUc/Az+sXIdJ39IetYnHUxL3nW4NVaPc6uy/8Qi8urzUyEXL/nn7cpSIP2aPQ==",
@@ -23811,6 +23876,22 @@
"node": ">= 0.8" "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": { "node_modules/unrs-resolver": {
"version": "1.12.2", "version": "1.12.2",
"resolved": "https://registry.npmjs.org/unrs-resolver/-/unrs-resolver-1.12.2.tgz", "resolved": "https://registry.npmjs.org/unrs-resolver/-/unrs-resolver-1.12.2.tgz",
+1 -2
View File
@@ -17,9 +17,7 @@
"lint": "eslint . --ext .js,.jsx,.ts,.tsx --max-warnings 9999", "lint": "eslint . --ext .js,.jsx,.ts,.tsx --max-warnings 9999",
"postinstall": "npm rebuild lightningcss && prisma generate", "postinstall": "npm rebuild lightningcss && prisma generate",
"storybook": "storybook dev -p 6006", "storybook": "storybook dev -p 6006",
"storybook:github": "STORYBOOK_BASE_PATH=true storybook dev -p 6006",
"storybook:build": "storybook build", "storybook:build": "storybook build",
"storybook:build:github": "STORYBOOK_BASE_PATH=true storybook build",
"knip": "knip --include files,exports --exclude duplicates", "knip": "knip --include files,exports --exclude duplicates",
"test": "vitest run --coverage", "test": "vitest run --coverage",
"test:watch": "vitest", "test:watch": "vitest",
@@ -69,6 +67,7 @@
"@lhci/cli": "^0.15.1", "@lhci/cli": "^0.15.1",
"@playwright/test": "^1.55.0", "@playwright/test": "^1.55.0",
"@storybook/addon-a11y": "^10.2.0", "@storybook/addon-a11y": "^10.2.0",
"@storybook/addon-docs": "^10.2.0",
"@storybook/nextjs": "^10.2.0", "@storybook/nextjs": "^10.2.0",
"@storybook/react": "^10.2.0", "@storybook/react": "^10.2.0",
"@svgr/webpack": "^8.1.0", "@svgr/webpack": "^8.1.0",
+8 -3
View File
@@ -1,4 +1,9 @@
import Button from "../../app/components/buttons/Button"; import Button from "../../app/components/buttons/Button";
import {
BUTTON_PALETTE_OPTIONS,
BUTTON_TYPE_OPTIONS,
SIZE_OPTIONS,
} from "../../lib/propNormalization";
export default { export default {
title: "Components/Buttons/Button", title: "Components/Buttons/Button",
@@ -15,17 +20,17 @@ export default {
argTypes: { argTypes: {
buttonType: { buttonType: {
control: { type: "select" }, control: { type: "select" },
options: ["filled", "outline", "ghost", "danger"], options: [...BUTTON_TYPE_OPTIONS],
description: "The button type (Figma prop)", description: "The button type (Figma prop)",
}, },
palette: { palette: {
control: { type: "select" }, control: { type: "select" },
options: ["default", "inverse"], options: [...BUTTON_PALETTE_OPTIONS],
description: "The button palette (Figma prop)", description: "The button palette (Figma prop)",
}, },
size: { size: {
control: { type: "select" }, control: { type: "select" },
options: ["xsmall", "small", "medium", "large", "xlarge"], options: [...SIZE_OPTIONS],
description: "The size of the button", description: "The size of the button",
}, },
disabled: { disabled: {
+1 -1
View File
@@ -33,7 +33,7 @@ export const Default = {
export const InParagraph = { export const InParagraph = {
render: () => ( render: () => (
<p className="max-w-md font-inter text-[14px] leading-[20px] text-[color:var(--color-content-default-primary,#fff)]"> <p className="max-w-md text-small-paragraph text-[var(--color-content-default-primary)]">
Share a bit more detail so the group can weigh in. You can always{" "} Share a bit more detail so the group can weigh in. You can always{" "}
<InlineTextButton onClick={() => {}}>expand this later</InlineTextButton>{" "} <InlineTextButton onClick={() => {}}>expand this later</InlineTextButton>{" "}
if you need more room. if you need more room.
+4 -4
View File
@@ -37,10 +37,10 @@ export const Default = {
<Icon <Icon
name="number" name="number"
size={32} size={32}
className="text-[var(--color-content-default-brand-primary,#fefcc9)]" className="text-[var(--color-content-default-brand-primary)]"
/> />
</span> </span>
<span className="w-full text-center font-inter text-[14px] font-medium leading-[18px] text-[var(--color-content-default-brand-primary,#fefcc9)]"> <span className="w-full text-center text-small-paragraph font-medium text-[var(--color-content-default-brand-primary)]">
Number Number
</span> </span>
</Vertical> </Vertical>
@@ -54,10 +54,10 @@ export const Disabled = {
<Icon <Icon
name="number" name="number"
size={32} size={32}
className="text-[var(--color-content-default-brand-primary,#fefcc9)]" className="text-[var(--color-content-default-brand-primary)]"
/> />
</span> </span>
<span className="w-full text-center font-inter text-[14px] font-medium leading-[18px] text-[var(--color-content-default-brand-primary,#fefcc9)]"> <span className="w-full text-center text-small-paragraph font-medium text-[var(--color-content-default-brand-primary)]">
Number Number
</span> </span>
</Vertical> </Vertical>
@@ -1,7 +1,5 @@
import React, { useState } from "react"; import React, { useState } from "react";
import AddCustomField from "../../app/components/controls/AddCustomField"; 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). */ /** Figma: Add Custom Field — node `20235:12994` (Community Rule System). */
export default { export default {
@@ -9,11 +7,9 @@ export default {
component: AddCustomField, component: AddCustomField,
decorators: [ decorators: [
(Story) => ( (Story) => (
<MessagesProvider messages={messages}>
<div className="w-[min(100%,546px)] bg-[var(--color-surface-default-primary)] p-6"> <div className="w-[min(100%,546px)] bg-[var(--color-surface-default-primary)] p-6">
<Story /> <Story />
</div> </div>
</MessagesProvider>
), ),
], ],
}; };
+8 -3
View File
@@ -1,4 +1,9 @@
import Chip from "../../app/components/controls/Chip"; import Chip from "../../app/components/controls/Chip";
import {
CHIP_PALETTE_OPTIONS,
CHIP_SIZE_OPTIONS,
CHIP_STATE_OPTIONS,
} from "../../lib/propNormalization";
export default { export default {
title: "Components/Controls/Chip", title: "Components/Controls/Chip",
@@ -13,17 +18,17 @@ export default {
}, },
state: { state: {
control: "select", control: "select",
options: ["unselected", "selected", "disabled", "custom"], options: [...CHIP_STATE_OPTIONS],
description: "Visual state of the chip", description: "Visual state of the chip",
}, },
palette: { palette: {
control: "select", control: "select",
options: ["default", "inverse"], options: [...CHIP_PALETTE_OPTIONS],
description: "Color palette of the chip", description: "Color palette of the chip",
}, },
size: { size: {
control: "select", control: "select",
options: ["s", "m"], options: [...CHIP_SIZE_OPTIONS],
description: "Size of the chip", description: "Size of the chip",
}, },
disabled: { disabled: {
+22 -19
View File
@@ -44,43 +44,46 @@ const Template = (args) => {
}; };
// Default story // Default story
export const Default = Template.bind({}); export const Default = {
Default.args = { args: {
label: "Default Select Input", label: "Default Select Input",
placeholder: "Choose an option", placeholder: "Choose an option",
state: "default", state: "default",
}; },
render: Template,
// States }; // States
export const Active = Template.bind({}); export const Active = {
Active.args = { args: {
label: "Active State", label: "Active State",
placeholder: "Choose an option", placeholder: "Choose an option",
state: "active", state: "active",
},
render: Template,
}; };
export const Focus = {
export const Focus = Template.bind({}); args: {
Focus.args = {
label: "Focus State", label: "Focus State",
placeholder: "Choose an option", placeholder: "Choose an option",
state: "focus", state: "focus",
},
render: Template,
}; };
export const Error = {
export const Error = Template.bind({}); args: {
Error.args = {
label: "Error State", label: "Error State",
placeholder: "Choose an option", placeholder: "Choose an option",
error: true, error: true,
},
render: Template,
}; };
export const Disabled = {
export const Disabled = Template.bind({}); args: {
Disabled.args = {
label: "Disabled State", label: "Disabled State",
placeholder: "Choose an option", placeholder: "Choose an option",
disabled: true, disabled: true,
}; },
render: Template,
// Interactive example }; // Interactive example
export const Interactive = (args) => { export const Interactive = (args) => {
const [value, setValue] = useState(""); const [value, setValue] = useState("");
+16 -12
View File
@@ -39,32 +39,36 @@ export default {
const Template = (args) => <Switch {...args} />; const Template = (args) => <Switch {...args} />;
export const Default = Template.bind({}); export const Default = {
Default.args = { args: {
propSwitch: false, propSwitch: false,
text: "Switch label", text: "Switch label",
},
render: Template,
}; };
export const Checked = {
export const Checked = Template.bind({}); args: {
Checked.args = {
propSwitch: true, propSwitch: true,
text: "Switch label", text: "Switch label",
},
render: Template,
}; };
export const Focus = {
export const Focus = Template.bind({}); args: {
Focus.args = {
propSwitch: false, propSwitch: false,
state: "focus", state: "focus",
text: "Switch label", text: "Switch label",
},
render: Template,
}; };
export const FocusChecked = {
export const FocusChecked = Template.bind({}); args: {
FocusChecked.args = {
propSwitch: true, propSwitch: true,
state: "focus", state: "focus",
text: "Switch label", text: "Switch label",
},
render: Template,
}; };
export const States = () => ( export const States = () => (
<div className="space-y-4"> <div className="space-y-4">
<div className="space-y-2"> <div className="space-y-2">
+41 -24
View File
@@ -1,5 +1,11 @@
import React from "react"; import React from "react";
import TextArea from "../../app/components/controls/TextArea"; 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 { export default {
title: "Components/Controls/TextArea", title: "Components/Controls/TextArea",
@@ -10,15 +16,19 @@ export default {
argTypes: { argTypes: {
size: { size: {
control: { type: "select" }, control: { type: "select" },
options: ["small", "medium", "large"], options: [...SMALL_MEDIUM_LARGE_OPTIONS],
}, },
labelVariant: { labelVariant: {
control: { type: "select" }, control: { type: "select" },
options: ["default", "horizontal"], options: [...LABEL_VARIANT_OPTIONS],
},
appearance: {
control: { type: "select" },
options: [...TEXT_AREA_APPEARANCE_OPTIONS],
}, },
state: { state: {
control: { type: "select" }, control: { type: "select" },
options: ["default", "active", "hover", "focus", "error"], options: [...INPUT_STATE_OPTIONS],
}, },
disabled: { disabled: {
control: { type: "boolean" }, control: { type: "boolean" },
@@ -31,47 +41,52 @@ export default {
const Template = (args) => <TextArea {...args} />; const Template = (args) => <TextArea {...args} />;
export const Default = Template.bind({}); export const Default = {
Default.args = { args: {
label: "Text Area", label: "Text Area",
placeholder: "Enter text...", placeholder: "Enter text...",
value: "", value: "",
},
render: Template,
}; };
export const WithValue = {
export const WithValue = Template.bind({}); args: {
WithValue.args = {
label: "Text Area", label: "Text Area",
placeholder: "Enter text...", placeholder: "Enter text...",
value: value:
"This is some sample text content that demonstrates how the text area looks with content.", "This is some sample text content that demonstrates how the text area looks with content.",
},
render: Template,
}; };
export const Small = {
export const Small = Template.bind({}); args: {
Small.args = {
size: "small", size: "small",
label: "Small Text Area", label: "Small Text Area",
placeholder: "Enter text...", placeholder: "Enter text...",
value: "", value: "",
},
render: Template,
}; };
export const Medium = {
export const Medium = Template.bind({}); args: {
Medium.args = {
size: "medium", size: "medium",
label: "Medium Text Area", label: "Medium Text Area",
placeholder: "Enter text...", placeholder: "Enter text...",
value: "", value: "",
},
render: Template,
}; };
export const Large = {
export const Large = Template.bind({}); args: {
Large.args = {
size: "large", size: "large",
label: "Large Text Area", label: "Large Text Area",
placeholder: "Enter text...", placeholder: "Enter text...",
value: "", value: "",
},
render: Template,
}; };
export const Embedded = {
export const Embedded = Template.bind({}); args: {
Embedded.args = {
label: "Section content", label: "Section content",
placeholder: "Enter text...", placeholder: "Enter text...",
value: value:
@@ -79,16 +94,18 @@ Embedded.args = {
appearance: "embedded", appearance: "embedded",
size: "large", size: "large",
rows: 4, rows: 4,
},
render: Template,
}; };
export const HorizontalLabel = {
export const HorizontalLabel = Template.bind({}); args: {
HorizontalLabel.args = {
labelVariant: "horizontal", labelVariant: "horizontal",
label: "Horizontal Label", label: "Horizontal Label",
placeholder: "Enter text...", placeholder: "Enter text...",
value: "", value: "",
},
render: Template,
}; };
export const AllSizes = () => ( export const AllSizes = () => (
<div className="space-y-6"> <div className="space-y-6">
<div className="space-y-4"> <div className="space-y-4">
+41 -32
View File
@@ -1,5 +1,9 @@
import React from "react"; import React from "react";
import TextInput from "../../app/components/controls/TextInput"; import TextInput from "../../app/components/controls/TextInput";
import {
INPUT_STATE_OPTIONS,
TEXT_INPUT_SIZE_OPTIONS,
} from "../../lib/propNormalization";
export default { export default {
title: "Components/Controls/TextInput", title: "Components/Controls/TextInput",
@@ -10,11 +14,11 @@ export default {
argTypes: { argTypes: {
inputSize: { inputSize: {
control: { type: "select" }, control: { type: "select" },
options: ["small", "medium", "Small", "Medium"], options: [...TEXT_INPUT_SIZE_OPTIONS],
}, },
state: { state: {
control: { type: "select" }, control: { type: "select" },
options: ["default", "active", "hover", "focus", "error", "disabled"], options: [...INPUT_STATE_OPTIONS],
}, },
disabled: { disabled: {
control: { type: "boolean" }, control: { type: "boolean" },
@@ -37,75 +41,80 @@ export default {
const Template = (args) => <TextInput {...args} />; const Template = (args) => <TextInput {...args} />;
// Default story // Default story
export const Default = Template.bind({}); export const Default = {
Default.args = { args: {
label: "Default Text Input", label: "Default Text Input",
placeholder: "Enter text...", placeholder: "Enter text...",
inputSize: "medium", inputSize: "medium",
state: "default", state: "default",
}; },
render: Template,
// Size variants }; // Size variants
export const Small = Template.bind({}); export const Small = {
Small.args = { args: {
label: "Small Text Input", label: "Small Text Input",
placeholder: "Small size", placeholder: "Small size",
inputSize: "small", inputSize: "small",
state: "default", state: "default",
},
render: Template,
}; };
export const Medium = {
export const Medium = Template.bind({}); args: {
Medium.args = {
label: "Medium Text Input", label: "Medium Text Input",
placeholder: "Medium size", placeholder: "Medium size",
inputSize: "medium", inputSize: "medium",
state: "default", state: "default",
}; },
render: Template,
// States }; // States
export const Active = Template.bind({}); export const Active = {
Active.args = { args: {
label: "Active State", label: "Active State",
placeholder: "Active input", placeholder: "Active input",
inputSize: "medium", inputSize: "medium",
state: "active", state: "active",
},
render: Template,
}; };
export const Hover = {
export const Hover = Template.bind({}); args: {
Hover.args = {
label: "Hover State", label: "Hover State",
placeholder: "Hover input", placeholder: "Hover input",
inputSize: "medium", inputSize: "medium",
state: "hover", state: "hover",
},
render: Template,
}; };
export const Focus = {
export const Focus = Template.bind({}); args: {
Focus.args = {
label: "Focus State", label: "Focus State",
placeholder: "Focused input", placeholder: "Focused input",
inputSize: "medium", inputSize: "medium",
state: "focus", state: "focus",
},
render: Template,
}; };
export const Error = {
export const Error = Template.bind({}); args: {
Error.args = {
label: "Error State", label: "Error State",
placeholder: "Error input", placeholder: "Error input",
inputSize: "medium", inputSize: "medium",
state: "default", state: "default",
error: true, error: true,
},
render: Template,
}; };
export const Disabled = {
export const Disabled = Template.bind({}); args: {
Disabled.args = {
label: "Disabled State", label: "Disabled State",
placeholder: "Disabled input", placeholder: "Disabled input",
inputSize: "medium", inputSize: "medium",
state: "default", state: "default",
disabled: true, disabled: true,
}; },
render: Template,
// Interactive example }; // Interactive example
export const Interactive = (args) => { export const Interactive = (args) => {
const [value, setValue] = React.useState(""); const [value, setValue] = React.useState("");
+8 -6
View File
@@ -44,22 +44,24 @@ export const States = () => (
</div> </div>
); );
export const WithText = Template.bind({}); export const WithText = {
WithText.args = { args: {
label: "Text Toggle", label: "Text Toggle",
checked: false, checked: false,
showText: true, showText: true,
text: "Toggle", text: "Toggle",
},
render: Template,
}; };
export const WithIcon = {
export const WithIcon = Template.bind({}); args: {
WithIcon.args = {
label: "Icon Toggle", label: "Icon Toggle",
checked: false, checked: false,
showIcon: true, showIcon: true,
icon: "I", icon: "I",
},
render: Template,
}; };
export const Interactive = () => { export const Interactive = () => {
const [checked, setChecked] = React.useState(false); const [checked, setChecked] = React.useState(false);
const [state, setState] = React.useState("default"); const [state, setState] = React.useState("default");
+26 -17
View File
@@ -1,5 +1,9 @@
import React from "react"; import React from "react";
import ToggleGroup from "../../app/components/controls/ToggleGroup"; import ToggleGroup from "../../app/components/controls/ToggleGroup";
import {
STATE_OPTIONS,
TOGGLE_GROUP_POSITION_OPTIONS,
} from "../../lib/propNormalization";
export default { export default {
title: "Components/Controls/ToggleGroup", title: "Components/Controls/ToggleGroup",
@@ -10,11 +14,11 @@ export default {
argTypes: { argTypes: {
position: { position: {
control: { type: "select" }, control: { type: "select" },
options: ["left", "middle", "right"], options: [...TOGGLE_GROUP_POSITION_OPTIONS],
}, },
state: { state: {
control: { type: "select" }, control: { type: "select" },
options: ["default", "hover", "focus", "selected"], options: [...STATE_OPTIONS],
}, },
showText: { showText: {
control: { type: "boolean" }, control: { type: "boolean" },
@@ -24,27 +28,30 @@ export default {
const Template = (args) => <ToggleGroup {...args}>Toggle Item</ToggleGroup>; const Template = (args) => <ToggleGroup {...args}>Toggle Item</ToggleGroup>;
export const Default = Template.bind({}); export const Default = {
Default.args = { args: {
position: "left", position: "left",
state: "default", state: "default",
showText: true, showText: true,
},
render: Template,
}; };
export const Middle = {
export const Middle = Template.bind({}); args: {
Middle.args = {
position: "middle", position: "middle",
state: "default", state: "default",
showText: true, showText: true,
},
render: Template,
}; };
export const Right = {
export const Right = Template.bind({}); args: {
Right.args = {
position: "right", position: "right",
state: "default", state: "default",
showText: true, showText: true,
},
render: Template,
}; };
export const States = () => ( export const States = () => (
<div className="space-y-4"> <div className="space-y-4">
<div className="space-y-2"> <div className="space-y-2">
@@ -89,22 +96,24 @@ export const Positions = () => (
</div> </div>
); );
export const WithText = Template.bind({}); export const WithText = {
WithText.args = { args: {
position: "left", position: "left",
state: "default", state: "default",
showText: true, showText: true,
children: "Active Deals", children: "Active Deals",
},
render: Template,
}; };
export const WithoutText = {
export const WithoutText = Template.bind({}); args: {
WithoutText.args = {
position: "left", position: "left",
state: "default", state: "default",
showText: false, showText: false,
children: "☰", children: "☰",
},
render: Template,
}; };
export const WithIcons = () => ( export const WithIcons = () => (
<div className="space-y-4"> <div className="space-y-4">
<div className="space-y-2"> <div className="space-y-2">
+35 -25
View File
@@ -34,87 +34,97 @@ export default {
const Template = (args) => <Upload {...args} />; const Template = (args) => <Upload {...args} />;
// Default story // Default story
export const Default = Template.bind({}); export const Default = {
Default.args = { args: {
label: "Upload", label: "Upload",
active: true, active: true,
showHelpIcon: true, showHelpIcon: true,
}; },
render: Template,
// Active state }; // Active state
export const Active = Template.bind({}); export const Active = {
Active.args = { args: {
label: "Upload", label: "Upload",
active: true, active: true,
showHelpIcon: true, showHelpIcon: true,
}; },
Active.parameters = { parameters: {
docs: { docs: {
description: { description: {
story: story:
"Upload component in active state with white button and black text.", "Upload component in active state with white button and black text.",
}, },
}, },
},
render: Template,
}; };
// Inactive state // Inactive state
export const Inactive = Template.bind({}); export const Inactive = {
Inactive.args = { args: {
label: "Upload", label: "Upload",
active: false, active: false,
showHelpIcon: true, showHelpIcon: true,
}; },
Inactive.parameters = { parameters: {
docs: { docs: {
description: { description: {
story: story:
"Upload component in inactive state with dark button and gray text.", "Upload component in inactive state with dark button and gray text.",
}, },
}, },
},
render: Template,
}; };
// Without help icon // Without help icon
export const WithoutHelpIcon = Template.bind({}); export const WithoutHelpIcon = {
WithoutHelpIcon.args = { args: {
label: "Upload", label: "Upload",
active: true, active: true,
showHelpIcon: false, showHelpIcon: false,
}; },
WithoutHelpIcon.parameters = { parameters: {
docs: { docs: {
description: { description: {
story: "Upload component without help icon.", story: "Upload component without help icon.",
}, },
}, },
},
render: Template,
}; };
// Without label // Without label
export const WithoutLabel = Template.bind({}); export const WithoutLabel = {
WithoutLabel.args = { args: {
active: true, active: true,
showHelpIcon: false, showHelpIcon: false,
}; },
WithoutLabel.parameters = { parameters: {
docs: { docs: {
description: { description: {
story: "Upload component without label.", story: "Upload component without label.",
}, },
}, },
},
render: Template,
}; };
// Custom label // Custom label
export const CustomLabel = Template.bind({}); export const CustomLabel = {
CustomLabel.args = { args: {
label: "Upload Files", label: "Upload Files",
active: true, active: true,
showHelpIcon: true, showHelpIcon: true,
}; },
CustomLabel.parameters = { parameters: {
docs: { docs: {
description: { description: {
story: "Upload component with custom label text.", story: "Upload component with custom label text.",
}, },
}, },
},
render: Template,
}; };
// All states comparison // All states comparison
@@ -1,16 +1,19 @@
import React from "react"; import React from "react";
import HeaderLockup from "../../app/components/type/HeaderLockup"; import HeaderLockup from "../../app/components/type/HeaderLockup";
import InfoMessageBox from "../../app/components/controls/InfoMessageBox"; import InfoMessageBox from "../../app/components/controls/InfoMessageBox";
import {
HEADER_LOCKUP_JUSTIFICATION_OPTIONS,
HEADER_LOCKUP_SIZE_OPTIONS,
} from "../../lib/propNormalization";
/** /**
* Compose pattern used by create-flow decision approaches rail: headline lockup * Compose pattern used by create-flow decision approaches rail: headline lockup
* plus bordered checklist no standalone wrapper component. * plus bordered checklist no standalone wrapper component.
*/ */
export default { export default {
title: "Components/Controls/Rail header (lockup + info box)", title: "Create Flow/Rail header (lockup + info box)",
parameters: { parameters: {
layout: "centered", layout: "centered",
backgrounds: { default: "dark" },
docs: { docs: {
description: { description: {
component: component:
@@ -18,14 +21,23 @@ export default {
}, },
}, },
}, },
argTypes: {
size: {
control: { type: "select" },
options: [...HEADER_LOCKUP_SIZE_OPTIONS],
},
justification: {
control: { type: "select" },
options: [...HEADER_LOCKUP_JUSTIFICATION_OPTIONS],
},
},
decorators: [ decorators: [
(Story) => ( (Story) => (
<div className="bg-black p-8 max-w-lg w-full"> <div className="w-full max-w-lg bg-[var(--color-surface-default-primary)] p-8">
<Story /> <Story />
</div> </div>
), ),
], ],
tags: ["autodocs"],
}; };
const messageItems = [ const messageItems = [
+73
View File
@@ -0,0 +1,73 @@
import ListItem from "../../app/components/layout/ListItem";
import { ICON_NAME_OPTIONS } from "../../app/components/asset/icon";
export default {
title: "Components/Layout/ListItem",
component: ListItem,
parameters: {
layout: "centered",
docs: {
description: {
component:
"Icon + label menuitem row used in Popover export menus and CreateFlowTopNav action menus. Distinct from List / ListEntry data rows.",
},
},
},
argTypes: {
label: { control: { type: "text" } },
leadingIcon: {
control: { type: "select" },
options: [...ICON_NAME_OPTIONS],
},
showDivider: { control: { type: "boolean" } },
variant: {
control: { type: "select" },
options: ["default", "destructive"],
},
onClick: { action: "clicked" },
},
decorators: [
(Story) => (
<div className="w-[240px] bg-[var(--color-surface-default-primary)]">
<Story />
</div>
),
],
};
export const Default = {
args: {
label: "PDF",
leadingIcon: "picture_as_pdf",
showDivider: true,
},
};
export const ExportMenu = {
render: (args) => (
<div>
<ListItem
{...args}
label="PDF"
leadingIcon="picture_as_pdf"
showDivider
/>
<ListItem {...args} label="CSV" leadingIcon="csv" showDivider />
<ListItem
{...args}
label="Markdown"
leadingIcon="markdown_copy"
showDivider={false}
/>
</div>
),
};
export const Destructive = {
args: {
label: "Log out",
leadingIcon: "log_out",
showDivider: false,
variant: "destructive",
},
};
+50 -37
View File
@@ -42,119 +42,132 @@ const Template = (args) => {
); );
}; };
export const ToastDefault = Template.bind({}); export const ToastDefault = {
ToastDefault.args = { args: {
title: "Short alert toast message goes here", title: "Short alert toast message goes here",
description: description:
"Nascetur ipsum a nisi tempor cras nam neque volutpat. Aliquam id est faucibus nunc quis. Eleifend suspendisse.", "Nascetur ipsum a nisi tempor cras nam neque volutpat. Aliquam id est faucibus nunc quis. Eleifend suspendisse.",
status: "default", status: "default",
type: "toast", type: "toast",
},
render: Template,
}; };
export const ToastPositive = {
export const ToastPositive = Template.bind({}); args: {
ToastPositive.args = {
title: "Short alert toast message goes here", title: "Short alert toast message goes here",
description: description:
"Nascetur ipsum a nisi tempor cras nam neque volutpat. Aliquam id est faucibus nunc quis. Eleifend suspendisse.", "Nascetur ipsum a nisi tempor cras nam neque volutpat. Aliquam id est faucibus nunc quis. Eleifend suspendisse.",
status: "positive", status: "positive",
type: "toast", type: "toast",
},
render: Template,
}; };
export const ToastWarning = {
export const ToastWarning = Template.bind({}); args: {
ToastWarning.args = {
title: "Short alert toast message goes here", title: "Short alert toast message goes here",
description: description:
"Nascetur ipsum a nisi tempor cras nam neque volutpat. Aliquam id est faucibus nunc quis. Eleifend suspendisse.", "Nascetur ipsum a nisi tempor cras nam neque volutpat. Aliquam id est faucibus nunc quis. Eleifend suspendisse.",
status: "warning", status: "warning",
type: "toast", type: "toast",
},
render: Template,
}; };
export const ToastDanger = {
export const ToastDanger = Template.bind({}); args: {
ToastDanger.args = {
title: "Short alert toast message goes here", title: "Short alert toast message goes here",
description: description:
"Nascetur ipsum a nisi tempor cras nam neque volutpat. Aliquam id est faucibus nunc quis. Eleifend suspendisse.", "Nascetur ipsum a nisi tempor cras nam neque volutpat. Aliquam id est faucibus nunc quis. Eleifend suspendisse.",
status: "danger", status: "danger",
type: "toast", type: "toast",
},
render: Template,
}; };
export const Banner = {
export const Banner = Template.bind({}); args: {
Banner.args = {
title: "Short alert banner message goes here", title: "Short alert banner message goes here",
description: description:
"Nascetur ipsum a nisi tempor cras nam neque volutpat. Aliquam id est faucibus nunc quis. Eleifend suspendisse.", "Nascetur ipsum a nisi tempor cras nam neque volutpat. Aliquam id est faucibus nunc quis. Eleifend suspendisse.",
status: "default", status: "default",
type: "banner", type: "banner",
},
render: Template,
}; };
export const BannerPositive = {
export const BannerPositive = Template.bind({}); args: {
BannerPositive.args = {
title: "Short alert banner message goes here", title: "Short alert banner message goes here",
description: description:
"Nascetur ipsum a nisi tempor cras nam neque volutpat. Aliquam id est faucibus nunc quis. Eleifend suspendisse.", "Nascetur ipsum a nisi tempor cras nam neque volutpat. Aliquam id est faucibus nunc quis. Eleifend suspendisse.",
status: "positive", status: "positive",
type: "banner", type: "banner",
},
render: Template,
}; };
export const BannerWarning = {
export const BannerWarning = Template.bind({}); args: {
BannerWarning.args = {
title: "Short alert banner message goes here", title: "Short alert banner message goes here",
description: description:
"Nascetur ipsum a nisi tempor cras nam neque volutpat. Aliquam id est faucibus nunc quis. Eleifend suspendisse.", "Nascetur ipsum a nisi tempor cras nam neque volutpat. Aliquam id est faucibus nunc quis. Eleifend suspendisse.",
status: "warning", status: "warning",
type: "banner", type: "banner",
},
render: Template,
}; };
export const BannerDanger = {
export const BannerDanger = Template.bind({}); args: {
BannerDanger.args = {
title: "Short alert banner message goes here", title: "Short alert banner message goes here",
description: description:
"Nascetur ipsum a nisi tempor cras nam neque volutpat. Aliquam id est faucibus nunc quis. Eleifend suspendisse.", "Nascetur ipsum a nisi tempor cras nam neque volutpat. Aliquam id est faucibus nunc quis. Eleifend suspendisse.",
status: "danger", status: "danger",
type: "banner", type: "banner",
},
render: Template,
}; };
export const TitleOnly = {
export const TitleOnly = Template.bind({}); args: {
TitleOnly.args = {
title: "Short alert banner message goes here", title: "Short alert banner message goes here",
status: "default", status: "default",
type: "toast", type: "toast",
},
render: Template,
}; };
export const ToastSmall = {
export const ToastSmall = Template.bind({}); args: {
ToastSmall.args = {
title: "Short alert toast message goes here", title: "Short alert toast message goes here",
description: description:
"Nascetur ipsum a nisi tempor cras nam neque volutpat. Aliquam id est faucibus nunc quis. Eleifend suspendisse.", "Nascetur ipsum a nisi tempor cras nam neque volutpat. Aliquam id est faucibus nunc quis. Eleifend suspendisse.",
status: "default", status: "default",
type: "toast", type: "toast",
size: "s", size: "s",
},
render: Template,
}; };
export const BannerSmall = {
export const BannerSmall = Template.bind({}); args: {
BannerSmall.args = {
title: "Short alert banner message goes here", title: "Short alert banner message goes here",
description: description:
"Nascetur ipsum a nisi tempor cras nam neque volutpat. Aliquam id est faucibus nunc quis. Eleifend suspendisse.", "Nascetur ipsum a nisi tempor cras nam neque volutpat. Aliquam id est faucibus nunc quis. Eleifend suspendisse.",
status: "positive", status: "positive",
type: "banner", type: "banner",
size: "s", size: "s",
},
render: Template,
}; };
const NoDismissTemplate = (args) => ( const NoDismissTemplate = (args) => (
<div className="p-8 max-w-[600px]"> <div className="p-8 max-w-[600px]">
<Alert {...args} /> <Alert {...args} />
</div> </div>
); );
export const BannerNoDismiss = NoDismissTemplate.bind({}); export const BannerNoDismiss = {
BannerNoDismiss.args = { args: {
title: "Non-dismissible banner (no onClose)", title: "Non-dismissible banner (no onClose)",
description: "Used when the message clears via navigation or parent state only.", description:
"Used when the message clears via navigation or parent state only.",
status: "danger", status: "danger",
type: "banner", type: "banner",
size: "s", size: "s",
},
render: NoDismissTemplate,
}; };
export const AllStatuses = () => { export const AllStatuses = () => {
const [visible, setVisible] = useState({ const [visible, setVisible] = useState({
default: true, default: true,
+34 -24
View File
@@ -54,8 +54,8 @@ const Template = (args) => {
); );
}; };
export const Default = Template.bind({}); export const Default = {
Default.args = { args: {
isOpen: true, isOpen: true,
title: "What do you call your group's new policy?", title: "What do you call your group's new policy?",
description: "You can also combine or add new approaches to the list", description: "You can also combine or add new approaches to the list",
@@ -72,10 +72,11 @@ Default.args = {
backButtonText: "Back", backButtonText: "Back",
nextButtonText: "Next", nextButtonText: "Next",
nextButtonDisabled: false, nextButtonDisabled: false,
},
render: Template,
}; };
export const WithStepper = {
export const WithStepper = Template.bind({}); args: {
WithStepper.args = {
isOpen: true, isOpen: true,
title: "What do you call your group's new policy?", title: "What do you call your group's new policy?",
description: "You can also combine or add new approaches to the list", description: "You can also combine or add new approaches to the list",
@@ -94,10 +95,11 @@ WithStepper.args = {
nextButtonDisabled: false, nextButtonDisabled: false,
currentStep: 1, currentStep: 1,
totalSteps: 3, totalSteps: 3,
},
render: Template,
}; };
export const Step2 = {
export const Step2 = Template.bind({}); args: {
Step2.args = {
isOpen: true, isOpen: true,
title: "How should conflicts be resolved?", title: "How should conflicts be resolved?",
description: "You can also combine or add new approaches to the list", description: "You can also combine or add new approaches to the list",
@@ -113,10 +115,11 @@ Step2.args = {
nextButtonDisabled: false, nextButtonDisabled: false,
currentStep: 2, currentStep: 2,
totalSteps: 3, totalSteps: 3,
},
render: Template,
}; };
export const Step3 = {
export const Step3 = Template.bind({}); args: {
Step3.args = {
isOpen: true, isOpen: true,
title: "Final step", title: "Final step",
description: "Review your settings", description: "Review your settings",
@@ -134,10 +137,11 @@ Step3.args = {
nextButtonDisabled: false, nextButtonDisabled: false,
currentStep: 3, currentStep: 3,
totalSteps: 3, totalSteps: 3,
},
render: Template,
}; };
export const WithCustomHeader = {
export const WithCustomHeader = Template.bind({}); args: {
WithCustomHeader.args = {
isOpen: true, isOpen: true,
headerContent: <div className="text-lg font-semibold">Custom header</div>, headerContent: <div className="text-lg font-semibold">Custom header</div>,
children: ( children: (
@@ -151,10 +155,11 @@ WithCustomHeader.args = {
showBackButton: false, showBackButton: false,
showNextButton: true, showNextButton: true,
nextButtonText: "Continue", nextButtonText: "Continue",
},
render: Template,
}; };
export const WithoutFooter = {
export const WithoutFooter = Template.bind({}); args: {
WithoutFooter.args = {
isOpen: true, isOpen: true,
title: "Simple Create Dialog", title: "Simple Create Dialog",
description: "This create dialog has no footer buttons", description: "This create dialog has no footer buttons",
@@ -167,13 +172,15 @@ WithoutFooter.args = {
), ),
showBackButton: false, showBackButton: false,
showNextButton: false, showNextButton: false,
},
render: Template,
}; };
export const LoginYellowBackdrop = {
export const LoginYellowBackdrop = Template.bind({}); args: {
LoginYellowBackdrop.args = {
isOpen: true, isOpen: true,
title: "Horizontalism", title: "Horizontalism",
description: "Edit or add to this description to describe what this value means to your community.", description:
"Edit or add to this description to describe what this value means to your community.",
backdropVariant: "blurredYellow", backdropVariant: "blurredYellow",
children: ( children: (
<div className="space-y-4"> <div className="space-y-4">
@@ -186,10 +193,11 @@ LoginYellowBackdrop.args = {
showNextButton: true, showNextButton: true,
nextButtonText: "Add Value", nextButtonText: "Add Value",
nextButtonDisabled: false, nextButtonDisabled: false,
},
render: Template,
}; };
export const NextButtonDisabled = {
export const NextButtonDisabled = Template.bind({}); args: {
NextButtonDisabled.args = {
isOpen: true, isOpen: true,
title: "What do you call your group's new policy?", title: "What do you call your group's new policy?",
description: "You can also combine or add new approaches to the list", description: "You can also combine or add new approaches to the list",
@@ -208,4 +216,6 @@ NextButtonDisabled.args = {
nextButtonDisabled: true, nextButtonDisabled: true,
currentStep: 1, currentStep: 1,
totalSteps: 3, totalSteps: 3,
},
render: Template,
}; };
+15 -12
View File
@@ -1,6 +1,9 @@
import React, { Suspense, useEffect } from "react"; import React, { Suspense, useEffect } from "react";
import Login from "../../app/components/modals/Login"; import Login from "../../app/components/modals/Login";
import LoginForm from "../../app/components/modals/Login/LoginForm"; import LoginForm from "../../app/components/modals/Login/LoginForm";
import messages from "../../messages/en/index";
const backToHome = messages.pages.login.backToHome;
/** /**
* Storybook runs outside Next.js request context; successful "Send link" needs fetch mocked * Storybook runs outside Next.js request context; successful "Send link" needs fetch mocked
@@ -43,11 +46,11 @@ function FakeMarketingPageBehindOverlay({
return ( return (
<div className="relative min-h-[100dvh] overflow-hidden"> <div className="relative min-h-[100dvh] overflow-hidden">
<div <div
className="absolute inset-0 bg-gradient-to-br from-amber-100 via-white to-amber-50" className="absolute inset-0 bg-[var(--color-surface-inverse-brand-primary)]"
aria-hidden aria-hidden
/> />
<div className="relative z-0 px-8 py-16"> <div className="relative z-0 px-8 py-16">
<p className="font-inter max-w-md text-lg text-neutral-800"> <p className="max-w-md text-large-paragraph text-[var(--color-content-invert-primary)]">
Placeholder page content the login overlay portals above this and Placeholder page content the login overlay portals above this and
uses backdrop blur (`blurredYellow`). uses backdrop blur (`blurredYellow`).
</p> </p>
@@ -107,13 +110,13 @@ export const HeaderOverlayBlurred = {
belowCard={ belowCard={
<a <a
href="/" href="/"
className="font-inter font-normal text-[14px] leading-[20px] text-[var(--color-content-invert-tertiary,#2d2d2d)] text-center hover:opacity-90" className="text-center text-small-paragraph text-[var(--color-content-invert-tertiary)] hover:opacity-90"
> >
Back to home {backToHome}
</a> </a>
} }
> >
<Suspense fallback={<p className="font-inter p-6">Loading</p>}> <Suspense fallback={<p className="p-6 text-small-paragraph">Loading</p>}>
<LoginForm /> <LoginForm />
</Suspense> </Suspense>
</Login> </Login>
@@ -152,13 +155,13 @@ export const FullPageRouteSolid = {
belowCard={ belowCard={
<a <a
href="/" href="/"
className="font-inter font-normal text-[14px] leading-[20px] text-[var(--color-content-invert-tertiary,#2d2d2d)] text-center hover:opacity-90" className="text-center text-small-paragraph text-[var(--color-content-invert-tertiary)] hover:opacity-90"
> >
Back to home {backToHome}
</a> </a>
} }
> >
<Suspense fallback={<p className="font-inter p-6">Loading</p>}> <Suspense fallback={<p className="p-6 text-small-paragraph">Loading</p>}>
<LoginForm /> <LoginForm />
</Suspense> </Suspense>
</Login> </Login>
@@ -178,15 +181,15 @@ export const ModalChromeOnly = {
belowCard={ belowCard={
<a <a
href="/" href="/"
className="font-inter font-normal text-[14px] leading-[20px] text-[var(--color-content-invert-tertiary,#2d2d2d)] text-center hover:opacity-90" className="text-center text-small-paragraph text-[var(--color-content-invert-tertiary)] hover:opacity-90"
> >
Back to home {backToHome}
</a> </a>
} }
> >
<p <p
id="login-modal-heading" id="login-modal-heading"
className="font-inter px-2 py-4 text-[var(--color-content-default-primary)]" className="px-2 py-4 text-[var(--color-content-default-primary)]"
> >
Placeholder body use &quot;Header overlay&quot; or &quot;Full-page Placeholder body use &quot;Header overlay&quot; or &quot;Full-page
route&quot; for the real flow. route&quot; for the real flow.
@@ -215,7 +218,7 @@ export const FormOnly = {
], ],
render: () => ( render: () => (
<div className="mx-auto max-w-[560px] rounded-[20px] bg-[var(--color-surface-default-primary)] p-6 shadow-lg"> <div className="mx-auto max-w-[560px] rounded-[20px] bg-[var(--color-surface-default-primary)] p-6 shadow-lg">
<Suspense fallback={<p className="font-inter">Loading</p>}> <Suspense fallback={<p className="text-small-paragraph">Loading</p>}>
<LoginForm /> <LoginForm />
</Suspense> </Suspense>
</div> </div>
+11 -7
View File
@@ -1,22 +1,28 @@
import React, { useState } from "react"; import React, { useState } from "react";
import Share from "../../app/components/modals/Share"; import Share from "../../app/components/modals/Share";
import { MessagesProvider } from "../../app/contexts/MessagesContext";
import messages from "../../messages/en/index";
/** Figma: Modal / Share — node 22073-30884 (Community Rule System). */ /** Figma: Modal / Share — node 22073-30884 (Community Rule System). */
export default { export default {
title: "modals/Share", title: "Components/Modals/Share",
component: Share, component: Share,
parameters: {
layout: "fullscreen",
docs: {
description: {
component:
"Share modal for published rules (copy link and channel actions).",
},
},
},
}; };
function ShareStoryHost() { function ShareStoryHost() {
const [open, setOpen] = useState(true); const [open, setOpen] = useState(true);
return ( return (
<MessagesProvider messages={messages}>
<div className="min-h-[100dvh] bg-[var(--color-surface-inverse-brand-primary)] p-6"> <div className="min-h-[100dvh] bg-[var(--color-surface-inverse-brand-primary)] p-6">
<button <button
type="button" type="button"
className="rounded-md bg-white px-4 py-2 text-sm text-black" className="rounded-md bg-[var(--color-surface-default-primary)] px-4 py-2 text-small-paragraph text-[var(--color-content-default-primary)]"
onClick={() => setOpen(true)} onClick={() => setOpen(true)}
> >
Open Share Open Share
@@ -31,11 +37,9 @@ function ShareStoryHost() {
onDiscordShare={() => {}} onDiscordShare={() => {}}
/> />
</div> </div>
</MessagesProvider>
); );
} }
export const Default = { export const Default = {
name: "Modal / Share",
render: () => <ShareStoryHost />, render: () => <ShareStoryHost />,
}; };
+22 -16
View File
@@ -1,6 +1,7 @@
import React from "react"; import React from "react";
import Tooltip from "../../app/components/modals/Tooltip"; import Tooltip from "../../app/components/modals/Tooltip";
import Button from "../../app/components/buttons/Button"; import Button from "../../app/components/buttons/Button";
import { TOOLTIP_POSITION_OPTIONS } from "../../lib/propNormalization";
export default { export default {
title: "Components/Modals/Tooltip", title: "Components/Modals/Tooltip",
@@ -8,7 +9,7 @@ export default {
argTypes: { argTypes: {
position: { position: {
control: { type: "select" }, control: { type: "select" },
options: ["top", "bottom"], options: [...TOOLTIP_POSITION_OPTIONS],
}, },
disabled: { disabled: {
control: { type: "boolean" }, control: { type: "boolean" },
@@ -29,37 +30,42 @@ const Template = (args) => (
</div> </div>
); );
export const Default = Template.bind({}); export const Default = {
Default.args = { args: {
text: "Tooltip text goes here", text: "Tooltip text goes here",
position: "top", position: "top",
disabled: false, disabled: false,
},
render: Template,
}; };
export const Top = {
export const Top = Template.bind({}); args: {
Top.args = {
text: "Tooltip positioned at top", text: "Tooltip positioned at top",
position: "top", position: "top",
},
render: Template,
}; };
export const Bottom = {
export const Bottom = Template.bind({}); args: {
Bottom.args = {
text: "Tooltip positioned at bottom", text: "Tooltip positioned at bottom",
position: "bottom", position: "bottom",
},
render: Template,
}; };
export const Disabled = {
export const Disabled = Template.bind({}); args: {
Disabled.args = {
text: "This tooltip is disabled", text: "This tooltip is disabled",
disabled: true, disabled: true,
},
render: Template,
}; };
export const LongText = {
export const LongText = Template.bind({}); args: {
LongText.args = {
text: "This is a longer tooltip text that demonstrates how the component handles multiple words and extended content", text: "This is a longer tooltip text that demonstrates how the component handles multiple words and extended content",
position: "top", position: "top",
},
render: Template,
}; };
export const WithIcon = () => ( export const WithIcon = () => (
<div className="p-16 flex items-center justify-center min-h-[200px]"> <div className="p-16 flex items-center justify-center min-h-[200px]">
<Tooltip text="Tooltip with icon button" position="top"> <Tooltip text="Tooltip with icon button" position="top">
+6 -2
View File
@@ -1,4 +1,8 @@
import MenuItem from "../../app/components/navigation/MenuItem"; import MenuItem from "../../app/components/navigation/MenuItem";
import {
MENU_ITEM_MODE_OPTIONS,
MENU_ITEM_SIZE_OPTIONS,
} from "../../lib/propNormalization";
export default { export default {
title: "Components/Navigation/MenuItem", title: "Components/Navigation/MenuItem",
@@ -15,12 +19,12 @@ export default {
argTypes: { argTypes: {
mode: { mode: {
control: { type: "select" }, control: { type: "select" },
options: ["default", "inverse"], options: [...MENU_ITEM_MODE_OPTIONS],
description: "The visual style mode of the menu item", description: "The visual style mode of the menu item",
}, },
size: { size: {
control: { type: "select" }, control: { type: "select" },
options: ["X Small", "Small", "Medium", "Large", "X Large"], options: [...MENU_ITEM_SIZE_OPTIONS],
description: "The size of the menu item", description: "The size of the menu item",
}, },
disabled: { disabled: {
@@ -0,0 +1,9 @@
import { CommunityStructureSelectScreen } from "../../app/(app)/create/screens/select/CommunityStructureSelectScreen";
export default {
title: "Pages/Create Flow/Community structure",
component: CommunityStructureSelectScreen,
parameters: { layout: "fullscreen" },
};
export const Default = {};
@@ -0,0 +1,17 @@
import { ConflictManagementScreen } from "../../app/(app)/create/screens/card/ConflictManagementScreen";
export default {
title: "Pages/Create Flow/Conflict management",
component: ConflictManagementScreen,
parameters: {
layout: "fullscreen",
docs: {
description: {
component:
"Conflict management step: card stack, customize modals, responsive layout.",
},
},
},
};
export const Default = {};
+9
View File
@@ -0,0 +1,9 @@
import { CoreValuesSelectScreen } from "../../app/(app)/create/screens/select/CoreValuesSelectScreen";
export default {
title: "Pages/Create Flow/Core values",
component: CoreValuesSelectScreen,
parameters: { layout: "fullscreen" },
};
export const Default = {};
+1 -1
View File
@@ -1,7 +1,7 @@
import { InformationalScreen } from "../../app/(app)/create/screens/informational/InformationalScreen"; import { InformationalScreen } from "../../app/(app)/create/screens/informational/InformationalScreen";
export default { export default {
title: "Pages/Create/Informational", title: "Pages/Create Flow/Informational",
component: InformationalScreen, component: InformationalScreen,
parameters: { layout: "fullscreen" }, parameters: { layout: "fullscreen" },
}; };
@@ -0,0 +1,17 @@
import { MembershipMethodsScreen } from "../../app/(app)/create/screens/card/MembershipMethodsScreen";
export default {
title: "Pages/Create Flow/Membership methods",
component: MembershipMethodsScreen,
parameters: {
layout: "fullscreen",
docs: {
description: {
component:
"Membership methods step: card stack, customize modals, responsive layout.",
},
},
},
};
export const Default = {};
@@ -0,0 +1,67 @@
import { useEffect, useRef } from "react";
import { PublishedStakeholdersManagePanel } from "../../app/(app)/create/screens/select/PublishedStakeholdersManagePanel";
function StakeholdersFetchMock({ children }) {
const origRef = useRef(undefined);
if (origRef.current === undefined) {
origRef.current = globalThis.fetch;
globalThis.fetch = async (input, init) => {
const url =
typeof input === "string"
? input
: input instanceof URL
? input.href
: input.url;
if (url.includes("/stakeholders")) {
if (init?.method && init.method !== "GET") {
return new Response(JSON.stringify({ ok: true }), {
status: 200,
headers: { "Content-Type": "application/json" },
});
}
return new Response(JSON.stringify({ stakeholders: [] }), {
status: 200,
headers: { "Content-Type": "application/json" },
});
}
return origRef.current(input, init);
};
}
useEffect(() => {
return () => {
if (origRef.current) {
globalThis.fetch = origRef.current;
}
};
}, []);
return children;
}
export default {
title: "Pages/Create Flow/Published stakeholders",
component: PublishedStakeholdersManagePanel,
parameters: {
layout: "fullscreen",
docs: {
description: {
component:
"Manage invites on a published rule. Fetch is stubbed empty in this story.",
},
},
},
decorators: [
(Story) => (
<StakeholdersFetchMock>
<div className="min-h-screen bg-[var(--color-surface-default-primary)] p-8">
<Story />
</div>
</StakeholdersFetchMock>
),
],
};
export const Default = {
args: {
ruleId: "storybook-rule",
},
};
+1 -1
View File
@@ -1,7 +1,7 @@
import { CommunityReviewScreen } from "../../app/(app)/create/screens/review/CommunityReviewScreen"; import { CommunityReviewScreen } from "../../app/(app)/create/screens/review/CommunityReviewScreen";
export default { export default {
title: "Pages/Create/Review", title: "Pages/Create Flow/Review",
component: CommunityReviewScreen, component: CommunityReviewScreen,
parameters: { layout: "fullscreen" }, parameters: { layout: "fullscreen" },
}; };
+1 -1
View File
@@ -1,7 +1,7 @@
import { CommunitySizeSelectScreen } from "../../app/(app)/create/screens/select/CommunitySizeSelectScreen"; import { CommunitySizeSelectScreen } from "../../app/(app)/create/screens/select/CommunitySizeSelectScreen";
export default { export default {
title: "Pages/Create/CommunitySize", title: "Pages/Create Flow/Community size",
component: CommunitySizeSelectScreen, component: CommunitySizeSelectScreen,
parameters: { layout: "fullscreen" }, parameters: { layout: "fullscreen" },
}; };
+1 -1
View File
@@ -1,7 +1,7 @@
import { CreateFlowTextFieldScreen } from "../../app/(app)/create/screens/text/CreateFlowTextFieldScreen"; import { CreateFlowTextFieldScreen } from "../../app/(app)/create/screens/text/CreateFlowTextFieldScreen";
export default { export default {
title: "Pages/Create/CommunityName", title: "Pages/Create Flow/Community name",
component: CreateFlowTextFieldScreen, component: CreateFlowTextFieldScreen,
parameters: { layout: "fullscreen" }, parameters: { layout: "fullscreen" },
}; };
+1 -1
View File
@@ -1,7 +1,7 @@
import { CommunityUploadScreen } from "../../app/(app)/create/screens/upload/CommunityUploadScreen"; import { CommunityUploadScreen } from "../../app/(app)/create/screens/upload/CommunityUploadScreen";
export default { export default {
title: "Pages/Create/CommunityUpload", title: "Pages/Create Flow/Community upload",
component: CommunityUploadScreen, component: CommunityUploadScreen,
parameters: { layout: "fullscreen" }, parameters: { layout: "fullscreen" },
}; };
+38
View File
@@ -0,0 +1,38 @@
import FaqAccordion from "../../app/components/sections/Accordion";
import about from "../../messages/en/pages/about.json";
export default {
title: "Components/Sections/Accordion",
component: FaqAccordion,
parameters: {
layout: "fullscreen",
docs: {
description: {
component:
"About-page FAQ wrapper over layout/Accordion. Sizes: s below lg, m at lg, l at xl.",
},
},
},
argTypes: {
title: { control: { type: "text" } },
size: {
control: { type: "select" },
options: ["s", "m", "l"],
},
lgSize: {
control: { type: "select" },
options: ["s", "m", "l"],
},
xlSize: {
control: { type: "select" },
options: ["s", "m", "l"],
},
},
};
export const Default = {
args: {
title: about.faq.title,
items: about.faq.items,
},
};
@@ -1,248 +0,0 @@
import HeroBanner from "../../app/components/sections/HeroBanner";
import ContentLockup from "../../app/components/type/ContentLockup";
import HeroDecor from "../../app/components/sections/HeroBanner/HeroDecor";
export default {
title: "Systems/HeroBanner System",
parameters: {
layout: "fullscreen",
docs: {
description: {
component:
"Complete HeroBanner system showcasing all nested components working together. This demonstrates the full responsive behavior and component integration.",
},
},
},
tags: ["autodocs"],
};
export const CompleteSystem = {
render: () => (
<div className="min-h-screen bg-gray-50">
<HeroBanner
title="Collaborate"
subtitle="with clarity"
description="Help your community make important decisions in a way that reflects its unique values."
ctaText="Learn how Community Rule works"
ctaHref="#"
/>
</div>
),
parameters: {
docs: {
description: {
story:
"Complete HeroBanner system with all components integrated. Resize your browser to see responsive behavior across all breakpoints.",
},
},
},
};
export const ComponentBreakdown = {
render: () => (
<div className="space-y-12 p-8">
<div>
<h2 className="text-2xl font-bold mb-6">HeroBanner Components</h2>
<div className="space-y-8">
<div>
<h3 className="text-lg font-semibold mb-4">
1. ContentLockup Component
</h3>
<div className="bg-[var(--color-surface-default-brand-primary)] p-8 rounded-lg">
<ContentLockup
title="Collaborate"
subtitle="with clarity"
description="Help your community make important decisions in a way that reflects its unique values."
ctaText="Learn how Community Rule works"
ctaHref="#"
/>
</div>
</div>
<div>
<h3 className="text-lg font-semibold mb-4">
2. HeroDecor Component
</h3>
<div className="bg-[var(--color-surface-default-brand-primary)] p-8 rounded-lg relative overflow-hidden h-64">
<HeroDecor className="w-full h-full" />
<div className="relative z-10 text-white mt-4">
<p>Decoration appears behind content</p>
</div>
</div>
</div>
<div>
<h3 className="text-lg font-semibold mb-4">
3. Complete HeroBanner
</h3>
<HeroBanner
title="Collaborate"
subtitle="with clarity"
description="Help your community make important decisions in a way that reflects its unique values."
ctaText="Learn how Community Rule works"
ctaHref="#"
/>
</div>
</div>
</div>
</div>
),
parameters: {
docs: {
description: {
story:
"Breakdown of individual components that make up the HeroBanner system, showing how they work together.",
},
},
},
};
export const ResponsiveBreakpoints = {
render: () => (
<div className="space-y-8 p-8">
<h2 className="text-2xl font-bold">Responsive Breakpoints</h2>
<div className="space-y-6">
<div>
<h3 className="text-lg font-semibold mb-2">XSmall (429px)</h3>
<div
className="border-2 border-gray-300 rounded-lg overflow-hidden"
style={{ width: "400px" }}
>
<HeroBanner
title="Collaborate"
subtitle="with clarity"
description="Help your community make important decisions in a way that reflects its unique values."
ctaText="Learn how Community Rule works"
ctaHref="#"
/>
</div>
</div>
<div>
<h3 className="text-lg font-semibold mb-2">Small (430px+)</h3>
<div
className="border-2 border-gray-300 rounded-lg overflow-hidden"
style={{ width: "600px" }}
>
<HeroBanner
title="Collaborate"
subtitle="with clarity"
description="Help your community make important decisions in a way that reflects its unique values."
ctaText="Learn how Community Rule works"
ctaHref="#"
/>
</div>
</div>
<div>
<h3 className="text-lg font-semibold mb-2">Medium (768px+)</h3>
<div
className="border-2 border-gray-300 rounded-lg overflow-hidden"
style={{ width: "900px" }}
>
<HeroBanner
title="Collaborate"
subtitle="with clarity"
description="Help your community make important decisions in a way that reflects its unique values."
ctaText="Learn how Community Rule works"
ctaHref="#"
/>
</div>
</div>
<div>
<h3 className="text-lg font-semibold mb-2">Large (1024px+)</h3>
<div
className="border-2 border-gray-300 rounded-lg overflow-hidden"
style={{ width: "1200px" }}
>
<HeroBanner
title="Collaborate"
subtitle="with clarity"
description="Help your community make important decisions in a way that reflects its unique values."
ctaText="Learn how Community Rule works"
ctaHref="#"
/>
</div>
</div>
<div>
<h3 className="text-lg font-semibold mb-2">XLarge (1440px+)</h3>
<div
className="border-2 border-gray-300 rounded-lg overflow-hidden"
style={{ width: "1600px" }}
>
<HeroBanner
title="Collaborate"
subtitle="with clarity"
description="Help your community make important decisions in a way that reflects its unique values."
ctaText="Learn how Community Rule works"
ctaHref="#"
/>
</div>
</div>
</div>
</div>
),
parameters: {
docs: {
description: {
story:
"HeroBanner system demonstrating responsive behavior at each breakpoint. Each container simulates a different screen size.",
},
},
},
};
export const ContentVariations = {
render: () => (
<div className="space-y-8 p-8">
<h2 className="text-2xl font-bold">Content Variations</h2>
<div className="space-y-6">
<div>
<h3 className="text-lg font-semibold mb-2">Standard Content</h3>
<HeroBanner
title="Collaborate"
subtitle="with clarity"
description="Help your community make important decisions in a way that reflects its unique values."
ctaText="Learn how Community Rule works"
ctaHref="#"
/>
</div>
<div>
<h3 className="text-lg font-semibold mb-2">Alternative Content</h3>
<HeroBanner
title="Build"
subtitle="better communities"
description="Create operating manuals that help your community thrive and make decisions together."
ctaText="Get started today"
ctaHref="/signup"
/>
</div>
<div>
<h3 className="text-lg font-semibold mb-2">Long Description</h3>
<HeroBanner
title="Collaborate"
subtitle="with clarity"
description="Help your community make important decisions in a way that reflects its unique values. Our platform provides the tools and frameworks needed to build successful, sustainable communities that can navigate complex challenges together."
ctaText="Learn how Community Rule works"
ctaHref="#"
/>
</div>
</div>
</div>
),
parameters: {
docs: {
description: {
story:
"HeroBanner system with different content variations to demonstrate flexibility and content handling.",
},
},
},
};
+2 -1
View File
@@ -1,4 +1,5 @@
import QuoteBlock from "../../app/components/sections/QuoteBlock"; import QuoteBlock from "../../app/components/sections/QuoteBlock";
import { QUOTE_BLOCK_VARIANT_OPTIONS } from "../../lib/propNormalization";
export default { export default {
title: "Components/Sections/QuoteBlock", title: "Components/Sections/QuoteBlock",
@@ -34,7 +35,7 @@ A responsive quote section component that displays inspirational governance quot
argTypes: { argTypes: {
variant: { variant: {
control: { type: "select" }, control: { type: "select" },
options: ["compact", "standard", "extended", "statement"], options: [...QUOTE_BLOCK_VARIANT_OPTIONS],
description: "Layout variant for different use cases", description: "Layout variant for different use cases",
}, },
quote: { quote: {
+6 -6
View File
@@ -1,23 +1,23 @@
import HeaderLockup from "../../app/components/type/HeaderLockup"; import HeaderLockup from "../../app/components/type/HeaderLockup";
import {
HEADER_LOCKUP_JUSTIFICATION_OPTIONS,
HEADER_LOCKUP_SIZE_OPTIONS,
} from "../../lib/propNormalization";
export default { export default {
title: "Components/Type/HeaderLockup", title: "Components/Type/HeaderLockup",
component: HeaderLockup, component: HeaderLockup,
parameters: { parameters: {
layout: "centered", layout: "centered",
backgrounds: {
default: "dark",
values: [{ name: "dark", value: "#000000" }],
},
}, },
argTypes: { argTypes: {
justification: { justification: {
control: { type: "select" }, control: { type: "select" },
options: ["left", "center", "Left", "Center"], options: [...HEADER_LOCKUP_JUSTIFICATION_OPTIONS],
}, },
size: { size: {
control: { type: "select" }, control: { type: "select" },
options: ["L", "M", "l", "m"], options: [...HEADER_LOCKUP_SIZE_OPTIONS],
}, },
}, },
}; };
+77
View File
@@ -0,0 +1,77 @@
import InputLabel from "../../app/components/type/InputLabel";
import {
INPUT_LABEL_PALETTE_OPTIONS,
INPUT_LABEL_SIZE_OPTIONS,
} from "../../lib/propNormalization";
export default {
title: "Components/Type/InputLabel",
component: InputLabel,
parameters: {
layout: "centered",
docs: {
description: {
component:
"Reusable form-input label with optional asterisk, help icon, and helper text. Figma Utility / InputLabel; canonical code under type/.",
},
},
},
argTypes: {
label: { control: { type: "text" } },
helpIcon: { control: { type: "boolean" } },
asterisk: { control: { type: "boolean" } },
helperText: { control: { type: "boolean" } },
size: {
control: { type: "select" },
options: [...INPUT_LABEL_SIZE_OPTIONS],
},
palette: {
control: { type: "select" },
options: [...INPUT_LABEL_PALETTE_OPTIONS],
},
},
};
export const Default = {
args: {
label: "Community name",
size: "s",
palette: "default",
},
};
export const Required = {
args: {
label: "Community name",
asterisk: true,
size: "s",
palette: "default",
},
};
export const WithHelp = {
args: {
label: "Applicable scope",
helpIcon: true,
size: "m",
palette: "default",
},
};
export const OptionalHelper = {
args: {
label: "Website",
helperText: true,
size: "s",
palette: "default",
},
};
export const Inverse = {
args: {
label: "Community name",
asterisk: true,
size: "m",
palette: "inverse",
},
};
+2 -5
View File
@@ -1,19 +1,16 @@
import NumberedList from "../../app/components/type/NumberedList"; import NumberedList from "../../app/components/type/NumberedList";
import { NUMBERED_LIST_SIZE_OPTIONS } from "../../lib/propNormalization";
export default { export default {
title: "Components/Type/NumberedList", title: "Components/Type/NumberedList",
component: NumberedList, component: NumberedList,
parameters: { parameters: {
layout: "centered", layout: "centered",
backgrounds: {
default: "dark",
values: [{ name: "dark", value: "#000000" }],
},
}, },
argTypes: { argTypes: {
size: { size: {
control: { type: "select" }, control: { type: "select" },
options: ["M", "S", "m", "s"], options: [...NUMBERED_LIST_SIZE_OPTIONS],
}, },
}, },
}; };