Files
OpenTerminal/docs/superpowers/specs/2026-10-16-theme-overhaul-design.md
T

256 lines
18 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 主题系统全面改造(A+B+C)设计文档
- 日期:2026-10-16
- 状态:待交叉审核(未实现)
- 范围:浅色主题可读性(A)+ 底色柔化(B)+ 全量 CSS/TSX 颜色变量化(C)
- 关联:`docs/ROADMAP.md`、AGENTS.md「主题机制」章节(M4 需同步更新)
## 1. 背景与问题
用户主诉:白色主题下整体刺眼,终端内大量文字看不清(截图为 GitHub Light 下运行 Claude Code 的实际效果)。
### 1.1 根因清单
| # | 根因 | 位置 | 影响 |
|---|------|------|------|
| R1 | 内置浅色主题 ANSI 调色板是暗底色直接搬来的 | `src/shared/theme.ts` | 终端内 16 色大半不可读 |
| R2 | `drawBoldTextInBrightColors: true` 写死 | `TerminalView.tsx:864` | 粗体强制跳到 bright 变体,放大 R1 |
| R3 | `minimumContrastRatio` 仅背景图模式开 4.5,平时恒 1 | `TerminalView.tsx:869,1268` | xterm 渲染时对比度兜底完全关闭 |
| R4 | antd 写死 `darkAlgorithm` | `main.tsx:227` | 浅色下弹层/边框/hover 按暗色派生 |
| R5 | CSS/TSX 194 处真硬编码颜色(另有 122 处 var() 兜底值) | 见 §3 清单 | 白底白字、灰字对比不足、深色面板突兀 |
| R6 | GitHub Light 纯 `#ffffff` 铺满终端+chrome+antd 表面 | `theme.ts:298` | 刺眼 |
| R7(外部) | Claude Code 等 TUI 经 ConPTY 拿不到真实底色,按暗色出图 | 应用侧,不可控 | 只能靠 R3 的渲染时兜底 |
### 1.2 关键实测数据(WCAG 对比度)
GitHub Light(bg `#ffffff`)16 色中 **10 个 < 3:1**:brightWhite 1.00(白字白底)、brightYellow 1.06、brightCyan 1.24、brightGreen 1.38、white 1.69、yellow 1.85、brightMagenta 2.26、green 2.41、brightRed 2.75、brightBlue 3.89。仅 fg/black/blue/red/brightBlack 达到 4.5。
One Half Light(bg `#fafafa`)ANSI 15 色中 **7 个 < 3:1**(white 1.00、brightWhite 1.04、brightYellow 1.65、brightGreen 1.93、brightBlue 2.26、brightCyan 2.27、brightMagenta 2.82),**13 个 < 4.5:1**(另含 red/green/yellow/cyan/brightRed 3.0–3.5);仅 magenta、brightBlack 达标(fg/black 同为 `#383a42` 达标)。
Solarized Light 未逐色测,其 bright 系列为 Solarized 体系的去饱和色,预计大部分达标;`white #eee8d5` vs bg `#fdf6e3` 明显不足,需纳入统一修复。
## 2. 目标与非目标
**目标**
1. 三套内置浅色主题下,终端与应用全部文字达到 WCAG AA(正文 ≥4.5:1,大字/图形 ≥3:1),以脚本验证。
2. 浅色主题不再刺眼(柔化底色,见 §5.3)。
3. 深色主题视觉零回归(语义色深色分支逐值对齐现状)。
4. 建立语义 token 层 + fail-closed 防护测试,杜绝硬编码颜色再次积累。
5. 主题切换时全 UI(终端、chrome、antd 弹层)一致跟随。
**非目标**
- 不新增主题/柔化强度设置项(YAGNI)。
- 不改关键词高亮引擎的着色算法(`highlightThemeColors` 跟随主题机制已存在,默认关,维持现状;预设颜色本身在浅色下的对比度由 R3 兜底,不单独重调)。
- 不动 `node_modules` 内 xterm.css。
- 不新增内置主题。
## 3. 现状盘点
### 3.1 硬编码分布(raw = 规则内真硬编码;fallback = `var(x, 兜底)` 内的值)
| 文件 | raw | fallback | 备注 |
|------|----:|----:|------|
| connections.css | 26 | 1 | 最大真硬编码户 |
| workspace.css | 22 | 42 | fallback 多为 `--chrome-*` 旧兜底 |
| terminal.css | 20 | 0 | 含白底隐形文字(:243/:312 `#ffffff` on 白)与深色工具条 |
| sftp.css | 18 | 17 | |
| global.css | 16 | 0 | |
| theme/chrome.ts | 16 | 0 | 混色锚点 `#000`/`#fff`,属算法参数,入白名单 |
| monitor.css | 15 | 12 | |
| settings.css | 11 | 25 | |
| MonitorPanel.tsx | 10 | 0 | 内联语义色 |
| TerminalView.tsx | 10 | 0 | 搜索高亮色 + 背景图深底衬 `#0d1117`(AGENTS.md 铁律,保留) |
| HighlightTab.tsx | 9 | 0 | 预设规则色板,属数据非 chrome,入白名单 |
| commands.css | 8 | 0 | |
| highlight.css | 6 | 18 | |
| ErrorBoundary.tsx | 2 | 0 | |
| lock.css | 2 | 7 | |
| main.tsx | 2 | 0 | antd token 的 mix() 失败兜底(暗色偏置,需顺手修正) |
| highlightEngine.ts | 1 | 0 | 规则默认色,属数据,入白名单 |
| **合计** | **194** | **122** | |
### 3.2 颜色族归类(按出现频次)
| 族 | 代表值 | 处数级 | 去向 |
|----|--------|--------|------|
| 中性灰文本 | `#cccccc` `#8a8a8a` `#9a9a9a` `#6a6a6a` `#d0d0d0` | ~120(含 fallback) | 文本层级变量 |
| GitHub 暗色语法色 | `#3fb950` `#f85149` `#d29922` `#58a6ff` `#e3b341` `#f0a55a` `#3fd06a` | ~70 | 语义色变量 |
| 白色半透明叠加 | `rgba(255,255,255,.04–.2)` | ~20 | hover/overlay 变量 |
| 深色面板 | `rgba(24,26,31,.85)` `#101418` `#181818` | 少量 | surface 变量 |
| 滚动条 | `rgba(125,145,170,.5)` | 4 | scrollbar 变量 |
| 阴影 | `rgba(0,0,0,.4–.55)` | 少量 | shadow 变量 |
### 3.3 失败模式
1. **白底白字**:`terminal.css:243,312` 的 `color:#ffffff` + 半透明白底、`#8a8a8a` 级灰字在浅色下 < 3.5:1。
2. **暗底语法色硬上浅底**:语义色全部取自 GitHub 暗色系(`#3fb950` 对白底 2.5:1)。
3. **深色构件不随主题**:搜索面板/工具条深色底在浅色窗口里成黑块(观感问题,可接受项,但颜色值需收敛进变量才可调)。
## 4. 总体架构
```
ThemeColors (L0 主题原始色, theme.ts)
└─ chrome.ts (L1 派生: --chrome-* 现有 8 个 + 新增 L2 语义变量 --ot-*)
├─ global.css / 各 *.css 消费语义变量(本改造主体)
├─ main.tsx ThemedConfigProvider(按亮度切 antd algorithm)
└─ semantic.ts(TS 常量,供 TSX 内联样式与 chrome.ts 共用单一来源)
```
- 亮度判定抽为共享纯函数 `isLightTheme(colors)`(chrome.ts 内已有 `chromeIsLight` 私有逻辑,抽出导出,配表驱动测试)。
- 语义变量一律带 fallback 写法(`var(--ot-text-muted, #8a8a8a)`),fallback 值收敛到受控常量清单(见 §7)。
## 5. 设计细节
### 5.1 L2 语义变量(新增,`--ot-` 前缀)
| 变量 | 用途 | 深色分支(=现值,零回归) | 浅色分支 |
|------|------|--------------------------|----------|
| `--ot-text` | 正文 | 主题 foreground | 混黑加深至 ≥7:1 |
| `--ot-text-secondary` | 次要文本 | fg 78% 透明(现 `color-mix` 值) | 同式,校验 ≥4.5:1 |
| `--ot-text-muted` | 弱化文本(`#8a8a8a` 族) | `#8a8a8a` | `mix(bg,#000,0.55)` 级,≥4.5:1 |
| `--ot-surface` | 面板底 | 现 `color-mix(fg 4–6%, bg)` | 同式反向 |
| `--ot-surface-raised` | 浮层/工具条底 | `rgba(24,26,31,.85)` 等 | `rgba(255,255,255,.9)` 级 |
| `--ot-hover` | 控件/行 hover 叠加 | `rgba(255,255,255,.04–.1)` | `rgba(0,0,0,.04–.1)` |
| `--ot-border` / `--ot-border-strong` | 边框 | 现 `--chrome-border` 族 | 同式 |
| `--ot-success`(+`-fg`) | 成功/运行态(`#3fb950` 族) | `#3fb950`(fg 亮化) | 加深变体 ≥3:1(fg ≥4.5:1) |
| `--ot-danger`(+`-fg`) | 危险(`#f85149` 族) | 同上策略 | 同上 |
| `--ot-warning`(+`-fg`) | 警告(`#d29922`/`#e3b341`) | 同上策略 | 同上 |
| `--ot-info`(+`-fg`) | 信息/链接(`#58a6ff`/`#3b8eea`) | 同上策略 | 同上 |
| `--ot-scrollbar` / `--ot-scrollbar-active` | 滚动条 | 现 `rgba(125,145,170,*)` | `rgba(0,0,0,.25/.4)` 级 |
| `--ot-shadow` | 浮层阴影 | `rgba(0,0,0,.4–.55)` | `rgba(0,0,0,.12–.2)` |
| `--ot-focus-ring` | 焦点环 | 现 `rgba(59,142,234,.18)` | 同值(蓝环两底均可读) |
规则:
- 语义色**不是**深浅共用一个值:深色分支锁定现值(零回归),浅色分支用 `mix(现值, #000, t)` 自动加深并经对比度脚本校验,t 在 `semantic.ts` 里按色相手调一次后固化。
- `--tab-accent` 机制不动(已有 `-fg` 明暗适配)。
- `:root` 同步写 `color-scheme: dark | light`(chrome.ts 按分支写入):原生滚动条、表单控件、`::selection` 等跟随明暗,避免变量化了的部分被原生控件的不跟随出卖。
### 5.2 A:终端画布可读性(M0)
1. **浅色调色板重做**(`theme.ts` 三套):
- 规则:16 色逐个过对比度脚本,<4.5:1 的用 `mix(原值, #000, t)` 加深至达标(保留色相,t 固化进表);`white`/`brightWhite` 在浅色主题一律重映射为中灰(提案:github-light brightWhite → `#57606f`,其余主题按各自 fg 深度取灰),**不允许任何颜色与背景对比 < 4.5:1**。
- 达标用新增 `tests/theme-contrast.mjs` 固化(见 §7),调色板具体数值在实现期以脚本为准微调,本设计只锁规则与阈值。
2. **`minimumContrastRatio`(MCR)动态化**——按「背景图 × 主题亮度」矩阵取值:
| 场景 | 现值 | 新值 |
|------|------|------|
| 背景图模式(任意主题) | 4.5 | **4.5(不变,铁律)** |
| 无图 · 浅色主题 | 1 | **4.5** |
| 无图 · 深色主题 | 1 | **1(不变)** |
构造(`TerminalView.tsx:869`)与热更(`:1268`)两处同步改;拼写用 `minimumContrastRatio`(AGENTS.md 已记 `minContrastRatio` 静默失效的坑)。此项是对 R7(应用输出 arbitrary truecolor 低对比色)的唯一渲染时兜底,浅色必须开。已验证(2026-10-16,node_modules 实查):WebGL 渲染器自带完整对比度执行路径(`addon-webgl.mjs` 的 `_getMinimumContrastColor` → `ensureContrastRatio`,含 dim 的 halfContrastCache),且 `drawBoldTextInBrightColors`/`minimumContrastRatio` 变更会随 options 同步进 webgl 配置;`rendererMode` 默认 `auto`(webgl 优先、dom 回退),两条路径均执行 MCR,无死角。
3. **`drawBoldTextInBrightColors`**:浅色 false、深色 true(维持现状),两处同改。
4. **antd algorithm**(`main.tsx`):按 `isLightTheme` 切 `defaultAlgorithm`/`darkAlgorithm`——**antd(含 v6)没有 `lightAlgorithm`,浅色就是 `defaultAlgorithm`**(已在 node_modules/antd/es/theme/index.js 核实导出仅有 default/dark/compact 三种);`colorBgContainer`/`colorBgElevated` 的 mix() 失败兜底改为按亮度取 `#f5f5f5`/`#1f1f1f` 两套。
### 5.3 B:底色柔化(M0)
- GitHub Light `background #ffffff` → `#f6f8fa`(GitHub 官方 light canvas 色),`cursorAccent` 同步;其余 15 色按 §5.2.1 重算。
- One Half Light `#fafafa`、Solarized Light `#fdf6e3` 本身带色调,**不动**(避免拍脑袋改两套成熟主题的观感)。
- 不加「柔化强度」设置。
### 5.4 C:清扫批次(M1–M3,每批独立提交、独立可回滚)
映射规则(机械执行):
| 现值模式 | 替换为 |
|----------|--------|
| `#8a8a8a`/`#9a9a9a`/`#6a6a6a` 文本 | `var(--ot-text-muted, …)` |
| `#cccccc`/`#d0d0d0` 文本 | `var(--ot-text, …)` 或 `-secondary`(按语境) |
| `color-mix(fg x%, …)` 文本 | `var(--ot-text-secondary/muted)` |
| `#3fb950` 族 | `var(--ot-success)` / `-fg` |
| `#f85149` 族 | `var(--ot-danger)` / `-fg` |
| `#d29922`/`#e3b341`/`#f0a55a` | `var(--ot-warning)` / `-fg` |
| `#58a6ff`/`#3b8eea`/`#3fd06a`(链接) | `var(--ot-info)` / `-fg` |
| `rgba(255,255,255,.04–.1)` 控件 hover | `var(--ot-hover)` |
| `rgba(255,255,255,.16–.2)` / 成块白色半透明面板 | `var(--ot-surface-raised)` / `var(--ot-border-strong)`(按语境:描边用后者) |
| `rgba(125,145,170,*)` | `var(--ot-scrollbar*)` |
| `rgba(0,0,0,.4+)` 阴影 | `var(--ot-shadow)` |
批次:
- **M1 基座**:`semantic.ts` + chrome.ts 写 L2 变量 + `isLightTheme` 导出 + global.css + terminal.css + TerminalView.tsx 内嵌色 + ErrorBoundary.tsx + main.tsx 兜底。
- **M2 主工作区**:workspace.css + connections.css + commands.css。
- **M3 功能面板**:sftp.css + monitor.css + MonitorPanel.tsx + settings.css + highlight.css + lock.css。
白名单(防护测试豁免,§7):**semantic.ts 自身**(它是语义色的定义处,扫描它等于扫描标尺);**theme.ts**(内置调色板是主题数据);**global.css 的 `:root` 静态兜底块**(chrome.ts 变量缺失时的暗色默认值);chrome.ts 混色锚点(`#000000`/`#ffffff`,属算法参数);TerminalView 的 xterm ITheme 搜索色(主题数据)、背景图深底衬 `#0d1117` 与 `#00000000` 透明色(铁律);HighlightTab.tsx/highlightEngine.ts 的规则色(数据)。扫描器必须**先剥离注释再匹配**(terminal.css:16、highlightEngine.ts:17 的命中都在注释里),且正则覆盖 3/4 位 hex(`#fff`)。
### 5.5 兼容性
- 用户自建 `customThemes` 不受影响(L2 从 ThemeColors 派生,无需迁移)。
- ThemeEditor 只能编辑/新建 customThemes(已核实 `ThemeEditor.tsx:70-74`),改内置主题颜色无兼容负担;想要旧色的用户可在编辑器里以旧值建副本。
- `getThemeById` fallback(DEFAULT_DARK)不变。
- 纯渲染层改动,无 IPC/settings 结构变更;`settings.terminal.themeId` 语义不变。
## 6. 测试与验收
### 6.1 新增测试(fail-closed,沿用仓库表驱动风格)
1. `tests/theme-contrast.mjs`:直接 `import '../src/shared/theme.ts'`(Node 22.18+ 原生类型剥离,同 terminal-cwd 等三测试的既有做法)。对每套内置主题:计算全部 16 色对 background 的对比度,**浅色主题(isLight=true)每色 ≥4.5,深色主题仅断言 foreground ≥4.5**(不约束暗色系既定美学)。另覆盖 `isLightTheme` 判定表。
2. `tests/css-theme-vars.mjs`:扫描 `src/renderer/src` 全部 `.css/.tsx/.ts`,正则提取色字面量,白名单外一律 exit 1;`var()` 内 fallback 必须命中受控常量清单。**接入 `package.json` `test` 脚本链**(纯文件扫描,无需 esbuild bundle)。
### 6.2 既有回归
`npm test`(pretest typecheck + 22 个离线测试)全绿。
### 6.3 手工验收清单(每批 M1–M3 + M0 各跑一次)
三套浅色主题 × {终端(含 Claude Code 实跑)、设置弹层/下拉/Tooltip、SFTP 面板、监控面板、锁屏(含 `.lock-screen-boot` 启动闪层)、命令面板、连接侧栏、zmodem 传输弹层};深色主题抽查同清单确认零观感变化;背景图模式抽查(深底衬铁律不破);运行中明暗主题热切换各一次(chrome/antd/xterm 三路同步)。
### 6.4 性能
`minimumContrastRatio=4.5` 生产先例即背景图模式(已在线上跑);实现时用诊断日志对比开关前后的渲染层 stall 事件与高频输出场景,数据记入 PR 描述。
## 7. 防护测试细则
- 扫描正则:`#[0-9a-fA-F]{3,8}\b` 与 `rgba?\(`;`var(...)` 内命中归 fallback 规则,其余归 raw 规则。
- 白名单按「文件 + 值」二元组维护在测试内(表驱动用例风格)。
- 受控 fallback 清单:`semantic.ts` 导出的常量表,测试同时校验「CSS 里出现的 fallback ⊆ 清单」与「清单无死值」双向一致。
- 里程碑 M4 收尾时把扫描基线清零:除白名单外 raw 计数必须为 0,此后新增硬编码直接红。
## 8. 里程碑与提交切分
| 里程碑 | 内容 | 提交 | 验收 |
|--------|------|------|------|
| M0 | §5.2 + §5.3(调色板、两开关、antd 算法、柔化)+ theme-contrast 测试 | 1 个 | 主诉立解;深色零回归 |
| M1 | 语义层基座 + global/terminal + TSX 内嵌 | 1 个 | 手工清单浅色全过 |
| M2 | workspace/connections/commands | 1 个 | 同上 |
| M3 | sftp/monitor/settings/highlight/lock | 1 个 | 同上 |
| M4 | css-theme-vars 测试 + 基线清零 + AGENTS.md「主题机制」重写 + docs/STATE.md 更新 | 1 个 | npm test 全绿含新测试 |
每个里程碑独立可回滚;M0 可单独先行发版验证效果。
## 9. 风险与缓解
| 风险 | 缓解 |
|------|------|
| WebGL 渲染器不执行 MCR | **已排除**(2026-10-16 实查 addon-webgl 源码,见 §5.2.2) |
| MCR=4.5 渲染性能 | 背景图模式生产先例;诊断日志实测;不行则浅色降 3.5 并记录取舍(4.5 仍为目标值) |
| 深色回归 | 语义色深色分支锁现值 + 每批手工清单深色抽查 + diff 审查 |
| 加深算法破坏色相辨识(如 red 混成棕) | t 按色相手调固化,不许纯算法一路推到底;脚本只做下限校验 |
| 用户依赖旧浅色配色 | 主题编辑器可建副本还原;CHANGELOG 注明 |
| 大批量 CSS 改动引入选择器/特异性事故 | 只改颜色声明值,不动布局/盒模型属性;AGENTS.md 已记 sftp grid 教训 |
## 10. 待拍板问题(交叉审核请逐条确认)
1. 语义变量命名空间 `--ot-*`(提案)还是并入现有 `--chrome-*`?
2. GitHub Light 柔化目标 `#f6f8fa` 是否接受?
3. 深色主题是否也统一开 MCR 4.5(提案:维持 1,最小改动)?
4. `var()` fallback 是否按 §7 收敛为受控清单(提案:是,M4 落地)?
5. One Half Light / Solarized Light 底色维持不动(提案)?
6. M0 是否单独先行发版,还是 M0–M4 一起发?
## 11. 对抗性审查记录(2026-10-16)
审查执行:独立审查子代理(环境故障未产出有效报告)+ 主会话逐项自行验证。以下发现已全部落入正文:
| # | 发现 | 级别 | 处置 |
|---|------|------|------|
| 1 | antd 无 `lightAlgorithm`,浅色应为 `defaultAlgorithm` | Important(真实错误) | §5.2.4 已改 |
| 2 | §5.2.2「深色 1(维持现状)」与背景图模式现状(深色+图=4.5)表述冲突 | Important | §5.2.2 改为显式矩阵 |
| 3 | WebGL 渲染器是否执行 MCR 未验证(承重假设) | Critical(假设) | **已实查源码排除**:addon-webgl 有完整对比度路径,§5.2.2 记录证据 |
| 4 | 两 xterm 选项运行时热设未验证 | Important(假设) | 已核实:options 同步进 webgl 配置;生产代码本就在热设 MCR |
| 5 | 防护测试扫描器会误伤 semantic.ts/theme.ts/global.css 兜底块自身 | Important | §5.4 白名单已显式枚举 |
| 6 | 扫描器须剥注释、覆盖 3/4 位 hex | Minor(实现细节) | §5.4 已写明 |
| 7 | One Half Light「9 个 <3:1」计数错误(实为 7 个) | Minor(数据错误) | §1.2 已改 |
| 8 | 原生控件(滚动条等)不随明暗 | Minor(遗漏) | §5.1 增 `color-scheme` |
| 9 | 锁屏启动闪层、zmodem 等面板浅色表现未入验收清单 | Minor | 已入 §6.3 手工清单 |