Poco 使用手册
开发指南

主题系统与前端配色开发手册

Poco 多主题架构、语义 Token 契约,以及新增主题、开发页面和排查配色问题的标准流程。

本文是 Poco 前端主题开发的主入口,服务三类工作:

  1. 接入一套新的主题方案。
  2. 开发新页面、新组件或新功能。
  3. 优化现有页面的配色,修复 Light/Dark 或主题之间的视觉兼容问题。

目标不是让所有页面“换成某一种颜色”,而是让页面只表达稳定的视觉语义,由主题决定最终色值。做到这一点后,新增主题主要修改主题定义和注册信息,不再逐页修补。

相关资料:

  • 主题视觉验收与踩坑手册:真实问题、审美基线和验收清单。
  • docs/New_Steel_AI前端开发标准_React_Next_Tailwind_2026-07-23.md:New Steel 的原始设计输入。
  • frontend/app/theme-presets.css:当前主题最终配色实现,色值冲突时以这里为准。
  • frontend/tests/theme-presets.test.mjs:主题组件状态的机器契约,变量完整性冲突时以这里为准。

按任务快速进入

当前任务先读主要交付
新增主题“主题架构”“新增主题流程”“完成定义”完整 Light/Dark 映射、前后端注册、测试和页面验收
开发新页面“新页面与新功能流程”“组件用色规则”只消费语义类、复用共享组件、多主题可用
优化现有配色“配色问题诊断流程”与踩坑手册修复正确层级,保护其他主题、模式和布局

不可破坏的规则

  1. 业务组件不得感知主题 ID。 不读取 data-theme-preset,不写 new-steel:*,不根据 classicnew_steel 返回颜色。
  2. 主题方案和 Light/Dark 是两条独立轴。 切换主题不能修改用户亮暗偏好,切换亮暗模式不能选择品牌主题。
  3. 默认主题必须是安全基线。 新主题适配不能顺手改变 Classic 的配色、布局或交互。
  4. 配色修改不能改变布局。 不借主题适配修改间距、尺寸、圆角、排列、溢出或 DOM 结构。
  5. 独立视觉表面必须显式拥有背景。 页面画布、内容卡片、浮层和嵌套区域不能依赖“刚好接近”的底色。
  6. 状态必须完整。 Default、Hover、Active、Focus、Selected、Disabled、Loading 不能互相借色。
  7. 优先修复系统层。 依次检查 Token、共享组件、主题语义类,最后才考虑真正的业务例外。
  8. 颜色不能只看源码。 最终必须在真实页面、真实数据和真实交互状态下确认观感。

主题架构

两条主题轴

DOM 表达负责内容不负责内容
主题方案data-theme-preset="classic"data-theme-preset="new_steel"色板、字体、表面、功能色、圆角和阴影用户选择 Light 还是 Dark
亮暗模式根节点 .dark当前主题方案在亮暗环境中的变量值选择具体主题方案

New Steel 的作用域示例:

:root[data-theme-preset="new_steel"] {
  color-scheme: light;
}

:root[data-theme-preset="new_steel"].dark {
  color-scheme: dark;
}

只有主题定义文件可以使用主题 ID。业务组件只能使用 bg-cardtext-muted-foregroundtheme-card-hover 等稳定语义。

Token 的三层模型

层级New Steel 示例使用者
主题私有色板--ns-primary--ns-bg-surface只在 theme-presets.css 内维护
项目语义 Token--primary--card--surface-card-hover全部主题共同履行的契约
组件语义类bg-primarybg-cardtheme-card-hover共享组件和业务页面

不要让业务组件直接消费 --ns-*。下一套主题可以使用完全不同的私有变量前缀,但必须映射到同一组项目语义 Token。

关键源码和职责

