主题视觉验收与踩坑手册
Poco 多主题页面的审美基线、问题诊断经验、验收矩阵和回归记录模板。
本文是主题系统与前端配色开发手册的实践配套,记录 New Steel 从设计、接入到全页面调优过程中真正暴露的问题。
它不要求所有主题长得一样,而是确保每套主题都满足同一组视觉关系:
- 页面画布、内容 Surface、嵌套区域和浮层层级清楚。
- Default、Hover、Selected、Active、Focus 不会互相混淆。
- 品牌色有识别度但不过量。
- 功能色和文字前景符合语义。
- 切换主题只改变视觉,不改变布局和行为。
审美基线
1. Surface 要稳定,强调色要克制
Light 中,独立卡片、公共页头、表格、历史消息区、Dialog 和工具执行区域通常需要稳定 Surface。不要让它们透出页面画布,也不要把全部区域机械染成主题色。
主题色优先用于:
- 主按钮、链接和可点击图标。
- 当前导航、Selected 和 Focus。
- 小面积身份、状态或强调标记。
主题色不应直接铺满:
- 大面积业务卡片 Hover。
- 整个表头或公共页头。
- 密集 Badge 列表。
- 普通正文和说明文字。
2. Hover 是反馈,不是换一个页面层级
Hover 应让用户知道“这里可交互”,但不能:
- 变得和 Selected 几乎一样。
- 变得接近页面画布,像卡片突然消失。
- 使用过重阴影,像 Popover。
- 改变尺寸、内边距、排列或文本换行。
中小面积导航、选项和表格行可以使用主题 Hover 背景;大卡片更适合保持 Surface,通过边框、轻阴影和小幅抬升反馈。
3. 默认状态保持安静
静态卡片默认使用中性边框。主题色边框只用于 Hover、Selected、Focus 或明确业务状态。
如果所有卡片默认都有彩色边框,页面会持续处于“已选择”或“需要注意”的视觉噪声中。MCP 卡片最终与 Skills 卡片统一:默认中性边框,Hover 才出现弱主题色边框。
4. 文字层级必须可读
- 主文字负责内容。
- Secondary 负责可连续阅读的说明。
- Muted 负责时间、ID 和元信息。
- Disabled 只负责真正不可用的控件。
不要把所有非标题文字都降成 Muted。视觉“轻”不等于可读性低。
5. 实底必须配套前景
蓝色、红色、绿色等实底上的文字和图标必须使用对应 Solid Foreground。尤其是危险按钮,Light/Dark 都需要单独确认;不能因为 Dark 的某个前景可用,就让 Light 出现红底黑字。
6. 默认主题是产品资产
新增主题不是重做全站。Classic 的原有观感必须通过安全默认 Token 保留。任何新语义角色都先定义 Classic 等价值,再提供新主题映射。
7. 配色不承担布局职责
主题语义类只负责颜色、边框和必要阴影。公共头“缺背景”时只补背景,不能同时增加 Padding、圆角、Overflow 或改变 CardContent 结构。
诊断问题时先记录四件事
每次开始排查前先记录:
- 主题方案,例如
classic或new_steel。 - 模式,例如 Light 或 Dark。
- 路由和数据状态。
- 交互状态,例如 Default、Hover、Selected、Disabled。
同一个组件在不同组合下可能走不同变量。没有这四项,“这里颜色不对”很容易被修成另一个范围的回归。
真实踩坑与正确处理
色板和前景
| 现象 | 根因 | 正确处理 | 回归检查 |
|---|---|---|---|
| 蓝色主按钮明显过深 | --primary 误用了 Active 色 | 分离 Default、Hover、Active Token | 检查 Button 三态和链接 |
| 状态色整体发暗、发脏 | 为局部对比度把功能色混入深色 | 先遵守确认色板;确需调整则新增全局状态前景 | 检查 Badge、图标和图表是否被误改 |
| 红色按钮出现黑字 | Light 继承了不合适的实底前景 | 使用 --destructive-foreground,逐模式核对 | 删除、停用和失败操作 |
| 标签像一排主按钮 | Badge 使用品牌色实底 | 默认使用 Subtle 背景和功能色前景 | 密集状态列表和表格 |
| 页面所有灰字一个层级 | Secondary、Muted、Disabled 混用 | 按可读说明、元信息、禁用拆分 | 长说明、时间、Placeholder |
Surface 和透明背景
| 现象 | 根因 | 正确处理 | 回归检查 |
|---|---|---|---|
| 设置弹窗、公共页头像没有背景 | 独立区域沿用透明或 Page Background | 使用 Card/Popover 或对应 theme-surface-* | Dialog、固定头、滚动内容 |
| 历史消息区和工具卡片透底 | 工具步骤、输入输出只用了通用弱透明色 | 使用 theme-tool-step、theme-tool-payload | 折叠、展开、长 JSON |
| 文本产物预览混入页面画布 | Markdown 阅读区直接使用 Page Background | 使用 theme-artifact-text-preview 明确阅读表面 | 普通预览、沉浸预览和编辑预览 |
| 表格头、卡片头露出页面底色 | 头部没有自己的 Surface | 使用表头语义类,仅补颜色 | Padding、圆角、边框不能变化 |
| 卡片圆角边框在四角出现断线 | 不透明子 Surface 覆盖父级圆角边框 | 共享 Card 首尾区域继承外层圆角,不全局裁剪内容 | Header、Content、Footer |
| 表单弹窗内部卡片层级不自然 | Body、一级表单卡片和 Page 混用背景 | 使用 theme-form-dialog-body 与 theme-form-section | 新增和编辑弹窗 |
| 分享页没有主题识别度 | 全页只使用通用背景和文字 | 用分享页语义表达少量身份和状态强调 | Classic/Dark 保持各自风格 |
| 选中卡片反而接近页面底色 | bg-primary/5 与 Page 混合 | 使用稳定的 theme-card-selected | Light/Dark 都检查 |
Hover、边框和阴影
| 现象 | 根因 | 正确处理 | 回归检查 |
|---|---|---|---|
| 大卡片 Hover 变成大片蓝色 | 把导航级 --surface-hover 用在大卡片 | 保持 Surface,以边框和轻阴影表达 | 工作台、发布页、能力卡片 |
| Hover 后卡片像融入页面画布 | Hover 背景接近 Page Background | 使用 Card Surface 或稳定 Hover Token | 查看边界是否仍清楚 |
| Hover 阴影像浮层 | 卡片复用了 Popover 级 shadow-md | 使用卡片级阴影语义 | 与 Dialog、Popover 对比 |
| 所有 MCP 卡片默认彩色边框 | Enabled 被直接当作 Selected | 默认中性边框,Hover 才用弱主题色 | Disabled 和 Selected 不混淆 |
| 能力卡片 Hover 突然整块变蓝 | 通用 theme-hover 用于较大能力卡片 | 使用 theme-capability-card-hover | Classic 和 New Steel Dark 不回归 |
Tabs、导航和图标
| 现象 | 根因 | 正确处理 | 回归检查 |
|---|---|---|---|
| Inactive 与 Active Tab 太像 | List、Inactive、Hover、Active 共用近似背景 | 分别定义 --tab-* 状态 | 放在白色和非白色父背景上 |
| Inactive Tab 直接设白后消失 | 只针对当前父背景调色 | Inactive 与 List 分开建模 | Surface、Subtle 两种容器 |
| Tab 文字变色但图标不变 | 图标有独立固定颜色 | 图标使用 currentColor 或继承 Trigger | Lucide 图标与自带语义图标 |
| Badge 没跟随 Active | 状态选择器写在没有 data-state 的子元素上 | 使用 group-data-[state=active] | 数量 Badge、装饰图标 |
| 下划线 Tabs 变成块状标签 | 页面自己复制普通 Tabs 状态类 | 使用共享 variant="line" | 定时任务详情等导航 |
| 点击侧栏文字闪一下主题色 | :active 复用了彩色选中前景 | 使用独立 Pressed 前景 Token | Light/Dark、选中和未选中项 |
输入、共享组件和第三方控件
| 现象 | 根因 | 正确处理 | 回归检查 |
|---|---|---|---|
| Dark Composer 内出现一块异色输入背景 | 内层 Textarea 的 Dark 背景覆盖父级 Surface | 仅在组合输入中使用 bg-transparent dark:bg-transparent | 普通表单 Input 不能一起变透明 |
| Light 输入框全选后像没有选中 | 选择状态误用了过浅的 Accent/Hover 背景 | 使用独立 --text-selection-* 语义,并分别映射前景和背景 | Input、Textarea、组合输入框 |
| React Flow 等第三方控件出现亮色泄漏 | 第三方默认 CSS 不消费项目 Token | 在组件作用域映射 Card、Border、Accent | Light/Dark 控件按钮和面板 |
| 修复一个页面导致所有 Dialog 改变 | 全局共享组件被过度覆盖 | 判断问题是共享默认还是特定业务角色 | 全站常见 Dialog 冒烟 |
| 主题类生效但下一主题容易忘记 | 组件依赖主题专用选择器 | 建立项目语义 Token,并加入机器契约 | componentStateTokens 完整 |
默认主题和维护方式
| 现象 | 根因 | 正确处理 | 回归检查 |
|---|---|---|---|
| New Steel 正常,Classic 工具卡片变深 | 新语义没有提供 Classic 等价默认值 | 先还原 Classic 基线,再单独映射新主题 | 对照 main 或变更前截图 |
| 颜色修复顺手改变布局 | 用主题适配重写组件结构 | 把颜色语义和布局类分开修改 | 对比 DOM、间距和响应式 |
| 问题越修越多,形成补丁地狱 | 每个页面复制颜色覆盖 | 上移到 Token 或共享组件 | 搜索同类 className |
| 只检查首页就宣布完成 | 动态页、公开页、浮层使用不同组合 | 按页面类型和状态矩阵验收 | 保存覆盖记录 |
修复层级选择
| 观察结果 | 应修改的位置 |
|---|---|
| 同一主题内所有主按钮都错 | 主题基础 Token 或共享 Button |
| 所有主题的同一共享组件都错 | 共享组件 |
| 只有某主题的同一视觉角色错 | 该主题的项目语义映射 |
| 多个页面出现同类新角色 | 新增项目语义 Token 和 theme-* 类 |
| 仅一个业务状态确实特殊 | 业务组件的最小语义例外 |
禁止用以下方式绕过判断:
className={themePreset === "new_steel" ? "bg-white" : "bg-muted"}也不要把主题 ID 包装成 Tailwind 自定义变体后散落到页面。它解决了眼前选择器,却把下一主题的遗漏变成必然。
页面验收矩阵
不要维护固定路由清单。先从当前源码盘点,再按页面类型抽取有代表性的真实数据:
rg --files frontend/app | rg '/page\.tsx$'必查页面类型
- 登录页、首页、全局导航和用户设置。
- Agent 工作台、任务创建、临时对话。
- 历史消息、运行中会话、工具执行、产物和实时电脑区域。
- Skills、MCP、Plugins、定时任务、发布中心等能力页面。
- 管理后台的概览、表格、筛选、表单和详情。
- Agent、Conversation、Publish 等公开分享页。
- 具有真实 ID 的动态详情路由。
- 文档站本身。
必查表面
- Page、Card、Popover、Dialog、Sheet、Drawer。
- 公共页头、卡片头、表格头和表格正文。
- 历史消息区、工具步骤、输入输出载荷。
- 表单 Dialog Body、分组卡片、Input 和 Textarea。
- 空状态、加载骨架、错误提示和 Toast。
必查交互状态
- Primary:Default、Hover、Active、Focus、Disabled、Loading。
- Destructive:Default、Hover、Focus、Disabled。
- Card:Default、Hover、Selected、Disabled。
- Tabs:List、Inactive、Hover、Active、图标和 Badge。
- Table:Header、Row Hover、Selected、Empty、Loading。
- Success、Warning、Danger、Info:浅色和必要的实底状态。
- Sidebar:Default、Hover、Selected、Pressed、Focus。
必查模式和视口
新主题完整验收至少覆盖:
| 维度 | 基线 |
|---|---|
| 主题 | Classic、新增主题 |
| 模式 | Light、Dark |
| 桌面 | 1440 × 900 |
| 中间宽度 | 1024 × 768 |
| 窄屏 | 390 × 844 |
局部维护任务按用户指定范围执行,但必须确认未修改主题和模式没有被全局 Token 或共享组件意外影响。
稳定触发交互状态
| 状态 | 推荐方式 |
|---|---|
| Hover | 指针停留,观察背景、边框、前景和阴影 |
| Active / Pressed | 保持按下,确认不闪色、不位移 |
| Focus | 使用 Tab 键,不用鼠标点击代替 |
| Disabled | 保持必填项为空或使用不可用分页按钮 |
| Loading | 在非生产环境减慢请求,确认宽高稳定 |
| Empty | 使用无结果搜索词或专用测试数据 |
| Error | 仅在非生产环境阻断目标请求或使用失败记录 |
| Destructive Dialog | 打开后检查,验收时点击取消 |
无法稳定触发的状态标记“未覆盖”和原因,不能根据默认外观推断通过。
验收记录模板
| 路由/组件 | 主题 | 模式 | 视口 | 状态 | 结果 | 备注/截图 |
|---|---|---|---|---|---|---|
/zh/example | new_steel | Light | 1440 × 900 | Default、Hover | 通过/未通过 | 问题说明 |
每个问题至少记录:
现象:
主题 / 模式 / 路由 / 状态:
实际计算色:
期望语义角色:
修复层级:
回归范围:通过标准
- 页面画布与独立 Surface 层级清楚,没有无意透底。
- Primary Default 没有误用 Hover 或 Active 色。
- 实底按钮、状态和图标使用正确前景。
- Hover、Selected、Active、Pressed 可以区分且不改变布局。
- 大卡片 Hover 不会铺满过强主题色,也不会融入页面画布。
- Tabs 在不同父背景上仍能区分 Inactive 和 Active,图标同步状态。
- 表头、卡片头、Dialog、工具卡片和历史消息区都有稳定表面。
- 文字层级清晰,重要说明没有被误降为 Muted。
- Classic 和未修改模式保持原有观感。
- 没有硬编码颜色、主题 ID 判断或局部主题补丁泄漏到业务组件。
- 状态同时包含文字、图标或可访问名称。
- 桌面和窄屏没有新增溢出、错位或操作不可见。
何时停止继续调色
满足以下条件时应停止局部微调,回到设计或语义层重新确认:
- 同一状态在三个以上页面需要不同临时色。
- 只有继续加入透明度或
color-mix(...)才能勉强可见。 - Hover、Selected 和 Page Surface 已无法稳定区分。
- 为了配色开始修改布局。
- 修复当前主题必然破坏 Classic。
- 无法说明当前颜色代表的语义角色。
主题适配的目标不是让每个角落都带品牌色,而是让用户在长时间使用时感到稳定、清晰和舒适。