Skip to content

Commit a5c004d

Browse files
committed
docs: add storybook for components and embed live previews
- Initialize Storybook with Vite in @appshell/react. - Add Header and Footer stories. - Embed Storybook iframes into Fumadocs documentation for Header and Footer. - Update GitHub Pages deployment workflow to build and publish Storybook.
1 parent d87f06b commit a5c004d

11 files changed

Lines changed: 319 additions & 1 deletion

File tree

.github/workflows/docs.yml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,12 +41,17 @@ jobs:
4141
env:
4242
NEXT_PUBLIC_BASE_PATH: /${{ env.REPO_NAME }}/examples
4343

44+
- name: Build Storybook
45+
run: cd packages/react && pnpm build-storybook
46+
4447
- name: Merge outputs
4548
run: |
4649
mkdir -p _site
4750
cp -r apps/docs/out/* _site/
4851
mkdir -p _site/examples
4952
cp -r apps/examples/out/* _site/examples/
53+
mkdir -p _site/storybook
54+
cp -r packages/react/storybook-static/* _site/storybook/
5055
5156
- name: Upload artifact
5257
uses: actions/upload-pages-artifact@v3

apps/docs/content/docs/components/footer.mdx

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,19 @@ description: Fixed footer with three variants -- tab-bar, floating, and mini --
55

66
The `Footer` component renders a fixed-position bar at the bottom of the viewport. It supports three visual variants and an auto-hide behavior.
77

8+
<div className="my-6">
9+
<iframe
10+
src="/react-appshell/storybook/iframe.html?id=components-footer--tab-bar&viewMode=story"
11+
className="w-full h-[300px] border rounded-lg bg-background shadow-sm"
12+
title="Footer Storybook Preview"
13+
/>
14+
<div className="mt-2 text-sm text-right">
15+
<a href="/react-appshell/storybook/index.html?path=/story/components-footer--tab-bar" target="_blank" rel="noopener noreferrer" className="underline text-muted-foreground hover:text-foreground transition-colors">
16+
Open full Storybook
17+
</a>
18+
</div>
19+
</div>
20+
821
## Footer props
922

1023
| Prop | Type | Default | Description |

apps/docs/content/docs/components/header.mdx

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,19 @@ description: A flexible, scroll-aware header with 6 behaviors, 3 themes, and mob
55

66
The `Header` component renders up to three rows -- navigation, context (title/subtitle), and search -- and controls how they respond to scrolling.
77

8+
<div className="my-6">
9+
<iframe
10+
src="/react-appshell/storybook/iframe.html?id=components-header--default&viewMode=story"
11+
className="w-full h-[400px] border rounded-lg bg-background shadow-sm"
12+
title="Header Storybook Preview"
13+
/>
14+
<div className="mt-2 text-sm text-right">
15+
<a href="/react-appshell/storybook/index.html?path=/story/components-header--default" target="_blank" rel="noopener noreferrer" className="underline text-muted-foreground hover:text-foreground transition-colors">
16+
Open full Storybook
17+
</a>
18+
</div>
19+
</div>
20+
821
## Props
922

1023
| Prop | Type | Default | Description |
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
@import 'tailwindcss';
2+
@source "../src";
3+
4+
@custom-variant dark (&:is(.dark *));
5+
6+
:root {
7+
--background: oklch(1 0 0);
8+
--foreground: oklch(0.145 0 0);
9+
--card: oklch(1 0 0);
10+
--card-foreground: oklch(0.145 0 0);
11+
--popover: oklch(1 0 0);
12+
--popover-foreground: oklch(0.145 0 0);
13+
--primary: oklch(0.205 0 0);
14+
--primary-foreground: oklch(0.985 0 0);
15+
--secondary: oklch(0.97 0 0);
16+
--secondary-foreground: oklch(0.205 0 0);
17+
--muted: oklch(0.97 0 0);
18+
--muted-foreground: oklch(0.556 0 0);
19+
--accent: oklch(0.97 0 0);
20+
--accent-foreground: oklch(0.205 0 0);
21+
--destructive: oklch(0.577 0.245 27.325);
22+
--destructive-foreground: oklch(0.577 0.245 27.325);
23+
--border: oklch(0.922 0 0);
24+
--input: oklch(0.922 0 0);
25+
--ring: oklch(0.708 0 0);
26+
--radius: 0.625rem;
27+
}
28+
29+
.dark {
30+
--background: oklch(0.145 0 0);
31+
--foreground: oklch(0.985 0 0);
32+
--card: oklch(0.145 0 0);
33+
--card-foreground: oklch(0.985 0 0);
34+
--popover: oklch(0.145 0 0);
35+
--popover-foreground: oklch(0.985 0 0);
36+
--primary: oklch(0.985 0 0);
37+
--primary-foreground: oklch(0.205 0 0);
38+
--secondary: oklch(0.269 0 0);
39+
--secondary-foreground: oklch(0.985 0 0);
40+
--muted: oklch(0.269 0 0);
41+
--muted-foreground: oklch(0.708 0 0);
42+
--accent: oklch(0.269 0 0);
43+
--accent-foreground: oklch(0.985 0 0);
44+
--destructive: oklch(0.396 0.141 25.723);
45+
--destructive-foreground: oklch(0.637 0.237 25.331);
46+
--border: oklch(0.269 0 0);
47+
--input: oklch(0.269 0 0);
48+
--ring: oklch(0.439 0 0);
49+
}
50+
51+
@theme inline {
52+
--font-sans: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
53+
--font-mono: ui-monospace, SFMono-Regular, 'SF Mono', Menlo, Consolas, 'Liberation Mono', monospace;
54+
--color-background: var(--background);
55+
--color-foreground: var(--foreground);
56+
--color-card: var(--card);
57+
--color-card-foreground: var(--card-foreground);
58+
--color-popover: var(--popover);
59+
--color-popover-foreground: var(--popover-foreground);
60+
--color-primary: var(--primary);
61+
--color-primary-foreground: var(--primary-foreground);
62+
--color-secondary: var(--secondary);
63+
--color-secondary-foreground: var(--secondary-foreground);
64+
--color-muted: var(--muted);
65+
--color-muted-foreground: var(--muted-foreground);
66+
--color-accent: var(--accent);
67+
--color-accent-foreground: var(--accent-foreground);
68+
--color-destructive: var(--destructive);
69+
--color-destructive-foreground: var(--destructive-foreground);
70+
--color-border: var(--border);
71+
--color-input: var(--input);
72+
--color-ring: var(--ring);
73+
--radius-sm: calc(var(--radius) - 4px);
74+
--radius-md: calc(var(--radius) - 2px);
75+
--radius-lg: var(--radius);
76+
--radius-xl: calc(var(--radius) + 4px);
77+
}
78+
79+
@layer base {
80+
* {
81+
@apply border-border outline-ring/50;
82+
}
83+
body {
84+
@apply bg-background text-foreground;
85+
}
86+
}

packages/react/.storybook/main.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import type { StorybookConfig } from '@storybook/react-vite';
2+
3+
import { dirname } from "path"
4+
5+
import { fileURLToPath } from "url"
6+
7+
/**
8+
* This function is used to resolve the absolute path of a package.
9+
* It is needed in projects that use Yarn PnP or are set up within a monorepo.
10+
*/
11+
function getAbsolutePath(value: string) {
12+
return dirname(fileURLToPath(import.meta.resolve(`${value}/package.json`)))
13+
}
14+
const config: StorybookConfig = {
15+
"stories": [
16+
"../src/**/*.mdx",
17+
"../src/**/*.stories.@(js|jsx|mjs|ts|tsx)"
18+
],
19+
"addons": [
20+
getAbsolutePath('@chromatic-com/storybook'),
21+
getAbsolutePath('@storybook/addon-vitest'),
22+
getAbsolutePath('@storybook/addon-a11y'),
23+
getAbsolutePath('@storybook/addon-docs')
24+
],
25+
"framework": getAbsolutePath('@storybook/react-vite')
26+
};
27+
export default config;
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import type { Preview } from '@storybook/react-vite'
2+
import './globals.css';
3+
4+
const preview: Preview = {
5+
parameters: {
6+
controls: {
7+
matchers: {
8+
color: /(background|color)$/i,
9+
date: /Date$/i,
10+
},
11+
},
12+
13+
a11y: {
14+
// 'todo' - show a11y violations in the test UI only
15+
// 'error' - fail CI on a11y violations
16+
// 'off' - skip a11y checks entirely
17+
test: 'todo'
18+
}
19+
},
20+
};
21+
22+
export default preview;
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import * as a11yAddonAnnotations from "@storybook/addon-a11y/preview";
2+
import { setProjectAnnotations } from '@storybook/react-vite';
3+
import * as projectAnnotations from './preview';
4+
5+
// This is an important step to apply the right configuration when testing your stories.
6+
// More info at: https://storybook.js.org/docs/api/portable-stories/portable-stories-vitest#setprojectannotations
7+
setProjectAnnotations([a11yAddonAnnotations, projectAnnotations]);

