Files
prop-ai-hr/mobile-uni/scripts/lib/ui-tokens.mjs
T

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 };
}