文件职责新增普通主题时
frontend/app/theme-presets.css各主题的 Light/Dark 色板和语义映射必须新增完整作用域
frontend/app/globals.cssClassic 默认值、Tailwind 映射和 theme-*现有契约足够时不修改
frontend/lib/theme-presets.ts主题 ID、默认值、规范化、即时应用和浏览器缓存必须扩展
frontend/app/layout.tsx服务端把公共配置写入根节点通常不修改
frontend/components/shared/theme-provider.tsx用户 Light/Dark 偏好不修改
frontend/components/docs/docs-theme-preset-sync.tsx静态文档加载后校准运行时主题并同步标签页通常不修改
frontend/features/admin/components/admin-system-configs-page.tsx管理员保存后即时应用主题选项仍由配置驱动时不修改
backend/app/schemas/public_config.py公开配置主题类型必须扩展
backend/app/services/system_config_service.py主题白名单、默认值和读取必须扩展
frontend/lib/i18n/locales/{zh,en}/translation.json后台显示名称必须扩展
frontend/tests/theme-presets.test.mjs回退、作用域和组件状态契约必须扩展

项目语义契约

基础语义 Token

基础 Token 对应 shadcn/ui 和 Tailwind 的常用语义,新增主题至少逐组核对:

分组重点 Token
页面与表面--background--foreground--card--card-foreground--popover--popover-foreground
次级与中性交互--secondary--secondary-foreground--muted--muted-foreground--accent--accent-foreground
Primary--primary--primary-foreground--primary-hover--primary-active--primary-subtle--interactive-foreground--ring
Disabled 与 Loading--disabled-foreground--skeleton
功能色--destructive*--success*--warning*--info*--status-*-foreground--status-solid-foreground
结构--border--input--chart-1--chart-5
Sidebar--sidebar*--sidebar-nav-pressed-foreground
形状与层级--radius--font-sans--shadow-*

frontend/app/globals.css 中的 @theme inline 把这些变量映射成 Tailwind 语义类。只有新增项目级语义角色时才修改该映射。

组件状态 Token

基础语义不足以表达“这是页面画布、卡片头还是大面积 Hover”。项目因此维护组件状态 Token:

角色Token / 类典型用途
公共表头--surface-header / theme-surface-header页面头、卡片头、面板标题区
内容表面--surface-content / theme-surface-content历史消息区、表格正文、带边框列表
文本产物预览--surface-artifact-text-preview / theme-artifact-text-previewMarkdown 等文本产物的阅读表面
弱卡片--surface-muted-card / theme-muted-cardClassic 的弱背景卡片
嵌套表面--surface-inset / theme-surface-inset一级卡片内部统计块和信息块
表单弹窗--surface-form-* / theme-form-*弹窗 Body 与一级表单分组
工具执行--surface-tool-* / theme-tool-*工具步骤、输入和输出载荷
分享页--surface-share-*--share-* / theme-share-*公开分享页表面、身份和状态强调
表格头尾--surface-*-table-* / theme-*-table-*shadcn Table 和原生 table
中小面积 Hover--surface-hover-* / theme-hover-*导航、列表行、选项
大卡片 Hover--surface-card-hover* / theme-card-hover*业务卡片和抬升卡片
特定能力卡片 Hover--surface-capability-card-hover / theme-capability-card-hover首页“更多能力”卡片
Selected--surface-selected* / theme-*-selected选中行、可选择卡片
Tabs--tab-*--compact-tab-* / theme-tab-*默认、紧凑和下划线式 Tabs
特殊状态--ai-note-*--sidebar-nav-pressed-foregroundAI 听记图形、导航按下瞬间
输入文字选择--text-selection-background--text-selection-foregroundInput、Textarea 的文字全选状态

完整、可执行的变量清单位于 componentStateTokens

frontend/tests/theme-presets.test.mjs

新增主题必须让该测试通过。文档负责解释语义,测试负责防止遗漏。

静态文档的运行时主题

在线手册继续使用静态生成,避免为了读取全局主题而把全部 MDX 页面改为按请求渲染。主题选择通过以下方式保持动态:

  1. 管理员保存主题后,applyThemePreset() 同时更新根节点和浏览器缓存。
  2. 文档布局在内容展示前优先应用浏览器缓存,避免新标签页回退到构建期默认主题。
  3. 文档客户端异步读取公开配置进行校准,并监听 storage 事件同步同源标签页。

