From 99a2d7ad7e968689a014d3267f12262bff17c84c Mon Sep 17 00:00:00 2001 From: JianFeeeee Date: Thu, 17 Sep 2026 22:32:25 +0800 Subject: [PATCH] =?UTF-8?q?fix(webui):=20=E6=B7=B1=E8=89=B2=E6=A8=A1?= =?UTF-8?q?=E5=BC=8F=E7=9C=9F=E7=9A=84=E8=90=BD=E5=9C=B0=E4=BA=86=20?= =?UTF-8?q?=E2=80=94=E2=80=94=20=E4=B9=8B=E5=89=8D=20.dark=20=E6=98=AF?= =?UTF-8?q?=E3=80=8C=E6=9C=89=E6=84=8F=E7=95=99=E7=A9=BA=E3=80=8D=E7=9A=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 用户报「深色模式可读性差」。实测(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 --- .../electron/src/components/CalendarView.tsx | 10 ++- client/electron/src/index.css | 77 +++++++++++++++---- client/electron/test/background.test.mjs | 33 +++++++- client/electron/test/theme.test.mjs | 74 ++++++++++++++++++ 4 files changed, 175 insertions(+), 19 deletions(-) diff --git a/client/electron/src/components/CalendarView.tsx b/client/electron/src/components/CalendarView.tsx index 31a62ff..89aa237 100644 --- a/client/electron/src/components/CalendarView.tsx +++ b/client/electron/src/components/CalendarView.tsx @@ -571,7 +571,15 @@ function MonthGrid({ 不显示农历人无法确认「每月十五」到底落在哪一格 */} {cellLunarLabel(d)} diff --git a/client/electron/src/index.css b/client/electron/src/index.css index 867504a..2428c13 100644 --- a/client/electron/src/index.css +++ b/client/electron/src/index.css @@ -263,6 +263,13 @@ * 三档从实到透:正文面(--bg-glass) > 嵌套卡(--bg-glass-inner) > 控件(--bg-glass-control)。 */ --bg-glass-control: 0.5; + --glass-card-a: 0.92; + --glass-card-hover-a: 0.99; + --glass-card-wall-a: 0.78; + --glass-card-wall-hover-a: 0.88; + /* 卡片边框:浅色用深色细线,深色用亮线(见 .dark,深色下层次靠边框不靠阴影)。 */ + --hairline-fg: 15 23 42 / 0.07; + --hairline-fg-strong: 15 23 42 / 0.12; /* 面板的朦胧感(与壁纸自身的 --bg-blur 分开:那层给照片打底,这层给面板) */ --bg-blur-panel: 10px; /* 浮动面板之间的缝隙:壁纸从缝隙里露出来,是圆角化玻璃化的关键 */ @@ -1410,6 +1417,7 @@ html.view-switch .pane-rise { * 我上一轮为了"白字对比度"把它做成深玻璃,那是**用错误的方式解决对比度**, * 真正的割裂就在这儿。深色底只应该是深色主题下的样子。 * +/* * 所以导航底色/文字都走令牌: * - 浅色主题:白玻璃(0.72)+ 深字; * - 深色主题(.dark):深玻璃(0.72)+ 亮字。 @@ -1439,15 +1447,46 @@ html.view-switch .pane-rise { * 而不是先把导航单独压深。 */ /* - * 深色主题:**这一块有意留空**。 + * 深色主题 —— **2026-09-17 落地**(用户报「深色模式可读性差」)。 * - * 两件事必须一起做,否则就是"半深不浅"(我在导航上已经栽过一次): - * 1. 组件侧补齐 `dark:` 变体(现在一个都没有 —— 这是真正的深色主题工作); - * 2. 玻璃只降 `--glass-base` 的 alpha(保持白色基材),绝不去换基材颜色。 - * 在 1 完成之前,2 单独生效只会让"面变深、字还是深色",读不了。 + * 这里原先是有意留空的,理由是「组件侧没有 dark: 变体,只换令牌会半深不浅」。 + * 实测证明那个担心方向反了:组件写的是**语义色阶**(`bg-white` / `text-gray-900`), + * 灰阶反转后它们自动适配;真正没适配的是**手写 CSS 里那几处硬编码白色**。 + * + * 实测(深色、1280×800,读页面计算值): + * .nav-rail 合成 rgb(188,189,190) 浅灰条;--nav-fg-muted 文字只有 4.03:1 + * .glass-card 合成 rgb(237,237,237) 白卡;--c-gray-900 文字 1.07:1(白底白字) + * 也就是「深色下整片白」—— 这正是可读性差的来源。 + * + * 修法遵守既有契约(background.test.mjs「玻璃是白色材料」):**基材不改** + * (--glass-base 仍只有一处定义、仍是白),主题之间只差 **alpha** —— + * 深色下把白的 alpha 压到 0.06,让深底透上来;文字令牌本来就已反转。 */ .dark { - /* 有意留空 */ + /* + * 导航:深玻璃 + 亮字。 + * + * 这里曾有一条判据禁止 `.dark` 出现 `--nav-*`(背景是**当时还没有深色主题** + * ⇒ 只压深导航就会「导航黑、正文白」,比全浅更割裂)。那条判据的注释写明 + * 「真做深色主题时,这条要连同 dark: 变体一起改」—— 本行即那个时刻, + * 判据已同步改成断言这套深色令牌**存在且对比度达标**。 + */ + --nav-bg: 30 35 44 / 0.72; + --nav-fg: 236 240 246; + --nav-fg-muted: 148 158 175; + --nav-hover-bg: 255 255 255 / 0.08; + --nav-active-bg: 30 58 95; + --nav-active-fg: 154 191 250; + --nav-border: 255 255 255 / 0.08; + + /* 卡片面:白基材只降 alpha(0.92 → 0.06),深底便透上来。 */ + --glass-card-a: 0.06; + --glass-card-hover-a: 0.1; + --glass-card-wall-a: 0.1; + --glass-card-wall-hover-a: 0.14; + /* 深色下靠「边框亮于底」而不是阴影来分层。 */ + --hairline-fg: 255 255 255 / 0.09; + --hairline-fg-strong: 255 255 255 / 0.16; } .nav-rail { @@ -1524,29 +1563,37 @@ html[data-bg='on'] .comm-pane > .bg-white { .glass-card { border-radius: var(--radius-card); - border: 1px solid rgb(15 23 42 / 0.07); - background-color: rgb(255 255 255 / 0.92); + border: 1px solid rgb(var(--hairline-fg)); + /* + * 基材**恒为白**,主题之间只差 alpha(--glass-card-a)。 + * + * 这里原先写死 `rgb(255 255 255 / 0.92)` —— 深色下它就是一块白卡, + * 而文字走的是反转后的近白令牌 ⇒ 实测 1.07:1 的白底白字。 + * 写死白色的地方,深色主题一律改不成 —— 走令牌才有得改。 + */ + background-color: rgb(var(--glass-base) / var(--glass-card-a)); transition: background-color 150ms, border-color 150ms; } .glass-card:hover { - background-color: rgb(255 255 255 / 0.99); - border-color: rgb(15 23 42 / 0.12); + background-color: rgb(var(--glass-base) / var(--glass-card-hover-a)); + border-color: rgb(var(--hairline-fg-strong)); } html[data-bg='on'] .glass-card { - background-color: rgb(var(--glass-base) / 0.78); - border-color: rgb(var(--glass-base) / 0.5); + background-color: rgb(var(--glass-base) / var(--glass-card-wall-a)); + border-color: rgb(var(--hairline-fg)); } html[data-bg='on'] .glass-card:hover { - background-color: rgb(var(--glass-base) / 0.88); + background-color: rgb(var(--glass-base) / var(--glass-card-wall-hover-a)); } /* * (这里曾有 `.dark .glass-card { background: rgba(255,255,255,0.06) }`。 - * 它是按"深色主题已经存在"写的 —— 而实际上组件还没有 dark 变体, - * 这条只会让卡片在浅色页面上几乎消失。等真深色主题落地再一起打开。) + * 它当年被撤掉的理由是「组件还没有 dark 变体,这条只会让卡片在浅色页面上 + * 几乎消失」—— 2026-09-17 实测确认组件侧其实是自洽的(灰阶已反转), + * 缺的正是这一档 alpha,于是它现在以 `--glass-card-a` 的形态回来了。) */ diff --git a/client/electron/test/background.test.mjs b/client/electron/test/background.test.mjs index f1cd2e6..73516cb 100644 --- a/client/electron/test/background.test.mjs +++ b/client/electron/test/background.test.mjs @@ -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(元素级工具类那条路未覆盖)', diff --git a/client/electron/test/theme.test.mjs b/client/electron/test/theme.test.mjs index c7cef32..ab72c88 100644 --- a/client/electron/test/theme.test.mjs +++ b/client/electron/test/theme.test.mjs @@ -422,6 +422,80 @@ check( secondaryContrast.toFixed(2) ); +console.log(`\n主题:${pass} 通过${fail ? `,${fail} 失败` : ''}`); +/* + * 31) ★ 深色下的**卡片面**必须跟着主题走,不能是写死的白色。 + * + * 2026-09-17 用户报「深色模式可读性差」。根因不是灰阶没反转(那些令牌一直是 + * 对的),而是**手写 CSS 里几处硬编码的白色**没进 `.dark`: + * - `.glass-card { background-color: rgb(255 255 255 / 0.92) }` + * - `--nav-bg`(导航令牌) + * 实测(1280×800,读计算值):“.glass-card” 合成成 rgb(237,237,237), + * 而它上面的 `--c-gray-900` 文字是 rgb(243,245,248) ⇒ **1.07:1 的白底白字**; + * 导航合成 rgb(188,189,190),未选中文字 4.03:1。 + * + * 为什么旧判据全绿而界面是坏的:theme.test 只扫 `--c-*` 色板变量, + * 而这两处根本不在色板里 —— **“变量都覆盖了”不等于“颜色都走变量了”**。 + * 所以这一条扫的是**整个非注释 CSS 里还有没有裸的不透明白色表面**。 + */ +const cssForHardcode = css.replace(/\/\*[\s\S]*?\*\//g, ''); +// 只扫**表面**(background),不扫 border/color: +// 白边框(如 .narrow-nav 里那条已被下一条覆盖的 rgb(255 255 255/.12)) 不会 +// 造成白底白字,而把它算进来只会逼出“为了让判据变绿而改无关代码”。 +// 白框里的注释已在上面切掉(源码里的中文说明不影响)。 +const hardcodedWhite = [...cssForHardcode.matchAll(/^\s*background(?:-color)?\s*:\s*[^;]*rgb\(\s*255\s+255\s+255[^;]*;/gm)] + .map(m => m[0].trim()) + .filter(line => !/var\(--glass-base\)/.test(line)); +check( + '手写 CSS 里没有绕过主题的硬编码白色表面', + hardcodedWhite.length === 0, + hardcodedWhite.length ? `${hardcodedWhite.length} 处:${hardcodedWhite.slice(0, 3).join(' | ')}` + : '白色表面一律走 --glass-base / 令牌的 alpha 档' +); +check('★ 判据自检:硬编码白色必须判红', + /^\s*background-color:\s*rgb\(255 255 255/.test(' background-color: rgb(255 255 255 / 0.92);')); + +/* + * 32) 深色下玻璃卡的 alpha 必须真的降下来(否则白色基材还是白的)。 + * + * 与 background.test 的「玻璃是白色材料」是**一对**,不矛盾: + * 那边守「基材恒为白」(不许换成深色层),这边守「深色下 alpha 要降」 + * (不许维持浅色的 0.92)。两条合起来才是用户要的「深色下透出深底的白玻璃」。 + */ +/* + * 取值工具:`lum()` 只认 `--c-*` 前缀,而 alpha 令牌是 `--glass-card-a`, + * 所以单给一个“取任意数值令牌”的函数,不把前者改宽(改宽会让所有 + * 现有 `lum(block,'gray-400')` 这类调用的语义变模糊)。 + */ +const numOf = (block, name) => { + const m = block.match(new RegExp(`--${name}:\\s*([\\d.]+)`)); + return m ? Number(m[1]) : null; +}; +const cardA = numOf(darkBlock, 'glass-card-a'); +const lightCardA = numOf(lightBlock, 'glass-card-a'); +check( + '深色下卡片面的 alpha 已降低(白基材不再是 0.92)', + cardA !== null && lightCardA !== null && cardA < 0.3 && lightCardA > 0.7, + `light=${lightCardA} dark=${cardA}` +); + +/* + * 33) 深色下每个不透明卡片面都能承载正文:用实际 alpha 合成后, + * `--c-gray-900`(正文色)对卡面必须 ≥ 4.5:1。 + * 这是把 1.07:1 那个具体事故写成可计算的断言,而不是只断言「变量存在」。 + */ +const overOn = (fg, a, bg) => [0, 1, 2].map(k => Math.round(fg[k] * a + bg[k] * (1 - a))); +const darkPage = rgbOf(darkBlock, 'gray-50'); +const darkBody = rgbOf(darkBlock, 'gray-900'); +const glassWhite = [255, 255, 255]; +const darkCardComposite = darkPage && cardA !== null ? overOn(glassWhite, cardA, darkPage) : null; +const darkTextContrast = darkCardComposite && darkBody ? contrast(darkBody, darkCardComposite) : 0; +check( + '深色卡片面上正文达到 4.5:1(1.07:1 白底白字那个事故)', + darkTextContrast >= 4.5, + `合成卡面 rgb(${darkCardComposite ? darkCardComposite.join(',') : '?'}) 对比度 ${darkTextContrast.toFixed(2)}` +); + console.log(`\n主题:${pass} 通过${fail ? `,${fail} 失败` : ''}`); /* * 机器可读的汇总(契约):`run-all.mjs` 只认这一行来判"这条判据到底跑了几条"。