← 返回笔记首页

Web Design System Pipeline

从 Pencil 设计规范到租户可定制主题的完整管线 · 更新于 2026-02-08

目录
1. 背景:为什么需要这条管线 2. 完整管线架构 3. 核心原理:CSS 变量级联覆盖 4. 两层 Token 设计(非三层) 5. 为什么用 oklch 色彩空间 6. 历史遗留:两套 Token 系统并存 7. Pencil 在管线中的角色 8. Storybook 的定位 9. 组件清单与优先级 10. 实施阶段 11. 优点总结 12. 与 Flutter 端的关系

1. 背景:为什么需要这条管线

Web 端存在三个问题:

  1. 没有设计规范文件 — 28 个 UI 组件没有视觉规范,一致性靠开发者记忆
  2. Token 体系混乱 — 两套 CSS 变量系统并存(design-tokens.css + globals.css),组件同时还硬编码 Tailwind 颜色类
  3. 租户主题色无法生效 — 硬编码的 bg-blue-600 不会跟着主题变量走

这条管线的目标:让租户 Super Admin 改一个颜色值,全站所有组件自动跟着变,零代码改动

2. 完整管线架构

设计规范层 designs/web-design-system.pen — Token 变量(颜色、字号、间距、圆角) — 核心组件规范(Button, Card, Dialog, Input...) — 品牌主题预览(默认主题 + 示例租户主题) ↓ 视觉规范(人工对齐,不自动生成) Token 定义层 globals.css :root { } — --primary / --secondary / --accent(语义化 Token) — --background / --foreground / --muted(表面 Token) — --radius / --shadow(形状 Token) — @theme inline { }(Tailwind 桥接) ↓ CSS 变量 → Tailwind 类 组件实现层 components/ui/*.tsx — Button → bg-primary text-primary-foreground — Card → bg-card text-card-foreground — Input → border-input bg-background — ...全部通过语义化 Tailwind 类消费 Token ↓ import 组件 页面使用层 app/[locale]/[tenant]/xxx/page.tsx — 直接用 <Button>, <Card> 等,不关心具体颜色 ↓ 租户 Super Admin 改主题色 主题切换层 运行时注入 CSS 变量覆盖 :root { --primary: oklch(0.65 0.15 45); } ← 租户金色 所有用了 bg-primary 的组件自动变色,零代码改动

3. 核心原理:CSS 变量级联覆盖

整条管线能工作的基石是 CSS Custom Properties 的级联(Cascade)特性

/* Step 1: globals.css 定义默认值 */
:root {
  --primary: oklch(0.205 0 0);        /* 默认深色 */
}

/* Step 2: @theme inline 把 CSS 变量桥接为 Tailwind 类 */
@theme inline {
  --color-primary: var(--primary);     /* bg-primary 现在能用了 */
}

/* Step 3: 组件使用语义化类 */
<Button className="bg-primary" />     /* 编译时不知道具体颜色 */

/* Step 4: 运行时,租户主题色覆盖 :root 变量 */
:root {
  --primary: oklch(0.65 0.18 55);     /* 租户的金色 */
}
/* 所有 bg-primary 的元素自动变成金色,无需重新编译 */
关键点

CSS 变量是运行时解析的,不是编译时。这意味着通过 JavaScript 修改 :root 上的变量值,页面上所有引用该变量的元素会即时更新,不需要重新构建。

这和 Tailwind 的 utility class 不冲突 — bg-primary 在 Tailwind 编译时生成 background-color: var(--color-primary),运行时再解析 var(--color-primary) 的具体值。

4. 两层 Token 设计(非三层)

Flutter 端用了 Material 的三层 Token(Reference → System → Component)。Web 端只用两层

层级含义例子存储位置
原语 Token 色盘中的具体值 oklch(0.65 0.18 55) — 某个金色 租户设置 / 数据库
语义 Token 这个值的用途 --primary — 品牌主色 globals.css :root

为什么不要第三层(Component Token)?

Component Token(如 --button-bg: var(--primary))在 Web 端不值得:

Flutter 端为什么需要三层?

因为 Flutter 的 ThemeData + ColorScheme 天然是 Component Token 模式 — 每个 Material Widget 内部已经在引用 colorScheme.primary,你不需要手动建这一层。Web 端没有这种框架级支持,自己建第三层是过度工程。

5. 为什么用 oklch 色彩空间

globals.css 用 oklch 而非 hex,有两个关键原因:

5.1 感知均匀(Perceptual Uniformity)

/* oklch: 改亮度 0.5 → 0.7,人眼感知均匀变亮 */
--primary: oklch(0.5 0.15 250);  /* 深蓝 */
--primary: oklch(0.7 0.15 250);  /* 亮蓝 — 感知上刚好亮了"一档" */

/* hex: 加亮 10%,视觉变化不可预测 */
--primary: #1a5276;  /* 深蓝 */
--primary: #2e86c1;  /* 亮蓝 — 感知上可能变了 30%,也可能只变了 5% */

这对租户自定义主题色至关重要 — 系统需要根据主色自动生成 hover 态、disabled 态、前景色(文字色),oklch 让这些计算可预测。

5.2 Tailwind 的透明度语法

/* bg-primary/90 需要色彩空间支持 alpha 分解 */
<button className="bg-primary/90 hover:bg-primary">
/* oklch 原生支持: oklch(0.205 0 0 / 0.9) */
/* hex 需要额外转换: #000000 → rgba(0,0,0,0.9) — Tailwind 处理不了 */