同步逻辑只能更新 html[data-theme-preset],不得复制主题色值或在文档组件中判断主题 ID。公开配置请求必须保持非阻塞,不能牺牲静态文档的首屏和缓存能力。

新增组件语义角色

只在现有 Token 无法准确表达、并且角色具有复用价值时新增:

  1. :root 提供与 Classic 当前视觉等价的默认值。
  2. 在所有主题作用域提供映射;Light/Dark 不同则分别覆写。
  3. globals.css 增加与主题名称无关的 theme-* 类。
  4. 让业务组件消费新语义类,不读取主题 ID。
  5. 把 Token 加入 componentStateTokens,补充必要的使用断言。
  6. 同步本文和踩坑手册

如果一个“新 Token”只服务一个随意色值,而不是稳定视觉角色,应停止并重新判断问题层级。

组件用色规则

先判断表面归属

问题选择
页面最底层承载区域?bg-background
独立内容块、卡片、固定页头?bg-card text-card-foreground
Dialog、Popover、Sheet、菜单?bg-popover text-popover-foreground
一级表面内部的弱分组?bg-secondarybg-muted 或已有 theme-* 嵌套表面
已有专用业务角色?使用对应 theme-*,不要退回通用灰色

background 是页面画布,不是“所有容器的白色”。一个区域拥有边框、圆角、独立滚动、固定头部或浮层关系时,通常也应该拥有明确的 Surface。

透明背景只适合已知父级表面内的无边界内容。不要依靠 bg-muted/20bg-background/50 之类的低透明度色独立表达卡片边界;底层主题变化后,它们可能消失或融入画布。

文字与实底前景

语义类用途
text-foreground标题、正文和关键数据
text-secondary-foreground可连续阅读的说明、描述
text-muted-foreground时间、ID、元信息、占位提示
text-disabled-foreground真正禁用的控件
text-interactive链接、选中态文字、可点击图标
text-status-*Success、Warning、Danger、Info 状态
text-primary-foregroundPrimary 实底上的文字和图标
text-destructive-foregroundDanger 实底上的文字和图标
text-status-solid其他功能色实底上的文字和图标

Secondary、Muted、Disabled 不能因为看起来都是灰色就合并。红色或蓝色实底也不能继续使用普通 text-foreground

Default、Hover、Selected 必须分开

  • Primary Button:Default、Hover、Active 分别使用对应 Token。
  • 导航和列表行:允许使用 theme-hover-*
  • 大面积业务卡片:优先保持 Surface,通过边框、轻阴影或抬升表达 Hover。
  • Selected:使用稳定的 Selected Token,不能只在当前底色上叠一层极淡透明色。
  • 默认静态卡片:使用中性边框;主题色边框只用于 Hover、Selected、Focus 或明确状态。

Hover 不能让卡片变得接近页面画布,也不能看起来和 Selected 一样。Hover/Active 只改视觉状态,不能改变组件尺寸和布局。

Tabs 是一组状态,不是一种颜色

Tabs 至少分别定义:

  1. List 容器背景。
  2. Inactive 背景与前景。
  3. Hover 背景与前景。
  4. Active 背景、前景、边框和阴影。
  5. Line 变体的指示线和数量 Badge。

Trigger 的直属图标应使用 currentColor。Badge 等嵌套组件需要通过 group-data-[state=active] 读取 Trigger 状态,不能把 data-[state=active] 写在没有该属性的子元素上。

不要全局覆盖 Tab 内所有 SVG;部分 SVG 可能表达 Success、Warning 等独立状态。

Dialog、表单和输入框

  • 新增/编辑/配置表单优先使用共享 FixedFormDialog
  • Dialog 外壳使用 Popover,Body 和表单分组使用对应 theme-form-*
  • 普通 Input/Textarea 保留自己的语义表面。
  • Input/Textarea 的文字选择状态统一使用 --text-selection-*,不能借用过浅的 Hover/Accent 背景。
  • Composer 或 InputGroup 已经承担边框和背景时,内层输入才使用 bg-transparent dark:bg-transparent
  • 不要全局移除输入背景,也不要只写 Light 下生效的 bg-transparent

