用途
本技能用于创建页面级 Storybook 故事(Page Stories),展示多个组件如何组合成真实页面布局。页面故事应尽量贴近终端用户在 Drupal Canvas 中的实际体验,因此应避免使用 Canvas 中不可用的模式。
位置与命名
- 页面故事必须放在
src/stories/example-pages/目录。 - 页面故事文件应命名为
<page-name>.stories.jsx。 - Storybook 标题必须使用以下格式:
title: "Example pages/[Page Title]"。
单故事提升(Single-story hoisting)
为获得更简洁的导航,页面故事应使用 Storybook 的单故事提升功能,避免侧边栏出现不必要的嵌套。实现方式:
- 在标题中包含完整页面名称;
- 只导出一个故事(通常为
Default); - 将故事的
name属性设置为与标题最后一段一致。
示例:
// src/stories/example-pages/product-detail.stories.jsx
export default {
title: "Example pages/Product: Detail",
component: ProductDetailPage,
parameters: {
layout: "fullscreen",
},
};
export const Default = {
name: "Product: Detail",
};
这样会在 "Example pages" 下生成扁平的侧边栏条目 "Product: Detail",而不是嵌套的 "Product: Detail" → "Default" 结构。
创建新组件时,如果适合,可将其加入现有页面故事,或创建新的页面故事以在上下文中展示该组件。修改现有组件时,应检查 src/stories/example-pages/ 中的页面故事,确保改动在组合布局中正常工作,必要时更新它们。
PageLayout 组件
所有页面故事必须使用来自 src/stories/example-pages/page-layout.jsx 的共享 PageLayout 组件。
创建 PageLayout 的时机:
- 创建第一个页面故事时,或
src/stories/example-pages/中不存在该组件时。
PageLayout 结构示例:
// src/stories/example-pages/page-layout.jsx
import Footer from "@/components/footer";
import Header from "@/components/header";
import Section from "@/components/section";
const footerData = {
// Shared footer data
};
const PageLayout = ({ children }) => (
<>
<Section width="wide" content={<Header />} />
{children}
<Section width="wide" content={<Footer {...footerData} />} />
</>
);
export default PageLayout;
使用 PageLayout 的示例:
// src/stories/example-pages/about-page.stories.jsx
import Section from "@/components/section";
import Text from "@/components/text";
import PageLayout from "./page-layout";
const AboutPage = () => (
<PageLayout>
<Section width="normal" content={<Text text="<p>About us...</p>" />} />
</PageLayout>
);
export default {
title: "Example pages/About",
component: AboutPage,
parameters: { layout: "fullscreen" },
};
export const Default = { name: "About" };
组合规则
页面故事只能导入和组合组件。
允许:
- 从
@/components/<name>导入; - 传递 props 并组合在一起;
- 定义示例数据(字符串、对象、数组)。
不允许:
- 内联定义 React 组件;
- 使用原始 HTML 元素(
<div>、<span>)进行布局; - 复制现有组件代码。
错误示例(内联定义组件并使用原始 HTML 元素):
const Logo = ({ color }) => <div className="flex">...</div>;
const Page = () => (
<div className="flex flex-col gap-8">
<Logo color="#000" />
<div className="mx-auto max-w-3xl">Content</div>
</div>
);
正确示例(导入并组合现有组件):
import Footer from "@/components/footer";
import Header from "@/components/header";
import Section from "@/components/section";
import Text from "@/components/text";
const Page = () => (
<>
<Header />
<Section width="normal" content={<Text text="<p>Content here</p>" />} />
<Footer />
</>
);
如果需要 <div>,应寻找现有组件。如果不存在,先在 src/components/ 中创建它。
页面故事中不使用 className
className prop 在 Canvas 中未暴露,页面故事不应传递它。
错误示例:
const AboutPage = () => (
<PageLayout>
<Section width="normal">
<Text className="mt-8 mb-12" content="..." />
</Section>
<Card className="shadow-xl" title="Mission" />
</PageLayout>
);
正确示例:
const AboutPage = () => (
<PageLayout>
<Section width="normal">
<Text content="..." />
</Section>
<Card title="Mission" />
</PageLayout>
);
className 适用的场景:
- 在组件自身的
index.jsx中组合其他组件时; - 在单个组件故事中(非页面故事)。
使用 Spacer 组件控制间距
使用 spacer 而非 margin 或 padding 来控制组件之间的间距。
如果 spacer 不存在,复制它:
cp -r examples/components/spacer src/components/
cp examples/stories/spacer.stories.jsx src/stories/
正确示例:
import Spacer from "@/components/spacer";
const AboutPage = () => (
<PageLayout>
<Hero title="About Us" />
<Spacer height="large" />
<Section width="normal">
<Text content="<p>Our story...</p>" />
</Section>
<Spacer height="medium" />
<Section width="normal">
<Text content="<p>Our mission...</p>" />
</Section>
</PageLayout>
);
错误示例:
const AboutPage = () => (
<PageLayout>
<Hero title="About Us" />
<div className="mt-16">
<Section width="normal">
<Text content="<p>Our story...</p>" />
</Section>
</div>
</PageLayout>
);
布局组件
使用现有布局组件而非内联样式。检查 src/components/ 和 examples/components/ 中的:
- 宽度约束:
section、container; - 列布局:
grid-container、columns; - 间距:
spacer、divider。
正确示例:
<WidthConstraintComponent variant="wide">
<ColumnLayoutComponent columns="sidebar-main">
{/* Content */}
</ColumnLayoutComponent>
</WidthConstraintComponent>
错误示例:
<div className="mx-auto max-w-6xl px-4">
<div className="grid grid-cols-[300px_1fr] gap-8">
{/* Content */}
</div>
</div>
适用场景
- 为 Nebula 项目创建页面级 Storybook 故事;
- 展示多个组件在真实页面布局中的组合方式;
- 确保页面故事与 Drupal Canvas 的终端用户体验保持一致;
- 在修改现有组件时验证其在组合布局中的表现。
限制
- 页面故事必须放在指定目录并使用指定命名和标题格式;
- 只能导入和组合现有组件,不能内联定义组件或使用原始 HTML 元素做布局;
- 页面故事中不能传递
className; - 间距必须使用 spacer 组件,布局必须使用现有布局组件;
- 应避免使用 Canvas 中不可用的模式。