78 lines
2.7 KiB
JavaScript
78 lines
2.7 KiB
JavaScript
/**
|
|
* 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 };
|
|
}
|