关于 sb-stories
sb-stories 是一项指导 Storybook 组件与页面故事编写的技能,面向维护组件目录的开发者。它强调依据真实使用情况和状态编写故事,并通过验证脚本检查结果。
主要功能
- 01按实际状态编写组件故事
- 02依据组件使用数据排序变体
- 03导入真实页面并模拟数据
- 04规范 Controls、覆盖层与交互
- 05运行脚本验证故事
适用场景
- 01为现有组件补充 Storybook 状态故事
- 02用真实页面和不同数据状态构建页面故事
- 03批量编写并验证组件故事
MCP 相关
根据项目是否配置 MCP,选择相应的编写参考;WITH_MCP 流程由 MCP 注入 CSF3 约定。
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ strongeron/ storybook-workbench --skill sb-stories安装后在 Claude Code 中输入/sb-stories确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
CONTEXT.mdagents/openai.yamlreferences/anti-patterns.mdcomposition-patterns.mddirectory-structure.mdextraction-workflow.mdfactory-patterns.mdstorybook-surface.mdtest-wiring.mdvalidate-workflow.mdwith-mcp.mdwithout-mcp.mdscripts/check-story-ready.shdiscover-runtime.pyextract-prop-shapes.shextract-states.shpage-patterns.pyscaffold-factory.shscaffold-page-story.pyvalidate-stories.shtemplates/controlled-component-story.tsx详细介绍
sb-stories——单个组件及其真实状态
默认的 Build 模式。组件位于 src/components/ 中;你需要为其编写可见状态的 story。
开始编写前(先问问自己)
- 它真的在使用吗? 检查
.storybook/project-inventory.json中的components.real[]。如果它在dead[]中,就移除它——不要编写故事。如果它属于vendor(shadcnui/),则降低优先级。 - 哪些状态会显著改变行为或外观? 避免笛卡尔积组合。
references/anti-patterns.md中列出了每种基础组件的最低状态表(Button 8 个、Input 8 个、Modal 5 个、Form 6 个)。请读取.storybook/component-states.json,不要猜测——如果文件不存在,请先生成:scripts/extract-states.sh。(更完整的提取上下文见:references/extraction-workflow.md。) 要在同一个画布上渲染所有这些状态,请使用sb-wrappers中的StateGrid包装器(variants × states →StateMatrix)——不要手动搭建网格。这些是组件阶段的包装器;数据包装器(ProjectInventory / DesignSystemHealth / AppFlowGraph …)应在各自步骤完成后再处理。 - 这个应用实际提供了哪些状态? 读取
.storybook/component-usage.json(由sb-inventory的extract-component-usage.sh生成——如果文件不存在,请生成)。使用props.<prop>.values优先展示真实调用处传入的变体;对于declaredButUnused中的内容(例如从未使用的variant=danger),仍需为完整性编写对应状态,但要添加['usage:unused']标签,并注明“此应用中未使用”——不要用生产环境从未渲染的状态充斥目录。 - 无需手动编写每个组件的文档——它们会统一组合生成。 每个组件的 autodocs 页面都会自动获得一个**“此应用中的真实用法”**区域:
sb-setup只需将UsageSection区块接入preview.ts的docs.page一次,它就会为每个组件渲染一张“使用位置”图,展示组件出现在哪些页面、嵌套在什么内容中、渲染了什么、使用了哪些令牌——这些信息来自用法图谱(component-pages.json)。因此,添加故事时,不要手动添加用法区块;只需运行refresh-usage.sh,让图谱保持最新并自动填充该区域。若要交互式探索整个图谱(点击任意令牌 / 组件 / 页面,即可查看其所有使用位置),请搭建sb-wrappers中的UsageExplorer包装器。详情及docs.page组合方式见sb-inventory(§“autodocs 中的真实用法”)和sb-setup(文档页面组合)。 - 要用 Factory 吗? 如果 3 个或更多故事共享同一种数据形状,就要用。读取
.storybook/prop-shapes.json(其中标记了候选项的liveUsages)——如果文件不存在,请先运行scripts/extract-prop-shapes.sh。 然后运行scripts/scaffold-factory.sh <Type> <import-path>,并补全确定性存根(references/factory-patterns.md);否则直接内联args。 - 标题分类体系? 按照
storySort.order匹配;如果项目没有设置,请根据references/directory-structure.md选择一种。 play是否确有必要? 仅当存在交互、异步数据、portal、CSS 驱动的状态或无障碍需求时才编写;绝不要只写一个toBeVisible()(反模式 34)。此外,项目必须恰好有一个CssCheck(反模式 33):一个断言真实getComputedStyle令牌值的故事,这是共享预览已加载应用 CSS 的唯一证明。二者均源自npx storybook ai setup的提示;validate-stories.sh的检查 13 和项目计数校验会强制执行。- “playground” / 展示故事由 Controls 驱动,而不是通过点击 / 悬停操作。 如果评审者问:“这个 playground 故事是什么——我可以点击或悬停吗?”答案是:playground 就是带有 Controls 面板的默认故事——你在 Controls 标签页中更改
args(变体、尺寸、状态),画布就会重新渲染。这不是画布交互;悬停 / 焦点 / 激活状态位于StateGrid交互矩阵(addon-pseudo-states)中,而脚本化的点击→断言流程则位于play函数中。因此,请明确说明故事使用哪一种方式:Controls = 尝试属性 · 伪状态 = 悬停 / 焦点列 ·play= 真实交互。 - Controls 面板已正确配置吗? 组件故事必须提供可用的 Controls 面板——它既是评审者的属性试验区,也为 autodocs 的 ArgTypes 表提供支持。react-vite 默认使用的
react-docgen不会将 TS 联合类型推断为下拉选项,因此要为每个枚举 / 联合类型属性声明argTypes(control: 'select'|'inline-radio'+options),使用table.category分组,隐藏逃生阀 / 不可序列化属性(className、refs、icon / callback / data 属性),并在仅用于渲染的展示故事中禁用控件(parameters: { controls: { disable: true } })。完整模式及 docgen 注意事项见:references/without-mcp.md§13。(面板本身必须可见——sb-setup会写入包含showPanel: true的manager.ts;sb-audit的audit-controls.sh会标记任何缺少此配置的组件故事。)
编写源文件(互斥——只加载一个)
grep -q '@storybook/addon-mcp' package.json && test -f .mcp.json && echo WITH_MCP || echo WITHOUT_MCP
WITH_MCP→references/with-mcp.md(MCP 会注入 CSF3 约定;你只需专注于判断)。 Storybook 的工具名称会随次要版本变化;当前名称见references/storybook-surface.md。WITHOUT_MCP→references/without-mcp.md(13 个验证缺口 + 4 个关键 SB10 模式)。- 受控组件(Switch/Toggle/Checkbox/Tabs/Accordion/Select)应从
templates/controlled-component-story.tsx开始——AI 容易弄错的是useArgs同步。
文件放在哪里(先询问——不要把文件散落在仓库各处)。 从
.storybook/audit/status.md 读取 storiesLocation(唯一规则位于 CONTEXT.md §STORIES LOCATION)。
- 如果未设置(例如,仓库已有 Storybook,因此
sb-setup从未询问),先停下并询问用户,再编写任何 story——绝不要猜测,也不要悄悄把文件放在同一目录。使用AskUserQuestion(Claude)/request_user_input(Codex);如果没有可用的阻塞式工具,则使用编号列表:应该把 stories 保存在哪里?(其他所有内容都已位于
.storybook/下。).storybook/stories/(推荐——集中在一处、彼此隔离;不改动src/,只需删除一个文件夹即可移除)- 与组件放在一起
src/**/<Name>.stories.tsx(适用于长期维护的项目) - 自定义文件夹 (由你指定——仍集中在一处)
然后在
.storybook/audit/status.md中将其记录为storiesLocation: <isolated|colocated|PATH>,确保main.ts的stories包含该路径,然后继续。推荐选项 1。
isolated(或.storybook/stories/) → 在.storybook/stories/下按目录树镜像编写 (.storybook/stories/components/CourseCard.stories.tsx),通过@/别名导入组件。 开发服务器不会监视.storybook/stories/:在那里新增、移动或删除 story 文件后,该文件会一直不在索引中(或在索引中保持过时状态),且每个 MCP 工具都会报告 “No story found”。在那里更改文件后,需重启 Storybook(已在 10.6.0 上验证;src/中的 stories 会实时更新)。colocated→src/components/<X>/<X>.stories.tsx。自定义路径 → 每个 story 都写入该路径。
无论用户选择哪种方式,所有 stories 都必须放在同一个位置——绝不要混用。
标题(即 Storybook 内的路径,与文件路径分开):遵循 .storybook/preview.ts
中的 storySort.order;否则使用 Components/<Domain>/<Name>。Overview / Spec / hub story 是一个入口——通过 storySort.order 将其固定在根目录顶部(根级 hub 排在最前;每个功能组内的 Overview/Spec 在子排序中排在最前,并以 '*' 收尾),绝不要让它按字母顺序排到其所概述内容的中间。参见 references/directory-structure.md → “Overview / spec / hub”。
页面 — 真实页面捕获(模式 A)
对于一个页面(pages/ · app/ · routes/ · views/ 下的路由/视图),不要重新创建它。
先运行 scripts/page-patterns.py <root>——它会逐页报告 importable、component、
layout、dataHook 和作为模拟信号的 dataType、sections(渲染顺序,不含 ui/),
gridHint,以及 sharedSections[]。然后根据 importable 选择模式:
importable: true(页面有一个默认导出的组件)→ 原样导入真实页面,只模拟其数据层。 使用scripts/scaffold-page-story.py <root> <page-suffix>搭建:它会生成一个导入真实页面并连接检测到的 Provider (InertiausePage/ router / store)的Pages/<Name>story,其 props 由基于dataType的工厂函数提供初始数据 (scripts/scaffold-factory.sh <dataType> <import-path>,参见references/factory-patterns.md)。 针对每种实质不同的数据状态(空 / 已填充 / 错误)各添加一个 story——使用不同的工厂函数输入,绝不使用不同的标记。 绝不要重写页面的 JSX(反模式 27):布局、列和组件都属于真实页面,而不是你。importable: false(内联组合 / 没有单一组件)→ 回退到页面组合 (references/composition-patterns.md模式 4):使用真实的sections进行组合—— 仍由工厂函数提供数据,仍使用真实组件。
sharedSections[](至少由 2 个页面渲染的 section)是可复用的页面模式组件——为每个 section
单独创建一个 Components/* story,让页面组合这些组件,而不是重复实现。
autodocs 中的覆盖层(Dialog / Modal / Sheet / Drawer / Popover)
打开的覆盖层会将 position:fixed inset-0 覆盖层传送到 document.body。如果像 Primary 区块那样在
autodocs 页面中内联渲染,覆盖层会覆盖整个 Docs 页面——标题、“真实用法”部分和 Controls 都会消失在空白遮罩层后面。因此,对于任何带有 autodocs 的覆盖层组件,都应将 story 限定在其自身的 iframe 中:
parameters: {
layout: 'fullscreen',
docs: { story: { inline: false, height: '640px' } }, // portal stays inside the frame; Docs prose stays readable
}
story 视图(一个 story、完整画布)不受影响——这只针对 Docs 页面。不要对非覆盖层组件使用它(内联渲染更轻量)。
页面自身的 dataHook 只是其数据;预览还必须提供页面所运行于其中的 Provider 树 + 根 CSS。 从
.storybook/runtime.json 读取这些内容(如果文件不存在,则运行 scripts/discover-runtime.py . --out .storybook/runtime.json)——
包括 providers[]、rootCss、portals[]、network.needsMsw——它们由 sb-setup 在共享预览中一次性设置,因此页面 story 通常无需重新连接它们。如果脚本已经写入 .storybook/*.json,绝不要再通过 shell 扫描推导这些内容——直接引用相应字段。
批量处理(多个组件)
每个组件编写一个 story(每个 story 只涵盖其真实状态),然后使用
scripts/validate-stories.sh 逐一检查。在 Claude Code 中,可以用 Agent 工具并行编写组件来加快批量处理,但工作内容相同——无需专用子 Agent。
完成前的检查
SKILL=${CLAUDE_PLUGIN_ROOT:-${CLAUDE_PLUGIN_ROOT}}
CORE=${CLAUDE_PLUGIN_ROOT:-${CLAUDE_PLUGIN_ROOT}}
"$CORE/scripts/validate-stories.sh" path/to/Foo.stories.tsx
# --strict adds tsc + eslint · --diff lints only changed stories
# For the "give me confidence" verdict (setup readiness preflight + conformance in one),
# run "$SKILL/scripts/check-story-ready.sh" path/to/Foo.stories.tsx instead — CONFIDENT when discovery JSONs are present.
任何 FAIL 都会以非零状态退出——继续之前先修复。如果你编写了 play,还要派发判断子 Agent(参见
references/validate-workflow.md);bash 无法验证 play 是否有意义。要将 stories 配置为 Agent 可运行的 CLI 检查(无头 vitest + a11y),请参见
references/test-wiring.md。