共享组件优先

优先使用:

  • Button 变体处理主操作、次级操作和危险操作。
  • Badge 变体处理状态。
  • Tabs 变体处理块状、紧凑和下划线导航。
  • Table 处理表头、Hover 和 Selected。
  • FixedFormDialog 处理表单浮层结构。

如果多个页面出现同类问题,先检查共享组件;如果只有某个主题出现,先检查主题映射;如果只有一种业务角色出现,再考虑项目语义类。

新增主题流程

1. 先完成设计输入

在写代码前分别确认 Light 和 Dark:

  • Page、Surface、Subtle、Hover、Selected、Disabled。
  • Primary、Secondary、Muted、Disabled 文字。
  • 默认和强边框。
  • Primary Default、Hover、Active、Subtle、Focus、Solid Foreground。
  • Success、Warning、Danger、Info 的前景、弱背景和实底前景。
  • Sidebar、Skeleton、Chart、Shadow、Radius 和字体。

只有品牌主色不能支撑一套主题。缺少状态色和前景色时,开发阶段必然产生未经确认的临时混色。

2. 注册稳定主题 ID

主题 ID 使用小写 ASCII snake_case,用户可见名称只写在 i18n。至少同步:

  1. frontend/lib/theme-presets.ts
  2. backend/app/schemas/public_config.py
  3. backend/app/services/system_config_service.py
  4. frontend/lib/i18n/locales/zh/translation.json
  5. frontend/lib/i18n/locales/en/translation.json
  6. frontend/content/docs/getting-started/admin.mdx

新增主题不等于修改默认主题。没有单独产品决策时,未知值继续回退到 classic

3. 定义完整作用域

:root[data-theme-preset="theme_id"] {
  color-scheme: light;

  /* 主题私有色板 */
  --preset-bg-page: /* approved value */;
  --preset-bg-surface: /* approved value */;
  --preset-primary: /* approved value */;

  /* 基础语义映射 */
  --background: var(--preset-bg-page);
  --card: var(--preset-bg-surface);
  --primary: var(--preset-primary);

  /* 完整映射 tests/theme-presets.test.mjs 中的组件状态 Token */
}

:root[data-theme-preset="theme_id"].dark {
  color-scheme: dark;

  /* 覆写 Dark 中不同的色板和语义值 */
}

不要复制某个页面的 className 作为主题实现,也不要在共享组件中加入新主题名称。

4. 先验证契约,再看页面

至少补充:

  • 新主题 ID 能被前后端接受。
  • 未知值仍回退到 Classic。
  • Light/Dark 作用域同时存在。
  • Primary、Muted、Disabled、Status 和实底前景映射正确。
  • componentStateTokens 全部存在。
  • 共享组件中没有主题 ID。
  • 后台保存成功后才即时应用主题。

5. 灰度启用

由管理员在非生产环境选择新主题。严重问题可立即切回 classic,无需重建前端。不要用紧急逐页改色代替主题回滚。

6. 完成真实页面验收

主题视觉验收与踩坑手册执行:

  • Light/Dark 分开检查。
  • 用户侧、管理后台、公开页和动态详情页都覆盖。
  • Default、Hover、Selected、Focus、Disabled、Loading、Empty、Error 都有记录。
  • 桌面、中间宽度和窄屏都检查。

新页面与新功能流程

  1. 画出页面画布、一级 Surface、嵌套 Surface 和浮层关系。
  2. 列出组件需要的 Default、Hover、Selected、Focus、Disabled 和状态色。
  3. 优先选择现有 shadcn/ui、共享组件和 theme-* 语义类。
  4. 只有现有契约无法表达稳定角色时才新增 Token。
  5. 在所有已支持主题中检查页面;新页面默认同时考虑 Light/Dark。
  6. 确认主题切换没有改变布局、滚动、响应式或交互行为。
  7. 执行 Lint、主题契约测试和真实页面检查。

