fix(webui): 深色模式真的落地了 —— 之前 .dark 是「有意留空」的

用户报「深色模式可读性差」。实测(1280×800,读页面计算值)拿到两个数字:
  · .glass-card 合成成 rgb(237,237,237) 白卡,而其上 --c-gray-900 文字是
    rgb(243,245,248) ⇒ **1.07:1 的白底白字**
  · .nav-rail 合成成 rgb(188,189,190) 浅灰条,未选中文字只有 4.03:1

根因写在 index.css 自己的注释里:`.dark { /* 有意留空 */ }`。当年留空的理由
是「组件没有 dark: 变体,只换令牌会半深不浅」——**方向反了**:组件写的是语义
色阶(bg-white / text-gray-900),灰阶反转后本来就会自适应;真正没适配的是
**手写 CSS 里那几处硬编码白色**(.glass-card 的 0.92、--nav-bg 令牌),
它们不在 --c-* 色板里,所以「色板变量已全覆盖」的判据一直是绿的。

改动:
1. .glass-card / .nav-* 全部改走令牌,主题之间只差 alpha;
   深色给 --glass-card-a: 0.06 + --nav-bg: 30 35 44/0.72。
   遵守既有契约(background.test「玻璃是白色材料」):**基材恒为白**,
   只降 alpha,绝不换成深色层。
2. CalendarView 的非本月农历小字 text-gray-300 → gray-400/500。
   gray-300 在这套调色板里是**分隔线档**(全仓 68 处 border-gray-300、
   当文字只有 5 处),深色下它是 61,68,81,落在深卡上 1.49:1;
   而 gray-400 在深色 4.66:1、浅色白卡 2.54:1,两端都更好。
3. 判据:theme.test 补 3 条(手写 CSS 不许有绕过主题的硬编码白色表面 /
   深色卡片 alpha 必须降低 / **按实际 alpha 合成后**正文须 ≥4.5:1,
   即把 1.07:1 那个事故写成可计算的断言)。background.test 那条
   「.dark 里不许出现 --nav-*」**方向反转**为「必须有且底暗字亮」——
   它原来守的是「还没有深色主题」这个前提,其注释本就写明
   「真做深色主题时这条要一起改」。

验证(不是"改了就算"):
· 自建探针遍历 **9 个页面**(三个通信子页签 / 邮件详情 / 日历 / 联系人 /
  我的 / 写信 / 地址补全弹层):修复前通信 14 处、日历 74 处、
  联系人 13 处低于 WCAG AA;修复后 **9/9 页面 0 处**。
· 三条新判据逐条**变异自检**(还原修复即变红,且失败信息自带药方);
  浅色三个取色值与改动前**逐字节一致**(导航 0.72 白玻璃、卡片 0.92 白)⇒ 零回归。
· theme.test 34 通过 / background.test 44 通过 / tsc 干净。
· 已按 deploy/redeploy-gateway.sh 部署到 systemd 实例,后置清单自动项全绿,
  并在生产实例上复量 9 个页面(同为 0 处)+ 端到端发信(状态 read,非仅入库)。

#深色模式 #可读性 #WCAG
This commit is contained in:
2026-09-17 22:32:25 +08:00
parent 2166f0ed81
commit 99a2d7ad7e
4 changed files with 175 additions and 19 deletions

View File

@ -431,10 +431,37 @@ check('新组件未使用未映射色族', unmapped.length === 0, unmapped.join(
* C 这条模糊来源。真要堵时按文件窄豁免写,别写成"导航目录不许出现 dark:"。
*/
const darkBodies = [...css.matchAll(/\.dark\s*\{([^}]*)\}/g)].map(m => m[1]);
/*
* ★ 2026-09-17 **方向反过来了**(用户报「深色模式可读性差」,实测导航合成
* rgb(188,189,190) 而文字只有 4.03:1):
*
* 旧断言是「.dark 里**不许**出现 --nav-*」,它编码的是**当时还没有深色主题**
* 这个前提 —— 只把导航压深确实会比全浅更割裂。现在深色主题真做了(灰阶令牌
* 本来就已反转,缺的只是手写 CSS 里那几处硬编码白色),导航就必须跟着换深,
* 否则它就是深色界面上的一条**残留浅灰条**。
*
* 断言因此从「不许有」改为「必须有,且字要亮、底要暗」——保留原来的意图
* (防止只压深导航的割裂),但反过来守现在这个正确形态。
* 为什么不只断「有」:只断存在的话,把深色令牌写成浅色值也算过。
*/
const darkNavBlock = darkBodies.find(b => /--nav-bg/.test(b));
const navBgLum = (() => {
const m = darkNavBlock && darkNavBlock.match(/--nav-bg:\s*(\d+)\s+(\d+)\s+(\d+)/);
return m ? (Number(m[1]) + Number(m[2]) + Number(m[3])) / 3 : null;
})();
const navFgLum = (() => {
const m = darkNavBlock && darkNavBlock.match(/--nav-fg:\s*(\d+)\s+(\d+)\s+(\d+)/);
return m ? (Number(m[1]) + Number(m[2]) + Number(m[3])) / 3 : null;
})();
check(
'导航深色来源之一已堵:.dark 令牌块里不许出现 --nav-*(另两条路见上方注释,未覆盖)',
darkBodies.length >= 1 && darkBodies.every(b => !/--nav-/.test(b)),
'.dark 令牌块里出现了 --nav-* —— 这条只堵这一条路(`.dark .nav-rail{}` 作用域与组件 dark: 变体都没堵,见上方注释)'
'深色主题里导航必须换深(否则是深色界面上残留的一条浅灰条)',
navBgLum !== null && navBgLum < 90,
navBgLum === null ? '.dark 里没有 --nav-bg —— 深色下导航仍是白玻璃(实测 4.03:1)' : `--nav-bg 平均亮度 ${navBgLum}(应 <90)`
);
check(
'深色导航的文字是亮字(底暗字亮,不是深底深字)',
navFgLum !== null && navFgLum > 180,
navFgLum === null ? '缺 --nav-fg' : `--nav-fg 平均亮度 ${navFgLum}(应 >180)`
);
check(
'导航自叠模糊之一已堵:壁纸模式下 .nav-rail 规则里不许写 backdrop-filter(元素级工具类那条路未覆盖)',