一键导入
nextjs-storybook
This skill should be used when working with Storybook, stories, or component documentation. Guides Storybook integration patterns.
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
菜单
This skill should be used when working with Storybook, stories, or component documentation. Guides Storybook integration patterns.
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
基于 SOC 职业分类
This skill should be used when implementing code, creating files, or writing new features. Provides complete file structure, naming patterns, and import rules.
Decompose large requirements into phased task files that fit AI context windows using Gherkin + State Machine approach.
This skill should be used when working with database, Drizzle ORM, entities, schemas, migrations, and seeds. It provides Rails-style DB conventions, local sqlite/libSQL file defaults, and Effect-friendly data patterns.
Use this skill when generating enterprise admin, back-office, or operations module UI. Activates on: new module screen, admin panel, dashboard, operations list, approval queue, entity detail, multi-step form wizard, RBAC console, audit log, analytics cockpit, support console, settings page, or any layout described as "enterprise", "compact", or "admin-heavy". Provides recipe selection, page anatomy contract, density mode, state checklist, and AI prompt template.
This skill should be used when performing browser testing, verifying in browser, or runtime checks. Guides browser verification patterns.
This skill should be used when working with Chakra UI, UI components, or the design system. Guides Chakra UI v3 and Ark UI patterns.
| name | nextjs-storybook |
| description | This skill should be used when working with Storybook, stories, or component documentation. Guides Storybook integration patterns. |
Use this skill when creating or maintaining Storybook stories for component documentation.
// .storybook/main.ts
import type { StorybookConfig } from "@storybook/nextjs-vite";
const config: StorybookConfig = {
addons: [
"@storybook/addon-docs",
"@storybook/addon-a11y",
"@chromatic-com/storybook",
],
features: {
experimentalRSC: true,
},
framework: {
name: "@storybook/nextjs-vite",
options: {
nextConfigPath: "../next.config.ts",
},
},
staticDirs: ["../public"],
stories: ["../src/**/*.stories.@(js|jsx|mjs|ts|tsx)"],
};
export default config;
// .storybook/preview.tsx
import { NextIntlClientProvider } from "next-intl";
import { NuqsAdapter } from "nuqs/adapters/next/app";
import { messages } from "../src/messages";
import { formats } from "../src/shared/config/i18n/formats";
import { TIME_ZONE } from "../src/shared/constants/timezone";
import { Provider } from "../src/shared/vendor/chakra-ui/provider";
import type { Preview } from "@storybook/nextjs-vite";
const preview: Preview = {
decorators: [
(Story, context) => (
<NextIntlClientProvider
formats={formats}
locale="en"
messages={messages.en}
now={new Date()}
timeZone={TIME_ZONE}
>
<NuqsAdapter>
<Provider forcedTheme={context.globals["colorMode"] as "dark" | "light"}>
<Story />
</Provider>
</NuqsAdapter>
</NextIntlClientProvider>
),
],
globalTypes: {
colorMode: {
toolbar: {
items: ["light", "dark"],
title: "Color Mode",
},
},
},
initialGlobals: {
colorMode: "light",
},
parameters: {
controls: {
matchers: {
color: /(background|color)$/i,
date: /Date$/i,
},
},
},
};
export default preview;
// src/modules/users/components/card-user/card-user.stories.tsx
import type { Meta, StoryObj } from "@storybook/nextjs-vite";
import { CardUser } from './card-user';
const meta = {
title: 'modules/users/components/user-card',
component: CardUser,
tags: ['autodocs'],
argTypes: {
user: {
description: 'User object to display',
},
},
} satisfies Meta<typeof CardUser>;
export default meta;
type Story = StoryObj<typeof meta>;
export const Default: Story = {
args: {
user: {
id: '1',
name: 'John Doe',
email: 'john@example.com',
avatar: 'https://i.pravatar.cc/150?u=john',
},
},
};
export const WithoutAvatar: Story = {
args: {
user: {
id: '2',
name: 'Jane Smith',
email: 'jane@example.com',
},
},
};
export const LongName: Story = {
args: {
user: {
id: '3',
name: 'Alexander Bartholomew Christopher Davidson III',
email: 'alexander@example.com',
},
},
};
// src/modules/users/containers/container-user-form/container-user-form.stories.tsx
import type { Meta, StoryObj } from "@storybook/nextjs-vite";
import { within, userEvent, expect } from '@storybook/test';
import { ContainerUserForm } from './container-user-form';
const meta: Meta<typeof ContainerUserForm> = {
title: 'modules/users/containers/user-form-container',
component: ContainerUserForm,
tags: ['autodocs'],
};
export default meta;
type Story = StoryObj<typeof ContainerUserForm>;
export const Default: Story = {};
export const FilledForm: Story = {
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
await userEvent.type(
canvas.getByLabelText(/name/i),
'John Doe'
);
await userEvent.type(
canvas.getByLabelText(/email/i),
'john@example.com'
);
},
};
export const WithValidationErrors: Story = {
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
// Submit empty form to trigger validation
await userEvent.click(canvas.getByRole('button', { name: /submit/i }));
// Verify error messages appear
await expect(canvas.getByText(/name is required/i)).toBeInTheDocument();
},
};
// src/shared/components/button/button.stories.tsx
import { HStack } from "@chakra-ui/react";
import type { Meta, StoryObj } from "@storybook/nextjs-vite";
import { Button } from './button';
const meta: Meta<typeof Button> = {
title: 'shared/components/button',
component: Button,
tags: ['autodocs'],
argTypes: {
variant: {
control: 'select',
options: ['solid', 'outline', 'ghost'],
},
colorPalette: {
control: 'select',
options: ['brand', 'gray', 'red', 'green'],
},
size: {
control: 'select',
options: ['sm', 'md', 'lg'],
},
loading: {
control: 'boolean',
},
disabled: {
control: 'boolean',
},
},
};
export default meta;
type Story = StoryObj<typeof Button>;
export const Default: Story = {
args: {
children: 'Button',
},
};
export const Variants: Story = {
render: () => (
<HStack gap="2">
<Button variant="solid">Solid</Button>
<Button variant="outline">Outline</Button>
<Button variant="ghost">Ghost</Button>
</HStack>
),
};
export const Sizes: Story = {
render: () => (
<HStack align="center" gap="2">
<Button size="sm">Small</Button>
<Button size="md">Medium</Button>
<Button size="lg">Large</Button>
</HStack>
),
};
export const Loading: Story = {
args: {
children: 'Loading',
loading: true,
},
};
// src/modules/users/screens/screen-user-list/screen-user-list.stories.tsx
import type { Meta, StoryObj } from "@storybook/nextjs-vite";
import { ScreenUserList } from './screen-user-list';
const sampleUsers = [
{ email: "john@example.com", id: "user-1", name: "John Doe" },
{ email: "jane@example.com", id: "user-2", name: "Jane Smith" },
];
const meta: Meta<typeof ScreenUserList> = {
title: 'modules/users/screens/user-list-screen',
component: ScreenUserList,
tags: ['autodocs'],
};
export default meta;
type Story = StoryObj<typeof ScreenUserList>;
export const Default: Story = {
args: {
users: sampleUsers,
},
};
export const Empty: Story = {
args: {
users: [],
},
};
export const Loading: Story = {
args: {
users: undefined,
loading: true,
},
};
export const ManyUsers: Story = {
args: {
users: [...sampleUsers, ...sampleUsers, ...sampleUsers],
},
};
src/modules/users/
├── components/
│ └── user-card/
│ ├── user-card.tsx
│ └── user-card.stories.tsx
├── containers/
│ └── user-form-container/
│ ├── user-form-container.tsx
│ └── user-form-container.stories.tsx
└── screens/
└── user-list-screen/
├── user-list-screen.tsx
└── user-list-screen.stories.tsx
// Story title follows the repo path shape
const meta = {
title: "modules/[module-name]/components/[component-name]",
// or
title: "shared/components/[component-name]",
} satisfies Meta<typeof Component>;
# Start Storybook dev server
npm run dev:storybook
# Build static Storybook
npm run build:storybook
# Run the Storybook smoke test project
npm run test -- src/test/stories-smoke.test.tsx
export const Default: Story = {};
export const Loading: Story = {};
export const Empty: Story = {};
export const Error: Story = {};
export const Disabled: Story = {};
// Good - realistic data
args: {
user: {
name: 'Sarah Johnson',
email: 'sarah.johnson@company.com',
},
}
// Bad - placeholder data
args: {
user: {
name: 'Test',
email: 'test@test.com',
},
}
export const FormSubmission: Story = {
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
await userEvent.type(canvas.getByLabelText(/email/i), 'test@example.com');
await userEvent.click(canvas.getByRole('button'));
await expect(canvas.getByText(/success/i)).toBeInTheDocument();
},
};