维护任务如果被明确限定为某个主题或模式,只修改该范围;仍需通过默认映射和静态检查确认其他范围没有被意外改变。

配色问题诊断流程

实际排查顺序:

  1. 记录当前 data-theme-preset.dark、路由和状态。
  2. 查看元素最终计算出的背景、前景、边框和阴影,不只看 className。
  3. 对照相同组件在另一个主题、模式或参考页面中的表现。
  4. 判断问题属于色板、基础语义、共享组件、项目语义还是业务例外。
  5. 修改最上游且最准确的层级。
  6. 复查目标页面、参考页面、Classic 和未修改模式。

修复时遵循:

先确认语义角色 → 再确认主题与模式 → 再选择 Token → 最后检查真实观感

不要从“这里需要一种蓝色”开始选色。

静态检查与验证

前端:

cd frontend
source ~/.nvm/nvm.sh
nvm use 22
pnpm lint
node --test tests/theme-presets.test.mjs

后端主题配置:

cd backend
uv run pytest tests/test_system_config_service.py tests/test_public_config_service.py

从仓库根目录检查可疑写法:

rg -n '#[0-9A-Fa-f]{3,8}' frontend --glob '*.{ts,tsx}'
rg -n '(text|bg|border)-(red|blue|green|yellow|orange|purple)-' frontend --glob '*.{ts,tsx}'
rg -n 'data-theme-preset|new_steel|classic' frontend/features frontend/components --glob '*.tsx'

搜索结果需要逐条判断。图表数据、第三方控件和明确的品牌资产可能是合法例外;不要为了让结果归零而机械替换。

当前路由从源码动态盘点,不在文档中维护容易过时的固定清单:

rg --files frontend/app | rg '/page\.tsx$'

New Steel 最终基线

原始设计标准是设计输入;经过真实页面调优后,最终实现以 frontend/app/theme-presets.css 为准。

Light

角色最终值
Page / Surface / Subtle#F8FBFF / #FFFFFF / #F5F5F5
Hover / Selected#EBF1FF / #EBF1FF
Primary / Hover / Active / Subtle#3370FF / #5C8DFF / #295ACC / #EBF1FF
Text Primary / Secondary / Muted / Disabled#333333 / #666666 / #999999 / #CCCCCC
Border / Strong Border#E6E6E6 / #CCCCCC
Success / Warning / Danger#3DD598 / #FF995E / #FC5A5A
Primary、Danger、Status 实底前景#FFFFFF

Dark

角色最终值
Page / Surface / Subtle#091022 / #1F2935 / #192340
Hover / Selected#1F2A4D / #293867
Primary / Hover / Active / Subtle#5C8DFF / #85A9FF / #3370FF / #1F2A4D
Text Primary / Secondary / Muted / Disabled#FFFFFF / #D6DAE6 / #ADB5CD / #5C6B9A
Border / Strong Border#334681 / #5C6B9A
Primary / Status 实底前景#141C34
Danger 实底前景#FFFFFF

功能色默认使用“弱背景 + 功能色前景”。实底只用于少量强状态,并必须使用对应实底前景。

完成定义

新主题完成

  • 前后端 ID、白名单、回退、i18n 和管理员配置已同步。
  • Light/Dark 基础语义和组件状态 Token 完整。
  • 共享组件不感知主题 ID。
  • Primary、Danger、Status、文字层级、Hover、Selected 和 Disabled 符合设计。
  • 主题测试、后端配置测试、Lint 和差异检查通过。
  • 真实页面、动态详情、浮层、桌面和窄屏完成验收。
  • 在线文档和已知限制已同步。

页面或配色优化完成

  • 问题修复在正确语义层级,没有逐页复制补丁。
  • 目标主题和模式观感自然,状态之间可辨认。
  • Classic、其他主题和未修改模式没有回归。
  • 没有借配色修改布局。
  • 组件状态、响应式、键盘焦点和可访问名称仍然成立。
  • 已留下可复用的 Token、共享组件规则或经验记录。

On this page