18 KiB
主题系统全面改造(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. 目标与非目标
目标
- 三套内置浅色主题下,终端与应用全部文字达到 WCAG AA(正文 ≥4.5:1,大字/图形 ≥3:1),以脚本验证。
- 浅色主题不再刺眼(柔化底色,见 §5.3)。
- 深色主题视觉零回归(语义色深色分支逐值对齐现状)。
- 建立语义 token 层 + fail-closed 防护测试,杜绝硬编码颜色再次积累。
- 主题切换时全 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 失败模式
- 白底白字:
terminal.css:243,312的color:#ffffff+ 半透明白底、#8a8a8a级灰字在浅色下 < 3.5:1。 - 暗底语法色硬上浅底:语义色全部取自 GitHub 暗色系(
#3fb950对白底 2.5:1)。 - 深色构件不随主题:搜索面板/工具条深色底在浅色窗口里成黑块(观感问题,可接受项,但颜色值需收敛进变量才可调)。
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)
-
浅色调色板重做(
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),调色板具体数值在实现期以脚本为准微调,本设计只锁规则与阈值。
- 规则:16 色逐个过对比度脚本,<4.5:1 的用
-
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,无死角。 -
drawBoldTextInBrightColors:浅色 false、深色 true(维持现状),两处同改。 -
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),改内置主题颜色无兼容负担;想要旧色的用户可在编辑器里以旧值建副本。 getThemeByIdfallback(DEFAULT_DARK)不变。- 纯渲染层改动,无 IPC/settings 结构变更;
settings.terminal.themeId语义不变。
6. 测试与验收
6.1 新增测试(fail-closed,沿用仓库表驱动风格)
tests/theme-contrast.mjs:直接import '../src/shared/theme.ts'(Node 22.18+ 原生类型剥离,同 terminal-cwd 等三测试的既有做法)。对每套内置主题:计算全部 16 色对 background 的对比度,浅色主题(isLight=true)每色 ≥4.5,深色主题仅断言 foreground ≥4.5(不约束暗色系既定美学)。另覆盖isLightTheme判定表。tests/css-theme-vars.mjs:扫描src/renderer/src全部.css/.tsx/.ts,正则提取色字面量,白名单外一律 exit 1;var()内 fallback 必须命中受控常量清单。接入package.jsontest脚本链(纯文件扫描,无需 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. 待拍板问题(交叉审核请逐条确认)
- 语义变量命名空间
--ot-*(提案)还是并入现有--chrome-*? - GitHub Light 柔化目标
#f6f8fa是否接受? - 深色主题是否也统一开 MCR 4.5(提案:维持 1,最小改动)?
var()fallback 是否按 §7 收敛为受控清单(提案:是,M4 落地)?- One Half Light / Solarized Light 底色维持不动(提案)?
- 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 手工清单 |