# 主题系统全面改造(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 手工清单 |