packages/react/package.json

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,9 @@
4444
"typecheck": "tsc --noEmit",
4545
"lint": "eslint src/",
4646
"clean": "rm -rf dist",
47-
"size": "node scripts/size-report.mjs"
47+
"size": "node scripts/size-report.mjs",
48+
"storybook": "storybook dev -p 6006",
49+
"build-storybook": "storybook build"
4850
},
4951
"peerDependencies": {
5052
"react": ">=18.0.0",
@@ -57,22 +59,35 @@
5759
}
5860
},
5961
"devDependencies": {
62+
"@chromatic-com/storybook": "^5.0.1",
6063
"@eslint/js": "^9.0.0",
64+
"@storybook/addon-a11y": "^10.2.13",
65+
"@storybook/addon-docs": "^10.2.13",
66+
"@storybook/addon-vitest": "^10.2.13",
67+
"@storybook/react-vite": "^10.2.13",
68+
"@tailwindcss/vite": "^4.2.1",
6169
"@testing-library/jest-dom": "^6.6.0",
6270
"@testing-library/react": "^16.3.0",
6371
"@types/react": "^19.2.0",
6472
"@types/react-dom": "^19.2.0",
73+
"@vitest/browser": "3.2.4",
74+
"@vitest/coverage-v8": "3.2.4",
6575
"clsx": "^2.1.0",
6676
"eslint": "^9.0.0",
6777
"eslint-plugin-react-hooks": "^7.0.1",
78+
"eslint-plugin-storybook": "^10.2.13",
6879
"framer-motion": "^12.6.0",
6980
"jsdom": "^26.1.0",
81+
"playwright": "^1.58.2",
7082
"react": "^19.2.0",
7183
"react-dom": "^19.2.0",
84+
"storybook": "^10.2.13",
7285
"tailwind-merge": "^3.3.0",
86+
"tailwindcss": "^4.1.0",
7387
"tsup": "^8.4.0",
7488
"typescript": "^5.7.3",
7589
"typescript-eslint": "^8.56.1",
90+
"vite": "^7.3.1",
7691
"vitest": "^3.2.0"
7792
}
7893
}
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
import React from 'react';
2+
import type { Meta, StoryObj } from '@storybook/react';
3+
import { Footer, FooterItem } from './Footer';
4+
import { AppShellProvider } from './context';
5+
import { Home, Search, PlusCircle, Bell, User } from 'lucide-react';
6+
7+
const meta = {
8+
title: 'Components/Footer',
9+
component: Footer,
10+
parameters: {
11+
layout: 'fullscreen',
12+
},
13+
decorators: [
14+
(Story) => (
15+
<AppShellProvider>
16+
<div className="h-screen bg-muted/20 pb-20">
17+
<Story />
18+
</div>
19+
</AppShellProvider>
20+
),
21+
],
22+
tags: ['autodocs'],
23+
} satisfies Meta<typeof Footer>;
24+
25+
export default meta;
26+
type Story = StoryObj<typeof meta>;
27+
28+
export const TabBar: Story = {
29+
args: {
30+
variant: 'tab-bar',
31+
behavior: 'static',
32+
children: (
33+
<>
34+
<FooterItem icon={<Home className="size-5" />} label="Home" active />
35+
<FooterItem icon={<Search className="size-5" />} label="Search" />
36+
<FooterItem icon={<PlusCircle className="size-5" />} label="Add" />
37+
<FooterItem icon={<Bell className="size-5" />} label="Alerts" badge={3} />
38+
<FooterItem icon={<User className="size-5" />} label="Profile" />
39+
</>
40+
),
41+
},
42+
};
43+
44+
export const Floating: Story = {
45+
args: {
46+
variant: 'floating',
47+
position: 'right',
48+
children: (
49+
<button className="flex items-center gap-2 rounded-full bg-primary px-4 py-3 font-semibold text-primary-foreground shadow-lg hover:shadow-xl transition-all hover:-translate-y-1">
50+
<PlusCircle className="size-5" /> Create
51+
</button>
52+
),
53+
},
54+
};
55+
56+
export const Mini: Story = {
57+
args: {
58+
variant: 'mini',
59+
children: (
60+
<div className="flex w-full items-center justify-between">
61+
<span className="text-sm font-medium">1 item selected</span>
62+
<div className="flex gap-2">
63+
<button className="text-sm font-medium text-primary hover:underline">Edit</button>
64+
<button className="text-sm font-medium text-destructive hover:underline">Delete</button>
65+
</div>
66+
</div>
67+
),
68+
},
69+
};
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import React from 'react';
2+
import type { Meta, StoryObj } from '@storybook/react';
3+
import { Header } from './Header';
4+
import { AppShellProvider } from './context';
5+
import { HeaderNav, HeaderNavItem } from './HeaderNav';
6+
7+
const meta = {
8+
title: 'Components/Header',
9+
component: Header,
10+
parameters: {
11+
layout: 'fullscreen',
12+
},
13+
decorators: [
14+
(Story) => (
15+
<AppShellProvider>
16+
<Story />
17+
</AppShellProvider>
18+
),
19+
],
20+
tags: ['autodocs'],
21+
} satisfies Meta<typeof Header>;
22+
23+
export default meta;
24+
type Story = StoryObj<typeof meta>;
25+
26+
export const Default: Story = {
27+
args: {
28+
title: 'Dashboard',
29+
subtitle: 'Welcome back, user',
30+
theme: 'light',
31+
behavior: 'static',
32+
logo: <span className="font-bold text-xl ml-4">AppShell</span>,
33+
nav: (
34+
<HeaderNav>
35+
<HeaderNavItem label="Home" active />
36+
<HeaderNavItem label="Analytics" />
37+
<HeaderNavItem label="Settings" />
38+
</HeaderNav>
39+
),
40+
},
41+
};
42+
43+
export const PrimaryTheme: Story = {
44+
args: {
45+
...Default.args,
46+
theme: 'primary',
47+
},
48+
};
49+
50+
export const DarkTheme: Story = {
51+
args: {
52+
...Default.args,
53+
theme: 'dark',
54+
},
55+
};

0 commit comments

Comments
 (0)