Catch up Storybook preview, coverage, and story hygiene #67

Merged
an.di merged 1 commits from adilallo/maintenance/StorybookCatchup into main 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
- `<ComponentName>.stories.js` — matches 69/70 existing files.
- Use `.tsx` only when the story genuinely needs types (rare; prefer JS to
match the codebase convention).
- `<ComponentName>.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/<area>/MyComponent";
import { CHIP_PALETTE_OPTIONS } from "../../lib/propNormalization";
export default {
title: "Components/<SubFolder>/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/<SubFolder>/<Name>` (e.g.
`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>`.
## `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";
+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 {
--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;
}
+1 -23
View File
@@ -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;
},
};
+52
View File
@@ -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) => (
<MessagesProvider messages={messages}>
<AuthModalProvider>
<CreateFlowProvider>
<div className="font-inter">
<Story />
</div>
</CreateFlowProvider>
</AuthModalProvider>
</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",
"eslint-config-next",
"typescript-eslint",
"@storybook/nextjs-vite",
"@eslint/js",
"@next/eslint-plugin-next",
"eslint-plugin-react",
+82 -1
View File
@@ -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",
+1 -2
View File
@@ -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",
+8 -3
View File
@@ -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: {
+1 -1
View File
@@ -33,7 +33,7 @@ export const Default = {
export const InParagraph = {
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{" "}
<InlineTextButton onClick={() => {}}>expand this later</InlineTextButton>{" "}
if you need more room.
+4 -4
View File
@@ -37,10 +37,10 @@ export const Default = {
<Icon
name="number"
size={32}
className="text-[var(--color-content-default-brand-primary,#fefcc9)]"
className="text-[var(--color-content-default-brand-primary)]"
/>
</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
</span>
</Vertical>
@@ -54,10 +54,10 @@ export const Disabled = {
<Icon
name="number"
size={32}
className="text-[var(--color-content-default-brand-primary,#fefcc9)]"
className="text-[var(--color-content-default-brand-primary)]"
/>
</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
</span>
</Vertical>
@@ -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) => (
<MessagesProvider messages={messages}>
<div className="w-[min(100%,546px)] bg-[var(--color-surface-default-primary)] p-6">
<Story />
</div>
</MessagesProvider>
),
],
};
+8 -3
View File
@@ -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: {
+22 -19
View File
@@ -44,43 +44,46 @@ const Template = (args) => {
};
// Default story
export const Default = Template.bind({});
Default.args = {
export const Default = {
args: {
label: "Default Select Input",
placeholder: "Choose an option",
state: "default",
};
// States
export const Active = Template.bind({});
Active.args = {
},
render: Template,
}; // States
export const Active = {
args: {
label: "Active State",
placeholder: "Choose an option",
state: "active",
},
render: Template,
};
export const Focus = Template.bind({});
Focus.args = {
export const Focus = {
args: {
label: "Focus State",
placeholder: "Choose an option",
state: "focus",
},
render: Template,
};
export const Error = Template.bind({});
Error.args = {
export const Error = {
args: {
label: "Error State",
placeholder: "Choose an option",
error: true,
},
render: Template,
};
export const Disabled = Template.bind({});
Disabled.args = {
export const Disabled = {
args: {
label: "Disabled State",
placeholder: "Choose an option",
disabled: true,
};
// Interactive example
},
render: Template,
}; // Interactive example
export const Interactive = (args) => {
const [value, setValue] = useState("");
+16 -12
View File
@@ -39,32 +39,36 @@ export default {
const Template = (args) => <Switch {...args} />;
export const Default = Template.bind({});
Default.args = {
export const Default = {
args: {
propSwitch: false,
text: "Switch label",
},
render: Template,
};
export const Checked = Template.bind({});
Checked.args = {
export const Checked = {
args: {
propSwitch: true,
text: "Switch label",
},
render: Template,
};
export const Focus = Template.bind({});
Focus.args = {
export const Focus = {
args: {
propSwitch: false,
state: "focus",
text: "Switch label",
},
render: Template,
};
export const FocusChecked = Template.bind({});
FocusChecked.args = {
export const FocusChecked = {
args: {
propSwitch: true,
state: "focus",
text: "Switch label",
},
render: Template,
};
export const States = () => (
<div className="space-y-4">
<div className="space-y-2">
+41 -24
View File
@@ -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,47 +41,52 @@ export default {
const Template = (args) => <TextArea {...args} />;
export const Default = Template.bind({});
Default.args = {
export const Default = {
args: {
label: "Text Area",
placeholder: "Enter text...",
value: "",
},
render: Template,
};
export const WithValue = Template.bind({});
WithValue.args = {
export const WithValue = {
args: {
label: "Text Area",
placeholder: "Enter text...",
value:
"This is some sample text content that demonstrates how the text area looks with content.",
},
render: Template,
};
export const Small = Template.bind({});
Small.args = {
export const Small = {
args: {
size: "small",
label: "Small Text Area",
placeholder: "Enter text...",
value: "",
},
render: Template,
};
export const Medium = Template.bind({});
Medium.args = {
export const Medium = {
args: {
size: "medium",
label: "Medium Text Area",
placeholder: "Enter text...",
value: "",
},
render: Template,
};
export const Large = Template.bind({});
Large.args = {
export const Large = {
args: {
size: "large",
label: "Large Text Area",
placeholder: "Enter text...",
value: "",
},
render: Template,
};
export const Embedded = Template.bind({});
Embedded.args = {
export const Embedded = {
args: {
label: "Section content",
placeholder: "Enter text...",
value:
@@ -79,16 +94,18 @@ Embedded.args = {
appearance: "embedded",
size: "large",
rows: 4,
},
render: Template,
};
export const HorizontalLabel = Template.bind({});
HorizontalLabel.args = {
export const HorizontalLabel = {
args: {
labelVariant: "horizontal",
label: "Horizontal Label",
placeholder: "Enter text...",
value: "",
},
render: Template,
};
export const AllSizes = () => (
<div className="space-y-6">
<div className="space-y-4">
+41 -32
View File
@@ -1,5 +1,9 @@
import React from "react";
import TextInput from "../../app/components/controls/TextInput";
import {
INPUT_STATE_OPTIONS,
TEXT_INPUT_SIZE_OPTIONS,
} from "../../lib/propNormalization";
export default {
title: "Components/Controls/TextInput",
@@ -10,11 +14,11 @@ export default {
argTypes: {
inputSize: {
control: { type: "select" },
options: ["small", "medium", "Small", "Medium"],
options: [...TEXT_INPUT_SIZE_OPTIONS],
},
state: {
control: { type: "select" },
options: ["default", "active", "hover", "focus", "error", "disabled"],
options: [...INPUT_STATE_OPTIONS],
},
disabled: {
control: { type: "boolean" },
@@ -37,75 +41,80 @@ export default {
const Template = (args) => <TextInput {...args} />;
// Default story
export const Default = Template.bind({});
Default.args = {
export const Default = {
args: {
label: "Default Text Input",
placeholder: "Enter text...",
inputSize: "medium",
state: "default",
};
// Size variants
export const Small = Template.bind({});
Small.args = {
},
render: Template,
}; // Size variants
export const Small = {
args: {
label: "Small Text Input",
placeholder: "Small size",
inputSize: "small",
state: "default",
},
render: Template,
};
export const Medium = Template.bind({});
Medium.args = {
export const Medium = {
args: {
label: "Medium Text Input",
placeholder: "Medium size",
inputSize: "medium",
state: "default",
};
// States
export const Active = Template.bind({});
Active.args = {
},
render: Template,
}; // States
export const Active = {
args: {
label: "Active State",
placeholder: "Active input",
inputSize: "medium",
state: "active",
},
render: Template,
};
export const Hover = Template.bind({});
Hover.args = {
export const Hover = {
args: {
label: "Hover State",
placeholder: "Hover input",
inputSize: "medium",
state: "hover",
},
render: Template,
};
export const Focus = Template.bind({});
Focus.args = {
export const Focus = {
args: {
label: "Focus State",
placeholder: "Focused input",
inputSize: "medium",
state: "focus",
},
render: Template,
};
export const Error = Template.bind({});
Error.args = {
export const Error = {
args: {
label: "Error State",
placeholder: "Error input",
inputSize: "medium",
state: "default",
error: true,
},
render: Template,
};
export const Disabled = Template.bind({});
Disabled.args = {
export const Disabled = {
args: {
label: "Disabled State",
placeholder: "Disabled input",
inputSize: "medium",
state: "default",
disabled: true,
};
// Interactive example
},
render: Template,
}; // Interactive example
export const Interactive = (args) => {
const [value, setValue] = React.useState("");
+8 -6
View File
@@ -44,22 +44,24 @@ export const States = () => (
</div>
);
export const WithText = Template.bind({});
WithText.args = {
export const WithText = {
args: {
label: "Text Toggle",
checked: false,
showText: true,
text: "Toggle",
},
render: Template,
};
export const WithIcon = Template.bind({});
WithIcon.args = {
export const WithIcon = {
args: {
label: "Icon Toggle",
checked: false,
showIcon: true,
icon: "I",
},
render: Template,
};
export const Interactive = () => {
const [checked, setChecked] = React.useState(false);
const [state, setState] = React.useState("default");
+26 -17
View File
@@ -1,5 +1,9 @@
import React from "react";
import ToggleGroup from "../../app/components/controls/ToggleGroup";
import {
STATE_OPTIONS,
TOGGLE_GROUP_POSITION_OPTIONS,
} from "../../lib/propNormalization";
export default {
title: "Components/Controls/ToggleGroup",
@@ -10,11 +14,11 @@ export default {
argTypes: {
position: {
control: { type: "select" },
options: ["left", "middle", "right"],
options: [...TOGGLE_GROUP_POSITION_OPTIONS],
},
state: {
control: { type: "select" },
options: ["default", "hover", "focus", "selected"],
options: [...STATE_OPTIONS],
},
showText: {
control: { type: "boolean" },
@@ -24,27 +28,30 @@ export default {
const Template = (args) => <ToggleGroup {...args}>Toggle Item</ToggleGroup>;
export const Default = Template.bind({});
Default.args = {
export const Default = {
args: {
position: "left",
state: "default",
showText: true,
},
render: Template,
};
export const Middle = Template.bind({});
Middle.args = {
export const Middle = {
args: {
position: "middle",
state: "default",
showText: true,
},
render: Template,
};
export const Right = Template.bind({});
Right.args = {
export const Right = {
args: {
position: "right",
state: "default",
showText: true,
},
render: Template,
};
export const States = () => (
<div className="space-y-4">
<div className="space-y-2">
@@ -89,22 +96,24 @@ export const Positions = () => (
</div>
);
export const WithText = Template.bind({});
WithText.args = {
export const WithText = {
args: {
position: "left",
state: "default",
showText: true,
children: "Active Deals",
},
render: Template,
};
export const WithoutText = Template.bind({});
WithoutText.args = {
export const WithoutText = {
args: {
position: "left",
state: "default",
showText: false,
children: "☰",
},
render: Template,
};
export const WithIcons = () => (
<div className="space-y-4">
<div className="space-y-2">
+35 -25
View File
@@ -34,87 +34,97 @@ export default {
const Template = (args) => <Upload {...args} />;
// Default story
export const Default = Template.bind({});
Default.args = {
export const Default = {
args: {
label: "Upload",
active: true,
showHelpIcon: true,
};
// Active state
export const Active = Template.bind({});
Active.args = {
},
render: Template,
}; // Active state
export const Active = {
args: {
label: "Upload",
active: true,
showHelpIcon: true,
};
Active.parameters = {
},
parameters: {
docs: {
description: {
story:
"Upload component in active state with white button and black text.",
},
},
},
render: Template,
};
// Inactive state
export const Inactive = Template.bind({});
Inactive.args = {
export const Inactive = {
args: {
label: "Upload",
active: false,
showHelpIcon: true,
};
Inactive.parameters = {
},
parameters: {
docs: {
description: {
story:
"Upload component in inactive state with dark button and gray text.",
},
},
},
render: Template,
};
// Without help icon
export const WithoutHelpIcon = Template.bind({});
WithoutHelpIcon.args = {
export const WithoutHelpIcon = {
args: {
label: "Upload",
active: true,
showHelpIcon: false,
};
WithoutHelpIcon.parameters = {
},
parameters: {
docs: {
description: {
story: "Upload component without help icon.",
},
},
},
render: Template,
};
// Without label
export const WithoutLabel = Template.bind({});
WithoutLabel.args = {
export const WithoutLabel = {
args: {
active: true,
showHelpIcon: false,
};
WithoutLabel.parameters = {
},
parameters: {
docs: {
description: {
story: "Upload component without label.",
},
},
},
render: Template,
};
// Custom label
export const CustomLabel = Template.bind({});
CustomLabel.args = {
export const CustomLabel = {
args: {
label: "Upload Files",
active: true,
showHelpIcon: true,
};
CustomLabel.parameters = {
},
parameters: {
docs: {
description: {
story: "Upload component with custom label text.",
},
},
},
render: Template,
};
// All states comparison
@@ -1,16 +1,19 @@
import React from "react";
import HeaderLockup from "../../app/components/type/HeaderLockup";
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
* plus bordered checklist no standalone wrapper component.
*/
export default {
title: "Components/Controls/Rail header (lockup + info box)",
title: "Create Flow/Rail header (lockup + info box)",
parameters: {
layout: "centered",
backgrounds: { default: "dark" },
docs: {
description: {
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: [
(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 />
</div>
),
],
tags: ["autodocs"],
};
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({});
ToastDefault.args = {
export const ToastDefault = {
args: {
title: "Short alert toast message goes here",
description:
"Nascetur ipsum a nisi tempor cras nam neque volutpat. Aliquam id est faucibus nunc quis. Eleifend suspendisse.",
status: "default",
type: "toast",
},
render: Template,
};
export const ToastPositive = Template.bind({});
ToastPositive.args = {
export const ToastPositive = {
args: {
title: "Short alert toast message goes here",
description:
"Nascetur ipsum a nisi tempor cras nam neque volutpat. Aliquam id est faucibus nunc quis. Eleifend suspendisse.",
status: "positive",
type: "toast",
},
render: Template,
};
export const ToastWarning = Template.bind({});
ToastWarning.args = {
export const ToastWarning = {
args: {
title: "Short alert toast message goes here",
description:
"Nascetur ipsum a nisi tempor cras nam neque volutpat. Aliquam id est faucibus nunc quis. Eleifend suspendisse.",
status: "warning",
type: "toast",
},
render: Template,
};
export const ToastDanger = Template.bind({});
ToastDanger.args = {
export const ToastDanger = {
args: {
title: "Short alert toast message goes here",
description:
"Nascetur ipsum a nisi tempor cras nam neque volutpat. Aliquam id est faucibus nunc quis. Eleifend suspendisse.",
status: "danger",
type: "toast",
},
render: Template,
};
export const Banner = Template.bind({});
Banner.args = {
export const Banner = {
args: {
title: "Short alert banner message goes here",
description:
"Nascetur ipsum a nisi tempor cras nam neque volutpat. Aliquam id est faucibus nunc quis. Eleifend suspendisse.",
status: "default",
type: "banner",
},
render: Template,
};
export const BannerPositive = Template.bind({});
BannerPositive.args = {
export const BannerPositive = {
args: {
title: "Short alert banner message goes here",
description:
"Nascetur ipsum a nisi tempor cras nam neque volutpat. Aliquam id est faucibus nunc quis. Eleifend suspendisse.",
status: "positive",
type: "banner",
},
render: Template,
};
export const BannerWarning = Template.bind({});
BannerWarning.args = {
export const BannerWarning = {
args: {
title: "Short alert banner message goes here",
description:
"Nascetur ipsum a nisi tempor cras nam neque volutpat. Aliquam id est faucibus nunc quis. Eleifend suspendisse.",
status: "warning",
type: "banner",
},
render: Template,
};
export const BannerDanger = Template.bind({});
BannerDanger.args = {
export const BannerDanger = {
args: {
title: "Short alert banner message goes here",
description:
"Nascetur ipsum a nisi tempor cras nam neque volutpat. Aliquam id est faucibus nunc quis. Eleifend suspendisse.",
status: "danger",
type: "banner",
},
render: Template,
};
export const TitleOnly = Template.bind({});
TitleOnly.args = {
export const TitleOnly = {
args: {
title: "Short alert banner message goes here",
status: "default",
type: "toast",
},
render: Template,
};
export const ToastSmall = Template.bind({});
ToastSmall.args = {
export const ToastSmall = {
args: {
title: "Short alert toast message goes here",
description:
"Nascetur ipsum a nisi tempor cras nam neque volutpat. Aliquam id est faucibus nunc quis. Eleifend suspendisse.",
status: "default",
type: "toast",
size: "s",
},
render: Template,
};
export const BannerSmall = Template.bind({});
BannerSmall.args = {
export const BannerSmall = {
args: {
title: "Short alert banner message goes here",
description:
"Nascetur ipsum a nisi tempor cras nam neque volutpat. Aliquam id est faucibus nunc quis. Eleifend suspendisse.",
status: "positive",
type: "banner",
size: "s",
},
render: Template,
};
const NoDismissTemplate = (args) => (
<div className="p-8 max-w-[600px]">
<Alert {...args} />
</div>
);
export const BannerNoDismiss = NoDismissTemplate.bind({});
BannerNoDismiss.args = {
export const BannerNoDismiss = {
args: {
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",
type: "banner",
size: "s",
},
render: NoDismissTemplate,
};
export const AllStatuses = () => {
const [visible, setVisible] = useState({
default: true,
+34 -24
View File
@@ -54,8 +54,8 @@ const Template = (args) => {
);
};
export const Default = Template.bind({});
Default.args = {
export const Default = {
args: {
isOpen: true,
title: "What do you call your group's new policy?",
description: "You can also combine or add new approaches to the list",
@@ -72,10 +72,11 @@ Default.args = {
backButtonText: "Back",
nextButtonText: "Next",
nextButtonDisabled: false,
},
render: Template,
};
export const WithStepper = Template.bind({});
WithStepper.args = {
export const WithStepper = {
args: {
isOpen: true,
title: "What do you call your group's new policy?",
description: "You can also combine or add new approaches to the list",
@@ -94,10 +95,11 @@ WithStepper.args = {
nextButtonDisabled: false,
currentStep: 1,
totalSteps: 3,
},
render: Template,
};
export const Step2 = Template.bind({});
Step2.args = {
export const Step2 = {
args: {
isOpen: true,
title: "How should conflicts be resolved?",
description: "You can also combine or add new approaches to the list",
@@ -113,10 +115,11 @@ Step2.args = {
nextButtonDisabled: false,
currentStep: 2,
totalSteps: 3,
},
render: Template,
};
export const Step3 = Template.bind({});
Step3.args = {
export const Step3 = {
args: {
isOpen: true,
title: "Final step",
description: "Review your settings",
@@ -134,10 +137,11 @@ Step3.args = {
nextButtonDisabled: false,
currentStep: 3,
totalSteps: 3,
},
render: Template,
};
export const WithCustomHeader = Template.bind({});
WithCustomHeader.args = {
export const WithCustomHeader = {
args: {
isOpen: true,
headerContent: <div className="text-lg font-semibold">Custom header</div>,
children: (
@@ -151,10 +155,11 @@ WithCustomHeader.args = {
showBackButton: false,
showNextButton: true,
nextButtonText: "Continue",
},
render: Template,
};
export const WithoutFooter = Template.bind({});
WithoutFooter.args = {
export const WithoutFooter = {
args: {
isOpen: true,
title: "Simple Create Dialog",
description: "This create dialog has no footer buttons",
@@ -167,13 +172,15 @@ WithoutFooter.args = {
),
showBackButton: false,
showNextButton: false,
},
render: Template,
};
export const LoginYellowBackdrop = Template.bind({});
LoginYellowBackdrop.args = {
export const LoginYellowBackdrop = {
args: {
isOpen: true,
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",
children: (
<div className="space-y-4">
@@ -186,10 +193,11 @@ LoginYellowBackdrop.args = {
showNextButton: true,
nextButtonText: "Add Value",
nextButtonDisabled: false,
},
render: Template,
};
export const NextButtonDisabled = Template.bind({});
NextButtonDisabled.args = {
export const NextButtonDisabled = {
args: {
isOpen: true,
title: "What do you call your group's new policy?",
description: "You can also combine or add new approaches to the list",
@@ -208,4 +216,6 @@ NextButtonDisabled.args = {
nextButtonDisabled: true,
currentStep: 1,
totalSteps: 3,
},
render: Template,
};
+15 -12
View File
@@ -1,6 +1,9 @@
import React, { Suspense, useEffect } from "react";
import Login from "../../app/components/modals/Login";
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
@@ -43,11 +46,11 @@ function FakeMarketingPageBehindOverlay({
return (
<div className="relative min-h-[100dvh] overflow-hidden">
<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
/>
<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
uses backdrop blur (`blurredYellow`).
</p>
@@ -107,13 +110,13 @@ export const HeaderOverlayBlurred = {
belowCard={
<a
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>
}
>
<Suspense fallback={<p className="font-inter p-6">Loading</p>}>
<Suspense fallback={<p className="p-6 text-small-paragraph">Loading</p>}>
<LoginForm />
</Suspense>
</Login>
@@ -152,13 +155,13 @@ export const FullPageRouteSolid = {
belowCard={
<a
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>
}
>
<Suspense fallback={<p className="font-inter p-6">Loading</p>}>
<Suspense fallback={<p className="p-6 text-small-paragraph">Loading</p>}>
<LoginForm />
</Suspense>
</Login>
@@ -178,15 +181,15 @@ export const ModalChromeOnly = {
belowCard={
<a
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>
}
>
<p
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
route&quot; for the real flow.
@@ -215,7 +218,7 @@ export const FormOnly = {
],
render: () => (
<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 />
</Suspense>
</div>
+11 -7
View File
@@ -1,22 +1,28 @@
import React, { useState } from "react";
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). */
export default {
title: "modals/Share",
title: "Components/Modals/Share",
component: Share,
parameters: {
layout: "fullscreen",
docs: {
description: {
component:
"Share modal for published rules (copy link and channel actions).",
},
},
},
};
function ShareStoryHost() {
const [open, setOpen] = useState(true);
return (
<MessagesProvider messages={messages}>
<div className="min-h-[100dvh] bg-[var(--color-surface-inverse-brand-primary)] p-6">
<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)}
>
Open Share
@@ -31,11 +37,9 @@ function ShareStoryHost() {
onDiscordShare={() => {}}
/>
</div>
</MessagesProvider>
);
}
export const Default = {
name: "Modal / Share",
render: () => <ShareStoryHost />,
};
+22 -16
View File
@@ -1,6 +1,7 @@
import React from "react";
import Tooltip from "../../app/components/modals/Tooltip";
import Button from "../../app/components/buttons/Button";
import { TOOLTIP_POSITION_OPTIONS } from "../../lib/propNormalization";
export default {
title: "Components/Modals/Tooltip",
@@ -8,7 +9,7 @@ export default {
argTypes: {
position: {
control: { type: "select" },
options: ["top", "bottom"],
options: [...TOOLTIP_POSITION_OPTIONS],
},
disabled: {
control: { type: "boolean" },
@@ -29,37 +30,42 @@ const Template = (args) => (
</div>
);
export const Default = Template.bind({});
Default.args = {
export const Default = {
args: {
text: "Tooltip text goes here",
position: "top",
disabled: false,
},
render: Template,
};
export const Top = Template.bind({});
Top.args = {
export const Top = {
args: {
text: "Tooltip positioned at top",
position: "top",
},
render: Template,
};
export const Bottom = Template.bind({});
Bottom.args = {
export const Bottom = {
args: {
text: "Tooltip positioned at bottom",
position: "bottom",
},
render: Template,
};
export const Disabled = Template.bind({});
Disabled.args = {
export const Disabled = {
args: {
text: "This tooltip is disabled",
disabled: true,
},
render: Template,
};
export const LongText = Template.bind({});
LongText.args = {
export const LongText = {
args: {
text: "This is a longer tooltip text that demonstrates how the component handles multiple words and extended content",
position: "top",
},
render: Template,
};
export const WithIcon = () => (
<div className="p-16 flex items-center justify-center min-h-[200px]">
<Tooltip text="Tooltip with icon button" position="top">
+6 -2
View File
@@ -1,4 +1,8 @@
import MenuItem from "../../app/components/navigation/MenuItem";
import {
MENU_ITEM_MODE_OPTIONS,
MENU_ITEM_SIZE_OPTIONS,
} from "../../lib/propNormalization";
export default {
title: "Components/Navigation/MenuItem",
@@ -15,12 +19,12 @@ export default {
argTypes: {
mode: {
control: { type: "select" },
options: ["default", "inverse"],
options: [...MENU_ITEM_MODE_OPTIONS],
description: "The visual style mode of the menu item",
},
size: {
control: { type: "select" },
options: ["X Small", "Small", "Medium", "Large", "X Large"],
options: [...MENU_ITEM_SIZE_OPTIONS],
description: "The size of the menu item",
},
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";
export default {
title: "Pages/Create/Informational",
title: "Pages/Create Flow/Informational",
component: InformationalScreen,
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";
export default {
title: "Pages/Create/Review",
title: "Pages/Create Flow/Review",
component: CommunityReviewScreen,
parameters: { layout: "fullscreen" },
};
+1 -1
View File
@@ -1,7 +1,7 @@
import { CommunitySizeSelectScreen } from "../../app/(app)/create/screens/select/CommunitySizeSelectScreen";
export default {
title: "Pages/Create/CommunitySize",
title: "Pages/Create Flow/Community size",
component: CommunitySizeSelectScreen,
parameters: { layout: "fullscreen" },
};
+1 -1
View File
@@ -1,7 +1,7 @@
import { CreateFlowTextFieldScreen } from "../../app/(app)/create/screens/text/CreateFlowTextFieldScreen";
export default {
title: "Pages/Create/CommunityName",
title: "Pages/Create Flow/Community name",
component: CreateFlowTextFieldScreen,
parameters: { layout: "fullscreen" },
};
+1 -1
View File
@@ -1,7 +1,7 @@
import { CommunityUploadScreen } from "../../app/(app)/create/screens/upload/CommunityUploadScreen";
export default {
title: "Pages/Create/CommunityUpload",
title: "Pages/Create Flow/Community upload",
component: CommunityUploadScreen,
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 { QUOTE_BLOCK_VARIANT_OPTIONS } from "../../lib/propNormalization";
export default {
title: "Components/Sections/QuoteBlock",
@@ -34,7 +35,7 @@ A responsive quote section component that displays inspirational governance quot
argTypes: {
variant: {
control: { type: "select" },
options: ["compact", "standard", "extended", "statement"],
options: [...QUOTE_BLOCK_VARIANT_OPTIONS],
description: "Layout variant for different use cases",
},
quote: {
+6 -6
View File
@@ -1,23 +1,23 @@
import HeaderLockup from "../../app/components/type/HeaderLockup";
import {
HEADER_LOCKUP_JUSTIFICATION_OPTIONS,
HEADER_LOCKUP_SIZE_OPTIONS,
} from "../../lib/propNormalization";
export default {
title: "Components/Type/HeaderLockup",
component: HeaderLockup,
parameters: {
layout: "centered",
backgrounds: {
default: "dark",
values: [{ name: "dark", value: "#000000" }],
},
},
argTypes: {
justification: {
control: { type: "select" },
options: ["left", "center", "Left", "Center"],
options: [...HEADER_LOCKUP_JUSTIFICATION_OPTIONS],
},
size: {
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 { NUMBERED_LIST_SIZE_OPTIONS } from "../../lib/propNormalization";
export default {
title: "Components/Type/NumberedList",
component: NumberedList,
parameters: {
layout: "centered",
backgrounds: {
default: "dark",
values: [{ name: "dark", value: "#000000" }],
},
},
argTypes: {
size: {
control: { type: "select" },
options: ["M", "S", "m", "s"],
options: [...NUMBERED_LIST_SIZE_OPTIONS],
},
},
};