Regression / regression (push) Canceled after 0s
## 品牌标识(本次会话) 起因:品牌此前没有任何图形标识 —— 唯一 favicon 是内联 data-URI 里的字母「A」, 那是 v2.0.0「Aurora Admin → Kole UI」改名漏掉的一处(PC 顶栏也是「A」, 移动端站已是「K」;移动端文档站则完全没有 favicon)。 - 几何:24 网格三个互不接触的笔画(竖 + 两斜),圆头描边; 描边 2.25 → 16px 标签页尺寸下正好 1.5px = 规范原文「描边1.5px」 - 取色分两套(刻意):favicon 硬编码品牌蓝/白(渲染在浏览器标签栏,不继承 kole-dark); 顶栏标记走 currentColor(实测暗色下自动转 rgb(20,22,28)) - 新增 theme-color 双条(light #FFFFFF / dark #1C1F26,取 --kole-color-card-bg) - 修 site/app.js hero 标语 KOLE ADMIN → KOLE UI(改名变形残留) - 移动端 7 个模板补 favicon(此前计数 0) 验收:门禁 9 条全 OK(site-routing/site-routes/mobile-docs/mobile-site/isolation/ theme/nav/i18n/icons);PC 回归 1464/1464 · 移动端 807/807,各连跑 8 次一致; 两端 favicon 405 字节逐字节一致;PC 站控制台错误 1→0。 ## 并行会话成果(本次一并入库) - 图标系统:2576 图标(TDesign/Element Plus,MIT)+ 11 端注入 + 5 个构建门禁工具 + IconPreview 预览页 + ICON-SPEC.md 冻结规格 - 移动端平台:47 组件 × 6 端 + 文档站 53 页 + 隔离门禁 - PC 组件:103 个大后台组件 / 组件11 批次 - uni-app:PC 端试点 + 移动端端实现 + 真实编译验证 ## 工程 - .gitignore 补 .scratch/ 与 .zcode-preexisting-*.txt(会话中间产物,实测 9.1MB,不入库) - CHANGELOG 补品牌标识条目 - ROADMAP 登 S8-P4(品牌标识任务包 + og:image/apple-touch-icon 未做部分)
92 lines
4.1 KiB
JavaScript
92 lines
4.1 KiB
JavaScript
/**
|
||
* contrast-probe.mjs — 站点 chrome 的对比度探针(浏览器内测量,零依赖)。
|
||
*
|
||
* 判据:正文 ≥4.5:1(WCAG 1.4.3 AA),大字(≥24px 或 ≥18.66px 粗体)与图标字形 ≥3:1(1.4.11)。
|
||
* 半透明背景沿父链合成后再算比值 —— 品牌浅底(`--kole-color-brand-bg`)就是半透明的,
|
||
* 不合成会算错(实测差异可达 1 个数量级)。
|
||
*
|
||
* 用途:`tools/verify-dark.mjs`(暗色 7 条路由)与 `tools/verify-theme.mjs`(亮/暗双模式)
|
||
* 共用同一套判据,避免两处各写一份、改一处漏一处。
|
||
*/
|
||
export const PROBE_SCOPE = '#sidebar *, #content *, .topbar *';
|
||
|
||
export async function probeContrast(page, { scope = PROBE_SCOPE, limit = 600 } = {}) {
|
||
return page.evaluate(
|
||
({ scopeSel, maxNodes }) => {
|
||
const bad = [];
|
||
const toRGBA = (s) => {
|
||
const m = (s || '').match(/rgba?\(([^)]+)\)/);
|
||
if (!m) {
|
||
if (/^#([0-9a-fA-F]{6})$/.test(s)) {
|
||
const c = s.slice(1);
|
||
return [parseInt(c.substr(0, 2), 16), parseInt(c.substr(2, 2), 16), parseInt(c.substr(4, 2), 16), 1];
|
||
}
|
||
return null;
|
||
}
|
||
const p = m[1].split(',').map((x) => parseFloat(x));
|
||
return [p[0], p[1], p[2], p.length > 3 ? p[3] : 1];
|
||
};
|
||
const lumOf = (rgb) => {
|
||
const f = (v) => { v /= 255; return v <= 0.03928 ? v / 12.92 : Math.pow((v + 0.055) / 1.055, 2.4); };
|
||
return 0.2126 * f(rgb[0]) + 0.7152 * f(rgb[1]) + 0.0722 * f(rgb[2]);
|
||
};
|
||
const compBg = (el) => {
|
||
let acc = null, p = el, d = 0;
|
||
while (p && d < 10) {
|
||
const rgba = toRGBA(getComputedStyle(p).backgroundColor);
|
||
if (rgba) {
|
||
if (!acc) acc = rgba.slice();
|
||
else {
|
||
const a = rgba[3] + acc[3] * (1 - rgba[3]);
|
||
acc = [
|
||
(rgba[0] * rgba[3] + acc[0] * acc[3] * (1 - rgba[3])) / (a || 1),
|
||
(rgba[1] * rgba[3] + acc[1] * acc[3] * (1 - rgba[3])) / (a || 1),
|
||
(rgba[2] * rgba[3] + acc[2] * acc[3] * (1 - rgba[3])) / (a || 1),
|
||
a
|
||
];
|
||
}
|
||
if (acc[3] >= 0.999) break;
|
||
}
|
||
p = p.parentElement; d++;
|
||
}
|
||
return acc && acc[3] > 0 ? lumOf(acc) : null;
|
||
};
|
||
const isIcon = (t, cls) =>
|
||
/^[\s\u2000-\u3300\u2190-\u21FF\u2600-\u27BF\u2B00-\u2BFF×✕✓⚠⌄⌃‹›«»]+$/.test(t) ||
|
||
/\b(icon|arrow|caret|close|clear|toggle|sort|cross|star-|sep|bullet|chevron|indicator)\b/i.test(cls || '');
|
||
let checked = 0;
|
||
for (const el of document.querySelectorAll(scopeSel)) {
|
||
if (checked > maxNodes) break;
|
||
const cs = getComputedStyle(el);
|
||
if (cs.display === 'none' || cs.visibility === 'hidden' || parseFloat(cs.opacity) === 0) continue;
|
||
const txt = (el.textContent || '').trim();
|
||
if (!txt) continue;
|
||
let direct = false;
|
||
for (const n of el.childNodes) if (n.nodeType === 3 && /\S/.test(n.nodeValue)) direct = true;
|
||
if (!direct) continue;
|
||
const rect = el.getBoundingClientRect();
|
||
if (rect.width === 0 || rect.height === 0) continue;
|
||
if (el.hasAttribute('disabled') || el.getAttribute('aria-disabled') === 'true') continue;
|
||
const fg = toRGBA(cs.color);
|
||
const bg = compBg(el);
|
||
if (!fg || bg === null) continue;
|
||
const r = (Math.max(lumOf(fg), bg) + 0.05) / (Math.min(lumOf(fg), bg) + 0.05);
|
||
const fs = parseFloat(cs.fontSize) || 14;
|
||
const fw = parseInt(cs.fontWeight, 10) || (cs.fontWeight === 'bold' ? 700 : 400);
|
||
const large = fs >= 24 || (fs >= 18.66 && fw >= 700);
|
||
const min = isIcon(txt, el.className) ? 3.0 : large ? 3.0 : 4.5;
|
||
checked++;
|
||
if (r < min) {
|
||
bad.push(
|
||
el.tagName.toLowerCase() + '.' + String(el.className || '').trim().split(/\s+/)[0] +
|
||
' "' + txt.slice(0, 10) + '" ' + r.toFixed(2) + ':1<' + min
|
||
);
|
||
}
|
||
if (bad.length >= 5) break;
|
||
}
|
||
return { checked, bad };
|
||
},
|
||
{ scopeSel: scope, maxNodes: limit }
|
||
);
|
||
}
|