用户两句话:
「那你为什么不把白字换成黑字或者自动反色或者描边呢?」
「部分动画十分不合理,会导致页面大范围的闪动,且不能让人自然的把注意力集中在
将要出现的页面上」
## ① 导航:他说得对,我之前是用错误的方式解决对比度
这个应用是**浅色底 + 深字**,导航却是全页唯一一块黑的 —— 那才是"割裂"。我上一轮
为了"白字对比度"把它做成深玻璃,等于**把一块地方永久压黑**来回避问题。
现在导航底色/文字全部走令牌,按主题切换:
浅色:白玻璃 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 条全绿(含"导航走令牌 + 两套令牌都在")。
90 lines
4.1 KiB
JavaScript
90 lines
4.1 KiB
JavaScript
/**
|
||
* 导航配色验收:浅色主题 = 白玻璃 + 深字;深色主题 = 自动反色。
|
||
*
|
||
* 用户(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;
|