从 Pencil 设计规范到租户可定制主题的完整管线 · 更新于 2026-02-08
Web 端存在三个问题:
design-tokens.css + globals.css),组件同时还硬编码 Tailwind 颜色类bg-blue-600 不会跟着主题变量走这条管线的目标:让租户 Super Admin 改一个颜色值,全站所有组件自动跟着变,零代码改动。
整条管线能工作的基石是 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) 的具体值。
Flutter 端用了 Material 的三层 Token(Reference → System → Component)。Web 端只用两层:
| 层级 | 含义 | 例子 | 存储位置 |
|---|---|---|---|
| 原语 Token | 色盘中的具体值 | oklch(0.65 0.18 55) — 某个金色 |
租户设置 / 数据库 |
| 语义 Token | 这个值的用途 | --primary — 品牌主色 |
globals.css :root |
Component Token(如 --button-bg: var(--primary))在 Web 端不值得:
bg-primary text-primary-foreground),不在 CSS 中因为 Flutter 的 ThemeData + ColorScheme 天然是 Component Token 模式 — 每个 Material Widget 内部已经在引用 colorScheme.primary,你不需要手动建这一层。Web 端没有这种框架级支持,自己建第三层是过度工程。
globals.css 用 oklch 而非 hex,有两个关键原因:
/* 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 让这些计算可预测。
/* 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 处理不了 */
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 这样的语义化类,开发体验最好。
| 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 验证。
| 工具 | Flutter 端对应 | 职责 |
|---|---|---|
| Pencil (.pen) | Pencil (.pen) | 组件的设计规范:应该长什么样 |
| Storybook | Widgetbook | 组件的活文档:实际渲染效果、交互预览 |
Storybook 保留并升级,增加 Theme Switcher decorator:在 Storybook 中实时切换不同租户主题预览。
| 优先级 | 组件 | 数量 | 来源 |
|---|---|---|---|
| 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 |
| 组件 | 位置 | 说明 |
|---|---|---|
| ScheduleKanbanCard | Schedules/ | 排班看板卡片 |
| StatsGrid | Dashboard/ | 仪表盘统计网格 |
| ServiceCard / CategoryCard / ServiceGrid | Admin/ | 服务管理卡片 |
| AdminLayout | layouts/ | 管理后台布局 |
| Typography | design/ | 字体排版系统 |
目标:让 globals.css 的 :root 变量成为唯一 Token 来源
globals.css 中的 :root — 补充 --success、--warning、--info 等语义色@theme inline — 映射所有新增 Token 到 Tailwind 类design-tokens.css 为 legacy影响范围:仅 CSS 文件
目标:消除组件中的硬编码颜色
<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 分批
目标:创建 designs/web-design-system.pen
目标:Storybook 成为可交互的主题预览工具
@storybook/nextjs framework adapter.storybook/main.ts — stories 路径、framework、addons 配置.storybook/preview.tsx — Theme Switcher decorator + next-intl providernpm run storybook (端口 6006)、npm run storybook:build利用 CSS Custom Properties 级联特性,在 decorator 的包裹 <div> 上通过 style 属性覆盖 :root 变量。
Light 模式不做覆盖(使用 globals.css 默认值),Dark/Brand 模式设置对应的 oklch 色值。零 JS 运行时成本。
| 优点 | 说明 |
|---|---|
| 租户主题色真的能用了 | 改一个 CSS 变量,全站所有组件跟着变,零代码改动 |
| 设计-开发对齐 | Pencil 组件和代码组件用同一套 Token 命名,沟通无歧义 |
| 零运行时成本 | CSS 变量是浏览器原生能力,不需要 JS 库、不增加 bundle size |
| 渐进式改造 | 不需要一次改完 28 个组件,按优先级逐步替换,每步都可验证 |
| Storybook 继续发挥作用 | 和 Flutter 的 Widgetbook 一样,作为"活文档"验证实际渲染 |
| 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 值、色盘、组件实现完全独立。