chore: add UI audits and reconcile release docs
This commit is contained in:
@@ -0,0 +1,77 @@
|
||||
/**
|
||||
* ui-tokens.mjs — UI token 审计共享库
|
||||
*
|
||||
* 以 src/styles/base.css 为唯一 palette / 字阶 / 半径来源,
|
||||
* 同时服务静态审计(ui-token-audit.mjs)与渲染审计(ui-rendered-audit.mjs)。
|
||||
*/
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { join } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
export const mobileRoot = join(fileURLToPath(new URL('.', import.meta.url)), '..', '..');
|
||||
export const tokenFile = join(mobileRoot, 'src', 'styles', 'base.css');
|
||||
|
||||
export function normalizeHex(hex) {
|
||||
let h = hex.toLowerCase();
|
||||
if (h.length === 4 || h.length === 5) {
|
||||
h = '#' + [...h.slice(1)].map((c) => c + c).join('');
|
||||
}
|
||||
return h;
|
||||
}
|
||||
|
||||
export function normalizeRgba(fn) {
|
||||
return fn.toLowerCase().replace(/\s+/g, '');
|
||||
}
|
||||
|
||||
/** 渲染侧 computed style 给出的是 rgb()/rgba() 字符串,统一规整;不透明时折叠为 hex */
|
||||
export function normalizeComputedColor(value) {
|
||||
const v = value.toLowerCase().replace(/\s+/g, '');
|
||||
const m = v.match(/^rgba?\((\d+),(\d+),(\d+)(?:,([\d.]+))?\)$/);
|
||||
if (!m) return v;
|
||||
const alpha = m[4] === undefined ? 1 : Number(m[4]);
|
||||
if (alpha === 0) return 'transparent';
|
||||
if (alpha === 1) {
|
||||
return '#' + [m[1], m[2], m[3]].map((n) => Number(n).toString(16).padStart(2, '0')).join('');
|
||||
}
|
||||
return v;
|
||||
}
|
||||
|
||||
export function hexToRgb(h) {
|
||||
return [1, 3, 5].map((i) => parseInt(h.slice(i, i + 2), 16));
|
||||
}
|
||||
|
||||
export function colorDist(a, b) {
|
||||
const [r1, g1, b1] = hexToRgb(a);
|
||||
const [r2, g2, b2] = hexToRgb(b);
|
||||
return Math.round(Math.sqrt((r1 - r2) ** 2 + (g1 - g2) ** 2 + (b1 - b2) ** 2));
|
||||
}
|
||||
|
||||
export const NEAR_MISS_MAX_DIST = 24; // RGB 欧氏距离 ≤24 视为"近色漂移",疑似 token 笔误
|
||||
|
||||
export function loadTokens() {
|
||||
const baseCss = readFileSync(tokenFile, 'utf8');
|
||||
|
||||
const palette = new Set(['transparent', 'currentcolor', 'inherit']);
|
||||
for (const m of baseCss.matchAll(/#[0-9a-fA-F]{3,8}\b/g)) palette.add(normalizeHex(m[0]));
|
||||
for (const m of baseCss.matchAll(/rgba?\([^)]*\)/g)) palette.add(normalizeRgba(m[0]));
|
||||
|
||||
const fontScale = new Set();
|
||||
for (const m of baseCss.matchAll(/font-size:\s*([\d.]+)px/g)) fontScale.add(Number(m[1]));
|
||||
|
||||
const radiusTokens = new Set();
|
||||
for (const m of baseCss.matchAll(/--[\w-]*radius[\w-]*:\s*([\d.]+)px/g)) radiusTokens.add(Number(m[1]));
|
||||
for (const m of baseCss.matchAll(/border-radius:\s*([\d.]+)px/g)) radiusTokens.add(Number(m[1]));
|
||||
|
||||
const paletteHexes = [...palette].filter((v) => v.startsWith('#') && v.length === 7);
|
||||
|
||||
function nearestPalette(hex) {
|
||||
let best = null;
|
||||
for (const p of paletteHexes) {
|
||||
const d = colorDist(hex, p);
|
||||
if (!best || d < best.dist) best = { value: p, dist: d };
|
||||
}
|
||||
return best;
|
||||
}
|
||||
|
||||
return { palette, paletteHexes, fontScale, radiusTokens, nearestPalette };
|
||||
}
|
||||
Reference in New Issue
Block a user