iter(g295): 新增表面阶梯审计并修正悬停底

This commit is contained in:
HIsheR
2026-10-26 23:35:57 +08:00
parent df40eb0073
commit 3533459e00
11 changed files with 105 additions and 23 deletions
+4 -5
View File
@@ -83,10 +83,9 @@ function parseColor(text) {
return { r: ch(parts[0]), g: ch(parts[1]), b: ch(parts[2]), a: parts[3] === undefined ? 1 : parseFloat(parts[3]) };
}
/*** 作用:求表达式的值:递归替换 var(--x),支持 alpha 变量与 color-mix 单层混色
* 只覆盖令牌文件里真实用到的语法(in srgb + 两个颜色 + 一个百分比),不做通用 CSS 求值器——
* 遇到认不出的表达式直接抛错,宁可红也不静默算错对比度。 ***/
function resolve(expr, vars, depth = 0) {
// 只覆盖令牌文件里真实用到的语法(in srgb + 两个颜色 + 一个百分比),不做通用 CSS 求值器——遇到认不出的表达式直接抛错,宁可红也不静默算错
/*** 作用:求表达式的值:递归替换 var(--x),支持 alpha 变量与 color-mix 单层混色 ***/
export function resolve(expr, vars, depth = 0) {
if (depth > 12) throw new Error(`令牌引用层级过深:${expr}`);
const text = expr.trim();
const color = parseColor(text);
@@ -167,7 +166,7 @@ function luminance(c) {
}
/*** 作用:两色对比度(1~21),前景先合成到底色上再算;半透明底由调用方压到页面底色上 ***/
function ratio(fg, bg) {
export function ratio(fg, bg) {
const solid = bg;
const f = over(fg, solid);
const l1 = Math.max(luminance(f), luminance(solid));
+47
View File
@@ -0,0 +1,47 @@
import { loadThemes, resolve, ratio } from './contrast-audit.mjs';
/*** 表面阶梯审计:同一条阶梯上相邻两档必须有肉眼可辨的亮度差
* —— 对比度门禁只管「文字 vs 底色」,管不到「底色 vs 底色」;而悬停底 / 按下底 / 凹陷底
* 全是底色对底色,差 1% 就等于用户看不出状态变化(历史实测表面→二阶表面仅 1.008)。 ***/
export const LADDER = [
{ label: '页面底 → 表面', from: 'bg', to: 'surface', min: 1.04 }, // 卡片与页底:靠边框也能分,阈值最低
{ label: '表面 → 悬停底', from: 'surface', to: 'surface-2', min: 1.04 }, // 悬停必须有反馈,低一档就白改
{ label: '悬停底 → 按下底', from: 'surface-2', to: 'surface-3', min: 1.04 }, // 按下要比悬停更实,两档不能糊在一起
{ label: '表面 → 凹陷底', from: 'surface', to: 'surface-inset', min: 1.02 }, // 凹陷区自带边框与内阴影,可放宽一档
];
/*** 作用:按四套主题(亮 / 暗 × 常规 / 高对比)逐档算亮度比,返回明细与未达标项 ***/
export function auditSurface(root) {
const themes = loadThemes(root);
const rows = [];
for (const [theme, vars] of Object.entries(themes)) {
for (const step of LADDER) {
const from = resolve(vars.get(`--ua-color-${step.from}`), vars);
const to = resolve(vars.get(`--ua-color-${step.to}`), vars);
const value = ratio(to, from);
rows.push({ theme, label: step.label, from: step.from, to: step.to, min: step.min, ratio: value, bad: value < step.min });
}
}
return { rows, failures: rows.filter((r) => r.bad) };
}
/*** 作用:按终端显示宽度补空格(中文占两格,padEnd 只按字符数补,表头会歪) ***/
const pad = (text, width) => {
const wide = [...text].reduce((n, ch) => n + (ch.codePointAt(0) > 0x2e80 ? 2 : 1), 0);
return text + ' '.repeat(Math.max(0, width - wide));
};
/*** 作用:把审计结果渲染成对齐的文本表(--json 之外的人读视图) ***/
export function formatSurfaceReport({ rows, failures }) {
const themes = [...new Set(rows.map((r) => r.theme))];
const lines = ['表面阶梯亮度比(1.00 = 完全看不出差别)', `${pad('阶梯', 22)}${themes.map((t) => t.padStart(14)).join('')} 阈值 结论`];
for (const step of LADDER) {
const cells = themes.map((t) => rows.find((r) => r.theme === t && r.label === step.label).ratio.toFixed(3).padStart(14));
const bad = rows.filter((r) => r.label === step.label && r.bad).map((r) => r.theme);
lines.push(`${pad(step.label, 22)}${cells.join('')}${String(step.min).padStart(6)} ${bad.length ? `FAIL(${bad.join('+')})` : 'OK'}`);
}
lines.push('');
lines.push(failures.length === 0 ? '表面阶梯全部达标' : `不达标 ${failures.length} 处:${failures.map((f) => `${f.theme}/${f.label}`).join(' / ')}`);
return lines.join('\n');
}