+
+ ),
+}
diff --git a/frontend/documentation/components/Banner.stories.tsx b/frontend/documentation/components/Banner.stories.tsx
deleted file mode 100644
index 6e6dbc31764b..000000000000
--- a/frontend/documentation/components/Banner.stories.tsx
+++ /dev/null
@@ -1,179 +0,0 @@
-import React from 'react'
-import type { Meta, StoryObj } from 'storybook'
-
-import Banner from 'components/Banner'
-import type { BannerProps } from 'components/Banner'
-import { Button } from 'components/base/forms/Button'
-
-const meta: Meta = {
- argTypes: {
- children: {
- control: 'text',
- description:
- 'Banner message content. Can include a CTA button as a child.',
- },
- variant: {
- control: 'select',
- description: 'Feedback colour variant.',
- options: ['success', 'warning', 'danger', 'info'],
- },
- },
- args: {
- children: 'This is a banner message.',
- variant: 'info',
- },
- component: Banner,
- parameters: { layout: 'padded' },
- title: 'Components/Banner',
-}
-
-export default meta
-
-type Story = StoryObj
-
-// ---------------------------------------------------------------------------
-// Default — interactive playground
-// ---------------------------------------------------------------------------
-
-export const Default: Story = {}
-
-// ---------------------------------------------------------------------------
-// Individual variants
-// ---------------------------------------------------------------------------
-
-export const Success: Story = {
- args: {
- children: 'Your changes have been saved successfully.',
- variant: 'success',
- },
- parameters: {
- docs: {
- description: {
- story: 'Use `success` for confirming a completed action.',
- },
- },
- },
-}
-
-export const Warning: Story = {
- args: {
- children: 'Your trial is ending in 3 days.',
- variant: 'warning',
- },
- parameters: {
- docs: {
- description: {
- story:
- 'Use `warning` for cautionary messages that need attention but are not critical.',
- },
- },
- },
-}
-
-export const Danger: Story = {
- args: {
- children: 'Your API key has been revoked.',
- variant: 'danger',
- },
- parameters: {
- docs: {
- description: {
- story: 'Use `danger` for errors or critical issues.',
- },
- },
- },
-}
-
-export const Info: Story = {
- args: {
- children: 'A new version of Flagsmith is available.',
- variant: 'info',
- },
- parameters: {
- docs: {
- description: {
- story: 'Use `info` for neutral informational messages.',
- },
- },
- },
-}
-
-// ---------------------------------------------------------------------------
-// With CTA (passed as children)
-// ---------------------------------------------------------------------------
-
-export const WithCTA: Story = {
- name: 'With CTA button',
- parameters: {
- docs: {
- description: {
- story:
- 'Add a CTA by passing a `Button` as part of `children`. This keeps the Banner API simple — the banner renders whatever you give it.',
- },
- },
- },
- render: () => (
-
- Your trial is ending in 3 days.
-
-
- ),
-}
-
-export const DangerWithCTA: Story = {
- name: 'Danger with CTA',
- parameters: {
- docs: {
- description: {
- story:
- 'For danger banners, use `theme="danger"` on the CTA button for visual consistency.',
- },
- },
- },
- render: () => (
-
- Your API key has been revoked.
-
-
- ),
-}
-
-// ---------------------------------------------------------------------------
-// All variants
-// ---------------------------------------------------------------------------
-
-export const AllVariants: Story = {
- name: 'All variants',
- parameters: {
- docs: {
- description: {
- story:
- 'All four banner variants. Each has a default icon that matches the variant. Banners are persistent — not closable or dismissable.',
- },
- },
- },
- render: () => (
-
-
- Your changes have been saved successfully.
-
-
- Your trial is ending in 3 days.
-
-
-
- Your API key has been revoked.
-
-
- A new version of Flagsmith is available.
-
- ),
-}
diff --git a/frontend/documentation/components/BooleanDotIndicator.stories.tsx b/frontend/documentation/components/BooleanDotIndicator.stories.tsx
new file mode 100644
index 000000000000..1d2a14bb0957
--- /dev/null
+++ b/frontend/documentation/components/BooleanDotIndicator.stories.tsx
@@ -0,0 +1,41 @@
+import React from 'react'
+import type { Meta, StoryObj } from 'storybook'
+
+import BooleanDotIndicator from 'components/BooleanDotIndicator'
+
+const meta: Meta = {
+ args: { enabled: true },
+ component: BooleanDotIndicator,
+ parameters: {
+ docs: {
+ description: {
+ component:
+ 'A small coloured dot signalling an on/off state. Used inline where space is tight — typically inside a tooltip trigger for permission rows or similar binary indicators.',
+ },
+ },
+ layout: 'centered',
+ },
+ title: 'Components/Data Display/BooleanDotIndicator',
+}
+export default meta
+
+type Story = StoryObj
+
+export const Enabled: Story = {}
+
+export const Disabled: Story = {
+ args: { enabled: false },
+}
+
+export const AllStates: Story = {
+ render: () => (
+
+
+ Enabled
+
+
+ Disabled
+
+
+ ),
+}
diff --git a/frontend/documentation/components/Breadcrumb.stories.tsx b/frontend/documentation/components/Breadcrumb.stories.tsx
new file mode 100644
index 000000000000..3b74e255e852
--- /dev/null
+++ b/frontend/documentation/components/Breadcrumb.stories.tsx
@@ -0,0 +1,32 @@
+import React from 'react'
+import type { Meta, StoryObj } from 'storybook'
+
+import Breadcrumb from 'components/Breadcrumb'
+import { withRouter } from './_decorators'
+
+const meta: Meta = {
+ decorators: [withRouter],
+ parameters: { layout: 'padded' },
+ title: 'Components/Navigation/Breadcrumb',
+}
+export default meta
+
+type Story = StoryObj
+
+export const Default: Story = {
+ render: () => (
+
+ ),
+}
+
+export const SingleLevel: Story = {
+ render: () => (
+
+ ),
+}
diff --git a/frontend/documentation/components/Button.stories.tsx b/frontend/documentation/components/Button.stories.tsx
index e4be8e2ffdab..d7be200780d3 100644
--- a/frontend/documentation/components/Button.stories.tsx
+++ b/frontend/documentation/components/Button.stories.tsx
@@ -53,34 +53,19 @@ export default meta
type Story = StoryObj
-// ---------------------------------------------------------------------------
-// Default — interactive playground
-// ---------------------------------------------------------------------------
-
export const Default: Story = {}
-// ---------------------------------------------------------------------------
-// All Variants
-// ---------------------------------------------------------------------------
-
export const Variants: Story = {
parameters: {
docs: {
description: {
story:
- 'All available button themes. Use `primary` for main actions, `secondary` for alternatives, `outline` for low-emphasis actions, `danger` for destructive actions, and `success` for positive confirmations.',
+ 'All available button themes. Use `primary` for main actions, `secondary` for alternatives, `outline` for low-emphasis actions, `danger` for destructive actions, and `success` for positive confirmations. `icon` is for icon-only buttons (copy, action triggers in tables); `project` is the avatar-style button used in the project picker.',
},
},
},
render: () => (
-
+ ),
+}
+
+export const Shapes: Story = {
+ parameters: {
+ docs: {
+ description: {
+ story:
+ '`square` (default) is the standard swatch. `circle` is used as a dot indicator — typical for boolean or status keys.',
+ },
+ },
+ },
+ render: () => (
+
+
+
+
+ ),
+}
+
+export const Palette: Story = {
+ parameters: {
+ docs: {
+ description: {
+ story:
+ 'A row of swatches representing a palette. Useful for previewing the available colours when assigning colour-coded values.',
+ },
+ },
+ },
+ render: () => (
+
+ {CHART_COLOURS.map((c, i) => (
+
+ ))}
+
+ ),
+}
diff --git a/frontend/documentation/components/Column.stories.tsx b/frontend/documentation/components/Column.stories.tsx
new file mode 100644
index 000000000000..f6bb3234dcf8
--- /dev/null
+++ b/frontend/documentation/components/Column.stories.tsx
@@ -0,0 +1,26 @@
+import React from 'react'
+import type { Meta, StoryObj } from 'storybook'
+
+import Column from 'components/base/grid/Column'
+
+const meta: Meta = {
+ parameters: { layout: 'padded' },
+ title: 'Components/Layout/Column',
+}
+export default meta
+
+type Story = StoryObj
+
+const Block: React.FC<{ label: string }> = ({ label }) => (
+
{label}
+)
+
+export const Default: Story = {
+ render: () => (
+
+
+
+
+
+ ),
+}
diff --git a/frontend/documentation/components/DropdownMenu.stories.tsx b/frontend/documentation/components/DropdownMenu.stories.tsx
new file mode 100644
index 000000000000..1270adc60df5
--- /dev/null
+++ b/frontend/documentation/components/DropdownMenu.stories.tsx
@@ -0,0 +1,45 @@
+import React from 'react'
+import type { Meta, StoryObj } from 'storybook'
+import { screen, userEvent, within } from 'storybook/test'
+
+import DropdownMenu from 'components/base/DropdownMenu'
+
+const meta: Meta = {
+ parameters: {
+ docs: { story: { height: '260px' } },
+ layout: 'centered',
+ },
+ title: 'Components/Data Display/DropdownMenu',
+}
+export default meta
+
+type Story = StoryObj
+
+const DEMO_ITEMS = [
+ { label: 'Edit', onClick: () => {} },
+ { label: 'Duplicate', onClick: () => {} },
+ { label: 'Archive', onClick: () => {} },
+ { label: 'Delete', onClick: () => {} },
+]
+
+export const Default: Story = {
+ render: () => ,
+}
+
+export const Open: Story = {
+ parameters: {
+ chromatic: { delay: 300 },
+ docs: {
+ description: {
+ story:
+ 'Menu in its open state. Storybook clicks the trigger in `play`; the menu portals into `document.body`, so it is queried via `screen` rather than the local canvas.',
+ },
+ },
+ },
+ play: async ({ canvasElement }: { canvasElement: HTMLElement }) => {
+ const canvas = within(canvasElement)
+ await userEvent.click(canvas.getByRole('button'))
+ await screen.findByText('Edit')
+ },
+ render: () => ,
+}
diff --git a/frontend/documentation/components/EmptyState.stories.tsx b/frontend/documentation/components/EmptyState.stories.tsx
new file mode 100644
index 000000000000..e3e567620606
--- /dev/null
+++ b/frontend/documentation/components/EmptyState.stories.tsx
@@ -0,0 +1,95 @@
+import React, { ComponentProps } from 'react'
+import type { Meta, StoryObj } from 'storybook'
+
+import EmptyState from 'components/EmptyState'
+import Button from 'components/base/forms/Button'
+
+type EmptyStateProps = ComponentProps
+
+const meta: Meta = {
+ argTypes: {
+ description: {
+ control: 'text',
+ description: 'Supporting copy below the title.',
+ },
+ docsLabel: {
+ control: 'text',
+ description: 'Link label when `docsUrl` is set.',
+ },
+ docsUrl: {
+ control: 'text',
+ description: 'Optional docs link rendered below the description.',
+ },
+ icon: {
+ control: 'select',
+ description:
+ 'Icon name from the design system. A representative subset is exposed here; the component accepts any `IconName`.',
+ options: [
+ 'features',
+ 'people',
+ 'flask',
+ 'info',
+ 'search',
+ 'setting',
+ 'shield',
+ 'bar-chart',
+ 'pie-chart',
+ 'bell',
+ ],
+ },
+ iconColour: {
+ control: 'color',
+ description:
+ 'Icon fill colour. Defaults to a neutral grey when not provided.',
+ },
+ title: {
+ control: 'text',
+ description: 'Headline shown to the user.',
+ },
+ },
+ args: {
+ description: 'Create your first feature flag to get started.',
+ icon: 'features',
+ title: 'No features yet',
+ },
+ component: EmptyState,
+ parameters: { layout: 'padded' },
+ title: 'Components/Feedback/EmptyState',
+}
+export default meta
+
+type Story = StoryObj
+
+// ---------------------------------------------------------------------------
+// Default — interactive playground (controls-driven)
+// ---------------------------------------------------------------------------
+
+export const Default: Story = {}
+
+// ---------------------------------------------------------------------------
+// Variants — deterministic snapshots
+// ---------------------------------------------------------------------------
+
+export const WithAction: Story = {
+ args: {
+ description: 'Segments let you target specific users.',
+ icon: 'people',
+ title: 'No segments found',
+ },
+ render: (args: EmptyStateProps) => (
+ Create segment}
+ />
+ ),
+}
+
+export const WithDocsLink: Story = {
+ args: {
+ description: 'Read the docs to learn how to set up your first project.',
+ docsLabel: 'View docs',
+ docsUrl: 'https://docs.flagsmith.com',
+ icon: 'info',
+ title: 'No projects yet',
+ },
+}
diff --git a/frontend/documentation/components/ErrorMessage.stories.tsx b/frontend/documentation/components/ErrorMessage.stories.tsx
new file mode 100644
index 000000000000..f6a634c98f17
--- /dev/null
+++ b/frontend/documentation/components/ErrorMessage.stories.tsx
@@ -0,0 +1,52 @@
+import React from 'react'
+import type { Meta, StoryObj } from 'storybook'
+
+import ErrorMessage from 'components/ErrorMessage'
+
+const meta: Meta = {
+ parameters: {
+ docs: {
+ description: {
+ component:
+ 'Inline alert for API errors. Pass the raw error response and the component will surface a readable message; renders nothing when the error is falsy.',
+ },
+ },
+ layout: 'padded',
+ },
+ title: 'Components/Feedback/ErrorMessage',
+}
+export default meta
+
+type Story = StoryObj
+
+export const StringError: Story = {
+ render: () => ,
+}
+
+export const ApiErrorWithData: Story = {
+ render: () => (
+
+ ),
+}
+
+export const ApiErrorWithMessage: Story = {
+ render: () => (
+
+ ),
+}
+
+export const NonFieldErrors: Story = {
+ render: () => (
+
+ ),
+}
diff --git a/frontend/documentation/components/Flex.stories.tsx b/frontend/documentation/components/Flex.stories.tsx
new file mode 100644
index 000000000000..d51b6a3cb058
--- /dev/null
+++ b/frontend/documentation/components/Flex.stories.tsx
@@ -0,0 +1,40 @@
+import React from 'react'
+import type { Meta, StoryObj } from 'storybook'
+
+import Flex from 'components/base/grid/Flex'
+
+const meta: Meta = {
+ parameters: { layout: 'padded' },
+ title: 'Components/Layout/Flex',
+}
+export default meta
+
+type Story = StoryObj
+
+const Block: React.FC<{ label: string }> = ({ label }) => (
+
+
+ ),
+}
diff --git a/frontend/documentation/components/PasswordRequirements.stories.tsx b/frontend/documentation/components/PasswordRequirements.stories.tsx
new file mode 100644
index 000000000000..b3916b81108a
--- /dev/null
+++ b/frontend/documentation/components/PasswordRequirements.stories.tsx
@@ -0,0 +1,77 @@
+import React, { useState } from 'react'
+import type { Meta, StoryObj } from 'storybook'
+
+import PasswordRequirements from 'components/PasswordRequirements'
+import Input from 'components/base/forms/Input'
+
+const meta: Meta = {
+ parameters: {
+ docs: {
+ description: {
+ component:
+ 'Checklist of password validation rules (length, numbers, special characters, casing) shown **below** a password input. Renders only the list — green for met, red for unmet — and reports the overall pass state via `onRequirementsMet`. The input is owned by the parent; the stories below pair one with the component to demonstrate real usage.',
+ },
+ },
+ layout: 'padded',
+ },
+ title: 'Components/Feedback/PasswordRequirements',
+}
+export default meta
+
+type Story = StoryObj
+
+const Interactive = ({ initial = '' }: { initial?: string }) => {
+ const [password, setPassword] = useState(initial)
+ return (
+
@@ -71,42 +71,6 @@ export const Variants: Story = {
),
}
-// ---------------------------------------------------------------------------
-// When to use
-// ---------------------------------------------------------------------------
-
-export const WhenToUse: Story = {
- name: 'When to use',
- parameters: {
- docs: {
- description: {
- story: `
-**Use Skeleton when:**
-- Loading data from an API and you want to show the layout shape before content arrives
-- The content area has a predictable structure (list rows, cards, form fields)
-- You want to reduce perceived loading time by showing a placeholder
-
-**Don't use Skeleton when:**
-- The loading state is brief (<200ms) — use a spinner instead
-- The content structure is unpredictable — use a full-page spinner
-- You're loading a single value — inline spinners work better
-
-**Accessibility:**
-- Shimmer animation respects \`prefers-reduced-motion: reduce\`
-- Skeleton elements are decorative — screen readers skip them
- `,
- },
- },
- },
- render: () => (
-