6. 历史遗留:两套 Token 系统并存

现状问题

Web 端目前有两套互不相认的 CSS 变量体系:

文件命名风格色彩格式谁在消费
design-tokens.css Material 风格
--color-primary-500
hex (#e91e63) 几乎没有组件在用
globals.css :root shadcn/ui 风格
--primary
oklch (oklch(0.205 0 0)) 部分组件通过 bg-primary 消费

决策:以 globals.css 的 shadcn/ui 变量体系为正式标准。design-tokens.css 标记为 legacy,不再新增变量。

原因:shadcn/ui 体系已经和 Tailwind 深度整合(通过 @theme inline),组件可以直接用 bg-primary 这样的语义化类,开发体验最好。

7. Pencil 在管线中的角色

Pencil (.pen)关系代码 (globals.css + components/)
Token 变量(~21 个色彩) ← 手动对齐 → :root CSS 变量
组件视觉规范(状态/尺寸) ← 手动对齐 → components/ui/*.tsx
品牌主题预览 ← 视觉验证 → Storybook Theme Switcher
重要原则

Pencil 是视觉规范的 source of truth,代码是实现的 source of truth。两者之间是人工对齐关系,不是自动生成。

这和 Flutter 端的工作流完全一致:Flutter 端也是 Pencil 定义视觉规范 → 手动实现 Dart Widget → Widgetbook 验证。

8. Storybook 的定位

工具Flutter 端对应职责
Pencil (.pen) Pencil (.pen) 组件的设计规范:应该长什么样
Storybook Widgetbook 组件的活文档:实际渲染效果、交互预览

Storybook 保留并升级,增加 Theme Switcher decorator:在 Storybook 中实时切换不同租户主题预览。

9. 组件清单与优先级

现有 Storybook 组件(28 个)

优先级组件数量来源
P0 核心 Button, Input, Card, Dialog, ConfirmDialog, Table, Checkbox, Switch, Avatar, Badge, Alert 11 Radix UI + Custom
P1 布局 TopBar, Sidebar, Breadcrumb, Tabs, StatsCard 5 Custom
P2 辅助 Select, Textarea, Calendar, Progress, BottomDrawer, Popover, LoadingSpinner, Skeleton, Tooltip, Separator, Label 11 Radix UI + Custom
跳过 I18nDemo 1 Demo only

业务组件(已有 Storybook Stories)

组件位置说明
ScheduleKanbanCardSchedules/排班看板卡片
StatsGridDashboard/仪表盘统计网格
ServiceCard / CategoryCard / ServiceGridAdmin/服务管理卡片
AdminLayoutlayouts/管理后台布局
Typographydesign/字体排版系统

10. 实施阶段

Phase 1 统一 Token 层

目标:让 globals.css 的 :root 变量成为唯一 Token 来源

影响范围:仅 CSS 文件

Phase 2 组件 Token 消费改造

目标:消除组件中的硬编码颜色

✘ 现在

<button className="bg-blue-600
  hover:bg-blue-700 text-white">

换主题色不跟着变

✔ 改后

<button className="bg-primary
  hover:bg-primary/90
  text-primary-foreground">

换主题色自动生效

影响范围:28 个 UI 组件文件,按 P0 → P1 → P2 分批

Phase 3 建立 Pencil Web 设计系统

目标:创建 designs/web-design-system.pen

Phase 4 Storybook 升级 ✅ 已完成

目标:Storybook 成为可交互的主题预览工具

Theme Switcher 工作原理

利用 CSS Custom Properties 级联特性,在 decorator 的包裹 <div> 上通过 style 属性覆盖 :root 变量。 Light 模式不做覆盖(使用 globals.css 默认值),Dark/Brand 模式设置对应的 oklch 色值。零 JS 运行时成本。

11. 优点总结

优点说明
租户主题色真的能用了 改一个 CSS 变量,全站所有组件跟着变,零代码改动
设计-开发对齐 Pencil 组件和代码组件用同一套 Token 命名,沟通无歧义
零运行时成本 CSS 变量是浏览器原生能力,不需要 JS 库、不增加 bundle size
渐进式改造 不需要一次改完 28 个组件,按优先级逐步替换,每步都可验证
Storybook 继续发挥作用 和 Flutter 的 Widgetbook 一样,作为"活文档"验证实际渲染
Web 和 Flutter 独立演进 两套设计系统各自维护,不相互阻塞

12. 与 Flutter 端的关系

架构决策:Web 端与 Flutter 端设计系统保持分离

这是有意为之的设计决策,延续自 设计与开发工作流 文档中的约定。

维度Flutter 端Web 端
Token 来源 packages/celoria_tokens/tokens.json globals.css :root
Token 生成 自动(generate.js → Dart/TS/CSS) 手写 CSS 变量
品牌色 Gold / Indigo / Teal 租户可自定义
Token 层数 三层(Reference → System → Component) 两层(原语 → 语义)
组件库 celoria_ui(14 个 Dart Widget) components/ui/(28 个 React 组件)
活文档 Widgetbook Storybook
设计规范 designs/celoria-design-system.pen designs/web-design-system.pen(Phase 3)

两端共享的只有 Pencil MCP 工具链(同一套 Pencil 操作 API),以及 "设计规范 → 手动对齐 → 代码实现 → 活文档验证" 的工作流模式。具体的 Token 值、色盘、组件实现完全独立。