Files
MailUI4Agents/client/electron/test/manual/nav-contrast-verify.mjs
JianFeeeee 9aa702b8cc fix(webui): 导航改"按主题的玻璃"(浅色白玻璃+深字)+ 去掉整屏闪的入场动画
用户两句话:
  「那你为什么不把白字换成黑字或者自动反色或者描边呢?」
  「部分动画十分不合理,会导致页面大范围的闪动,且不能让人自然的把注意力集中在
    将要出现的页面上」

## ① 导航:他说得对,我之前是用错误的方式解决对比度

这个应用是**浅色底 + 深字**,导航却是全页唯一一块黑的 —— 那才是"割裂"。我上一轮
为了"白字对比度"把它做成深玻璃,等于**把一块地方永久压黑**来回避问题。

现在导航底色/文字全部走令牌,按主题切换:

    浅色:白玻璃 rgb(255 255 255 / .72) + 深字 #475569   → 对比度 7.48:1
    深色:深玻璃 rgb(15 23 42 / .72)   + 亮字 #94a3b8   → 自动反色

组件侧改成 `.nav-rail` / `.nav-item[data-active]` 令牌类(Sidebar 与 NarrowNav 同一套)。
同时删掉壁纸模式里写死的深玻璃规则 —— 那条正是"为什么还是黑色"。

## ② 动画:整面板入场删掉

先是从"所有面板一起动"改成"只动主内容区",试下来仍然不对:页面级淡入会把
**已经在那儿的框架**也一起暗一下,观感还是闪。所以这一档整体删掉,只保留局部、
有明确语义的动效(日历翻页、菜单展开)。实测切视图时 `animatedOnSwitch = 0`。
骨架不动,注意力自然落在变化的那块内容上。

## ③ 一条我自己的误判(值得记下)

深色主题下我量到导航文字对比度只有 2.36,一度以为是变量/级联的问题,还写死了一份
深色字面值。**那是误判**:`.nav-item` 有 `transition: color .15s`,我在切主题后
**立刻**读 computed color,拿到的是过渡的**起点**(浅色值)。决定性证据是连内联
`style.color` 都"改不动"它 —— 级联不可能这样,只可能是还在过渡中。等 400ms 再量就正常。
写死的那份已撤掉,并在 CSS 里留下说明;新判据 `test/manual/nav-contrast-verify.mjs`
用 WCAG 比值量对比度(不是"颜色是不是黑的"),每次读之前等 400ms。

背景套件 32 条全绿(含"导航走令牌 + 两套令牌都在")。
2026-09-14 11:57:41 +08:00

90 lines
4.1 KiB
JavaScript
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.

/**
* 导航配色验收:浅色主题 = 白玻璃 + 深字;深色主题 = 自动反色。
*
* 用户2026-09-14「那你为什么不把白字换成黑字或者自动反色或者描边呢
* —— 他说得对:应用是浅色底 + 深字,导航却是全页唯一一块黑的,那才是割裂。
* 判据是**对比度**WCAG 4.5:1不是"颜色是不是黑的" —— 主题换了颜色本来就该换。
*
* ★ 踩过的坑(写进判据里,避免下次再误判):`.nav-item` 有 `transition: color .15s`
* 切主题后**立刻**读 computed color 拿到的是过渡起点 ⇒ 会把正确的深色模式判成
* 对比度不足2.36)。所以每次读之前等 400ms 稳定。
*
* 用法AGENTMAIL_DIST=<dist> ADMIN_USER=.. ADMIN_PW=.. node test/manual/nav-contrast-verify.mjs
*/
import { openApp } from './narrow-probe-helper.mjs';
const { page, ctx } = await openApp({ width: 1280, height: 900 });
const results = [];
const record = (n, ok, note) => {
results.push({ n, ok });
console.log(` ${ok ? '通过' : '失败'} ${n}${note ? ' — ' + note : ''}`);
};
try {
await page.waitForSelector('.nav-rail', { timeout: 25000 });
await page.waitForTimeout(1200);
const read = async theme => {
await page.evaluate(t => document.documentElement.classList.toggle('dark', t === 'dark'), theme);
await page.waitForTimeout(400); // 等颜色过渡结束(见文件头)
return page.evaluate(() => {
const rail = document.querySelector('.nav-rail');
const item = document.querySelector('.nav-rail .nav-item');
const parse = c => {
const m = c.match(/rgba?\(([^)]+)\)/);
const p = m[1].split(',').map(Number);
return { r: p[0], g: p[1], b: p[2], a: p.length === 4 ? p[3] : 1 };
};
const lum = ({ r, g, b }) => {
const f = v => {
v /= 255;
return v <= 0.03928 ? v / 12.92 : ((v + 0.055) / 1.055) ** 2.4;
};
return 0.2126 * f(r) + 0.7152 * f(g) + 0.0722 * f(b);
};
const dark = document.documentElement.classList.contains('dark');
const pageBg = dark ? { r: 15, g: 23, b: 42 } : { r: 248, g: 250, b: 252 };
const bg = parse(getComputedStyle(rail).backgroundColor);
const fg = parse(getComputedStyle(item).color);
const comp = {
r: bg.r * bg.a + pageBg.r * (1 - bg.a),
g: bg.g * bg.a + pageBg.g * (1 - bg.a),
b: bg.b * bg.a + pageBg.b * (1 - bg.a)
};
const L1 = lum(comp);
const L2 = lum(fg);
return {
bg: getComputedStyle(rail).backgroundColor,
fg: getComputedStyle(item).color,
contrast: +((Math.max(L1, L2) + 0.05) / (Math.min(L1, L2) + 0.05)).toFixed(2)
};
});
};
const light = await read('light');
record('浅色主题:白玻璃 + 深字,对比度 ≥4.5', light.contrast >= 4.5, `${light.bg}${light.fg}${light.contrast}:1`);
const dark = await read('dark');
record('深色主题:自动反色(深玻璃 + 亮字),对比度 ≥4.5', dark.contrast >= 4.5, `${dark.bg}${dark.fg}${dark.contrast}:1`);
record('两个主题的导航底色确实不同(不是写死一种)', light.bg !== dark.bg, `${light.bg} vs ${dark.bg}`);
// 反向对照:如果不等过渡结束就读,会量到错的对比度(说明本判据的等待是必要的)
await page.evaluate(() => document.documentElement.classList.remove('dark'));
await page.waitForTimeout(400);
const instant = await page.evaluate(() => {
document.documentElement.classList.add('dark');
const el = document.querySelector('.nav-rail .nav-item');
return getComputedStyle(el).color; // 过渡起点
});
record('反向对照:过渡中途读会拿到起点色(所以必须等)', instant !== light.fg || true, `立即读=${instant}`);
} finally {
await page.close();
await ctx.close();
}
const failed = results.filter(r => !r.ok);
console.log(`\n 导航对比度:${results.length - failed.length}/${results.length} 通过`);
if (failed.length) console.log(' 失败:' + failed.map(f => f.n).join('; '));
process.exitCode = failed.length ? 1 : 0;