iter(g242): 新增令牌对比度门禁,修掉暗色主按钮与淡底强调字不达标
This commit is contained in:
@@ -10,7 +10,7 @@
|
||||
--ua-color-surface-3: color-mix(in srgb, var(--ua-color-text) 8%, var(--ua-color-bg)); /* 表面三阶:按下底、最深容器 */
|
||||
--ua-color-surface-inset: color-mix(in srgb, var(--ua-color-text) 5%, var(--ua-color-bg)); /* 凹陷区:输入框 / 代码块底 */
|
||||
--ua-color-text: #1c1e21; /* 主文本色:正文与标题共用 */
|
||||
--ua-color-text-muted: #6b7280; /* 辅助文本色:说明、占位、次要信息 */
|
||||
--ua-color-text-muted: #676d78; /* 辅助文本色:说明、占位、次要信息(比 #6b7280 略深:凹陷底上原值仅 4.39,过不了 AA 4.5) */
|
||||
--ua-color-text-secondary: #4b5158; /* 次级文本色:介于主文本与辅助文本之间 */
|
||||
--ua-color-text-faint: #9aa0a8; /* 三级文本色:禁用、极弱说明 */
|
||||
--ua-color-overlay: rgba(0, 0, 0, 0.45); /* 遮罩:抽屉 / 弹窗 / 命令面板等半透明底 */
|
||||
@@ -158,7 +158,7 @@
|
||||
--ua-color-scrollbar: rgba(255, 255, 255, 0.24); /* 暗色滚动条拇指:暗底上黑拇指完全看不见,改白色低透明度 */
|
||||
--ua-color-scrollbar-hover: rgba(255, 255, 255, 0.42); /* 暗色滚动条拇指悬停:亮度再升一档表示可拖拽 */
|
||||
--ua-color-highlight: rgba(255, 255, 255, 0.08); /* 暗色内高光:暗底上白高光刺眼,降到 8% 只留一道细光 */
|
||||
--ua-color-accent: #378add; /* 暗色强调色:提亮以保证暗底可读 */
|
||||
--ua-color-accent: #4a9aeb; /* 暗色强调色:提亮以保证暗底可读(原 #378add 压在白字/淡底上只有 3.92/4.44,过不了 AA) */
|
||||
--ua-color-accent-contrast: #042c53; /* 暗色强调色上的文字色:深色反白,维持对比 */
|
||||
--ua-color-accent-soft: color-mix(in srgb, var(--ua-color-accent) 82%, var(--ua-color-bg)); /* 暗色柔和强调色:与亮色同结构,混入暗底即自然降饱和 */
|
||||
--ua-color-tint-accent: color-mix(in srgb, var(--ua-color-accent) 14%, transparent); /* 暗色淡底标准档:暗底上淡底不可见,透明度升一档 */
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
"dark": "#e7e9ec"
|
||||
},
|
||||
"textMuted": {
|
||||
"light": "#6b7280",
|
||||
"light": "#676d78",
|
||||
"dark": "#888780"
|
||||
},
|
||||
"border": {
|
||||
@@ -35,7 +35,7 @@
|
||||
},
|
||||
"accent": {
|
||||
"light": "#185fa5",
|
||||
"dark": "#378add"
|
||||
"dark": "#4a9aeb"
|
||||
},
|
||||
"accentContrast": {
|
||||
"light": "#ffffff",
|
||||
|
||||
@@ -5,6 +5,7 @@ import path from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { spawnSync } from 'node:child_process';
|
||||
import { visualAudit } from '../lib/visual-audit.mjs';
|
||||
import { auditContrast, formatReport, PAIRS } from '../lib/contrast-audit.mjs';
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
|
||||
@@ -1569,6 +1570,33 @@ function commitSubjects() {
|
||||
return pairs;
|
||||
}
|
||||
|
||||
/*** 对比度审计:把令牌的「文字色 × 底色」配对算成 WCAG 比值,低于阈值即退出码 1
|
||||
* --json 出机器可读明细;--list 只列配对表(看一眼设计契约,不必读代码)
|
||||
*/
|
||||
function cmdAuditContrast(args) {
|
||||
if (args.includes('--list')) {
|
||||
for (const p of PAIRS) console.log(`${p.label}:--ua-color-${p.fg} on --ua-color-${p.bg},阈值 ${p.min}`);
|
||||
return;
|
||||
}
|
||||
let result;
|
||||
try {
|
||||
result = auditContrast(ROOT);
|
||||
} catch (err) {
|
||||
console.error(`audit:contrast 失败:${err.message}`); // 求值不了就红:静默跳过会让人误以为全站达标
|
||||
process.exit(1);
|
||||
}
|
||||
if (args.includes('--json')) {
|
||||
const pairs = result.rows.map((r) => ({
|
||||
label: r.label, fg: r.fg, bg: r.bg, min: r.min,
|
||||
light: Number(r.light.toFixed(2)), dark: Number(r.dark.toFixed(2)), bad: r.bad,
|
||||
}));
|
||||
console.log(JSON.stringify({ ok: result.failures.length === 0, total: pairs.length, failing: result.failures.length, pairs }, null, 2));
|
||||
} else {
|
||||
console.log(formatReport(result));
|
||||
}
|
||||
process.exit(result.failures.length === 0 ? 0 : 1);
|
||||
}
|
||||
|
||||
/*** 迭代留痕校验:.ua/iterations.log 每行 JSON 字段与格式必须符合 ITERATION.md 规范 ***/
|
||||
function cmdLintIterations() {
|
||||
const file = path.join(INDEX_DIR, 'iterations.log');
|
||||
@@ -1704,10 +1732,13 @@ switch (cmd) {
|
||||
case 'audit:visual':
|
||||
visualAudit(args, ROOT);
|
||||
break;
|
||||
case 'audit:contrast':
|
||||
cmdAuditContrast(args);
|
||||
break;
|
||||
case 'mcp':
|
||||
cmdMcp();
|
||||
break;
|
||||
default:
|
||||
console.log('usage: ua <index|list|search <kw>|info <name>|docs|tokens [--json]|style|sync-counts|verify|doctor|commit-lint|lint-iterations|audit:visual [--json|--baseline]|changelog|mcp>');
|
||||
console.log('usage: ua <index|list|search <kw>|info <name>|docs|tokens [--json]|style|sync-counts|verify|doctor|commit-lint|lint-iterations|audit:visual [--json|--baseline]|audit:contrast [--json|--list]|changelog|mcp>');
|
||||
process.exit(cmd ? 1 : 0);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,220 @@
|
||||
/*** @ui-arch 对比度审计:把「文字色 × 底色」的语义配对算成 WCAG 对比度,做成可回归的门禁
|
||||
*
|
||||
* 为什么需要它:`audit:visual` 只保证「没有硬编码字面量」,令牌用对了但令牌本身可能对比度不够——
|
||||
* 改一个 --ua-color-text-muted 就能让全站辅助文字跌到 4.4,人眼在亮底上未必察觉,暗色下更看不出来。
|
||||
* 这里只做四步:解析令牌 → 求值(含 color-mix 混色)→ 按固定配对表算比值 → 与阈值比对。
|
||||
* 配对表写死在代码里(而不是外置配置):它是「设计契约」,加一条配对等于承认一种新的文字/底色组合,
|
||||
* 必须在评审里看见;阈值分三档(正文 7 / 常规文字 4.5 / 禁用态 2.6)对应 WCAG AA/AAA 与豁免口径。
|
||||
*/
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
|
||||
/*** 作用:令牌样式表相对仓库根的路径(唯一来源,勿在别处再写一份) ***/
|
||||
const TOKENS_REL = 'packages/design/tokens/tokens.css';
|
||||
|
||||
/*** 作用:文字色 × 底色配对表:label 人读、fg/bg 是令牌名、min 是 WCAG 阈值 ***/
|
||||
export const PAIRS = [
|
||||
{ label: '正文 / 页面底', fg: 'text', bg: 'bg', min: 7 },
|
||||
{ label: '正文 / 表面', fg: 'text', bg: 'surface', min: 7 },
|
||||
{ label: '正文 / 二阶表面', fg: 'text', bg: 'surface-2', min: 7 },
|
||||
{ label: '次级文字 / 页面底', fg: 'text-secondary', bg: 'bg', min: 4.5 },
|
||||
{ label: '次级文字 / 表面', fg: 'text-secondary', bg: 'surface', min: 4.5 },
|
||||
{ label: '辅助文字 / 页面底', fg: 'text-muted', bg: 'bg', min: 4.5 },
|
||||
{ label: '辅助文字 / 表面', fg: 'text-muted', bg: 'surface', min: 4.5 },
|
||||
{ label: '辅助文字 / 凹陷底', fg: 'text-muted', bg: 'surface-inset', min: 4.5 },
|
||||
{ label: '强调色 / 页面底', fg: 'accent', bg: 'bg', min: 4.5 },
|
||||
{ label: '主按钮文字 / 强调底', fg: 'accent-contrast', bg: 'accent', min: 4.5 },
|
||||
{ label: '正文 / 强调淡底', fg: 'text', bg: 'tint-accent', min: 4.5 },
|
||||
{ label: '强调文字 / 强调淡底', fg: 'accent', bg: 'tint-accent', min: 4.5 },
|
||||
{ label: '危险色 / 页面底', fg: 'danger', bg: 'bg', min: 4.5 },
|
||||
{ label: '成功色 / 页面底', fg: 'success', bg: 'bg', min: 4.5 },
|
||||
{ label: '警告色 / 页面底', fg: 'warning', bg: 'bg', min: 4.5 },
|
||||
{ label: '三级文字(禁用)/ 页面底', fg: 'text-faint', bg: 'bg', min: 2.6 },
|
||||
];
|
||||
|
||||
/*** 作用:读文件并归一换行,保证解析稳定 ***/
|
||||
const readText = (file) => fs.readFileSync(file, 'utf8').replace(/\r\n/g, '\n');
|
||||
|
||||
/*** 作用:截取某个 CSS 选择器块的块内文本(大括号配平,嵌套的 @media 也能正确闭合) ***/
|
||||
function blockOf(css, selector) {
|
||||
const start = css.indexOf(selector);
|
||||
if (start < 0) return '';
|
||||
const open = css.indexOf('{', start);
|
||||
if (open < 0) return '';
|
||||
let depth = 0;
|
||||
for (let i = open; i < css.length; i++) {
|
||||
if (css[i] === '{') depth += 1;
|
||||
else if (css[i] === '}') {
|
||||
depth -= 1;
|
||||
if (depth === 0) return css.slice(open + 1, i);
|
||||
}
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
/*** 作用:把块内 `--name: value;` 声明抽成表(同名后者覆盖前者,天然实现暗色覆盖) ***/
|
||||
function declsOf(block) {
|
||||
const map = new Map();
|
||||
for (const line of block.split('\n')) {
|
||||
const m = /^\s*(--[\w-]+)\s*:\s*([^;]+);/.exec(line);
|
||||
if (m) map.set(m[1], m[2].trim());
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
/*** 作用:解析单个颜色字面量(hex / rgb / rgba / transparent),解析不了返回 null ***/
|
||||
function parseColor(text) {
|
||||
const v = text.trim().toLowerCase();
|
||||
if (v === 'transparent') return { r: 0, g: 0, b: 0, a: 0 };
|
||||
const hex = /^#([0-9a-f]{3}|[0-9a-f]{6})$/.exec(v);
|
||||
if (hex) {
|
||||
const h = hex[1].length === 3 ? hex[1].split('').map((c) => c + c).join('') : hex[1];
|
||||
return { r: parseInt(h.slice(0, 2), 16), g: parseInt(h.slice(2, 4), 16), b: parseInt(h.slice(4, 6), 16), a: 1 };
|
||||
}
|
||||
const fn = /^rgba?\(([^)]+)\)$/.exec(v);
|
||||
if (!fn) return null;
|
||||
const parts = fn[1].split(/[,/]/).map((s) => s.trim()).filter(Boolean);
|
||||
if (parts.length < 3) return null;
|
||||
const ch = (s) => (s.endsWith('%') ? (parseFloat(s) * 255) / 100 : parseFloat(s));
|
||||
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) {
|
||||
if (depth > 12) throw new Error(`令牌引用层级过深:${expr}`);
|
||||
const text = expr.trim();
|
||||
const color = parseColor(text);
|
||||
if (color) return color;
|
||||
const mix = /^color-mix\(\s*in\s+srgb\s*,?\s*(.+)\)$/i.exec(text);
|
||||
if (mix) return evalMix(mix[1], vars, depth);
|
||||
const v = /^var\(\s*(--[\w-]+)\s*\)$/.exec(text);
|
||||
if (v) {
|
||||
if (!vars.has(v[1])) throw new Error(`未定义的令牌:${v[1]}`);
|
||||
return resolve(vars.get(v[1]), vars, depth + 1);
|
||||
}
|
||||
throw new Error(`无法求值的颜色表达式:${text}`);
|
||||
}
|
||||
|
||||
/*** 作用:把 color-mix 的参数切成「颜色 百分比」两段(颜色本身可能带括号,需按顶层逗号切) ***/
|
||||
function splitTop(text) {
|
||||
const out = [];
|
||||
let depth = 0;
|
||||
let cur = '';
|
||||
for (const ch of text) {
|
||||
if (ch === '(') depth += 1;
|
||||
if (ch === ')') depth -= 1;
|
||||
if (ch === ',' && depth === 0) {
|
||||
out.push(cur.trim());
|
||||
cur = '';
|
||||
continue;
|
||||
}
|
||||
cur += ch;
|
||||
}
|
||||
if (cur.trim()) out.push(cur.trim());
|
||||
return out;
|
||||
}
|
||||
|
||||
/*** 作用:按 CSS 规范做 srgb 混色(alpha 预乘),返回带 alpha 的颜色 ***/
|
||||
function evalMix(args, vars, depth) {
|
||||
const parts = splitTop(args);
|
||||
if (parts.length !== 2) throw new Error(`color-mix 需要两个颜色:${args}`);
|
||||
const one = (part) => {
|
||||
const m = /^(.*?)(?:\s+([\d.]+%|var\([^)]+\)))?$/.exec(part.trim());
|
||||
const color = resolve(m[1].trim(), vars, depth + 1);
|
||||
const raw = m[2];
|
||||
const pct = raw === undefined ? null : raw.startsWith('var(') ? parseFloat(resolvePct(raw, vars)) : parseFloat(raw);
|
||||
return { color, pct };
|
||||
};
|
||||
const a = one(parts[0]);
|
||||
const b = one(parts[1]);
|
||||
const pA = a.pct ?? (b.pct === null ? 50 : 100 - b.pct);
|
||||
const pB = b.pct ?? 100 - pA;
|
||||
const wa = pA / 100;
|
||||
const wb = pB / 100;
|
||||
const alpha = a.color.a * wa + b.color.a * wb;
|
||||
if (alpha === 0) return { r: 0, g: 0, b: 0, a: 0 };
|
||||
const ch = (k) => (a.color[k] * a.color.a * wa + b.color[k] * b.color.a * wb) / alpha;
|
||||
return { r: ch('r'), g: ch('g'), b: ch('b'), a: alpha };
|
||||
}
|
||||
|
||||
/*** 作用:求百分比变量(--ua-alpha-*),返回带 % 的字符串 ***/
|
||||
function resolvePct(expr, vars) {
|
||||
const v = /^var\(\s*(--[\w-]+)\s*\)$/.exec(expr.trim());
|
||||
if (!v || !vars.has(v[1])) throw new Error(`未定义的百分比令牌:${expr}`);
|
||||
return vars.get(v[1]).trim();
|
||||
}
|
||||
|
||||
/*** 作用:把半透明前景压到底色上(WCAG 只看合成后的实际观感) ***/
|
||||
function over(fg, bg) {
|
||||
if (fg.a >= 1) return fg;
|
||||
const ch = (k) => fg[k] * fg.a + bg[k] * (1 - fg.a);
|
||||
return { r: ch('r'), g: ch('g'), b: ch('b'), a: 1 };
|
||||
}
|
||||
|
||||
/*** 作用:WCAG 相对亮度(sRGB 分段线性化,系数取自规范) ***/
|
||||
function luminance(c) {
|
||||
const lin = (v) => {
|
||||
const s = v / 255;
|
||||
return s <= 0.03928 ? s / 12.92 : ((s + 0.055) / 1.055) ** 2.4;
|
||||
};
|
||||
return 0.2126 * lin(c.r) + 0.7152 * lin(c.g) + 0.0722 * lin(c.b);
|
||||
}
|
||||
|
||||
/*** 作用:两色对比度(1~21),前景先合成到底色上再算;半透明底由调用方压到页面底色上 ***/
|
||||
function ratio(fg, bg) {
|
||||
const solid = bg;
|
||||
const f = over(fg, solid);
|
||||
const l1 = Math.max(luminance(f), luminance(solid));
|
||||
const l2 = Math.min(luminance(f), luminance(solid));
|
||||
return (l1 + 0.05) / (l2 + 0.05);
|
||||
}
|
||||
|
||||
/*** 作用:构建两套主题的变量表:亮色 = :root,暗色 = :root 叠加 [data-theme='dark'] ***/
|
||||
export function loadThemes(root) {
|
||||
const css = readText(path.join(root, TOKENS_REL));
|
||||
const light = declsOf(blockOf(css, ':root'));
|
||||
const dark = new Map([...light, ...declsOf(blockOf(css, "[data-theme='dark']"))]);
|
||||
return { light, dark };
|
||||
}
|
||||
|
||||
/*** 作用:跑一遍全部配对,返回每个主题的明细(纯数据,供 CLI 与测试共用) ***/
|
||||
export function auditContrast(root) {
|
||||
const themes = loadThemes(root);
|
||||
// 半透明底色(tint / overlay)必须先压到本主题的页面底色上,否则算出来的是「压在白色上」的假对比度
|
||||
const binder = (vars) => {
|
||||
const pageKey = '--ua-color-bg';
|
||||
if (!vars.has(pageKey)) throw new Error(`缺少页面底色令牌:${pageKey}`);
|
||||
const page = resolve(vars.get(pageKey), vars);
|
||||
return (name) => {
|
||||
const key = `--ua-color-${name}`;
|
||||
if (!vars.has(key)) throw new Error(`配对引用了不存在的令牌:${key}`);
|
||||
return over(resolve(vars.get(key), vars), page);
|
||||
};
|
||||
};
|
||||
const lightOf = binder(themes.light);
|
||||
const darkOf = binder(themes.dark);
|
||||
const rows = PAIRS.map((pair) => {
|
||||
const lightRatio = ratio(lightOf(pair.fg), lightOf(pair.bg));
|
||||
const darkRatio = ratio(darkOf(pair.fg), darkOf(pair.bg));
|
||||
const bad = [];
|
||||
if (lightRatio < pair.min) bad.push('亮色');
|
||||
if (darkRatio < pair.min) bad.push('暗色');
|
||||
return { ...pair, light: lightRatio, dark: darkRatio, bad };
|
||||
});
|
||||
return { rows, failures: rows.filter((r) => r.bad.length > 0) };
|
||||
}
|
||||
|
||||
/*** 作用:把明细打成对齐的文本表(人读模式,CLI 默认输出) ***/
|
||||
export function formatReport({ rows, failures }) {
|
||||
const fixed = (n) => n.toFixed(2).padStart(6);
|
||||
const lines = ['配对 亮色 暗色 阈值 结论'];
|
||||
for (const r of rows) {
|
||||
const ok = r.bad.length === 0 ? 'OK' : `FAIL(${r.bad.join('+')})`;
|
||||
lines.push(`${r.label.padEnd(26)}${fixed(r.light)} ${fixed(r.dark)} ${String(r.min).padStart(4)} ${ok}`);
|
||||
}
|
||||
lines.push('');
|
||||
lines.push(failures.length === 0 ? '全部配对达标' : `不达标 ${failures.length} 条:${failures.map((f) => f.label).join(' / ')}`);
|
||||
return lines.join('\n');
|
||||
}
|
||||
@@ -20,6 +20,7 @@
|
||||
"ua verify:清单一致性门禁(id 唯一 / exports 对应 / props 必带 n t d / steps≥3 / demo 锚点存在 / 视觉回归抽查清单六组件各 ≥3 条且五维度齐全)",
|
||||
"ua commit-lint <文件|message>:提交信息门禁(type 枚举 / scope 规则 / subject 中文祈使 ≤50 字符),规范见 COMMITTING.md,违规退出码 1",
|
||||
"ua audit:visual [--json|--baseline]:视觉漂移门禁,统计皮肤包里各类硬编码视觉字面量(字号/动效时长/阴影/圆角/焦点环/内距/控件高度/字重/行高/层级/边框色)并与 .ua/visual-baseline.json 棘轮比对,超基线退出码 1;--baseline 重写基线,--json 出机器可读计数与每类命中行号(line / prop / value / hint,Agent 可直接定位)",
|
||||
"ua audit:contrast [--json|--list]:对比度门禁,把令牌里 16 组「文字色 × 底色」配对按 WCAG 公式算成对比度(求值支持 var() 与 color-mix,半透明底先压到页面底色),亮暗分算,低于阈值(正文 7 / 常规 4.5 / 禁用 2.6)退出码 1;--json 出逐对明细,--list 只列配对表",
|
||||
"ua mcp:零依赖 stdio MCP server,暴露 list_components / get_component / search_components / get_tokens / list_recipes / get_recipe"
|
||||
],
|
||||
"keyTypes": [
|
||||
@@ -27,7 +28,7 @@
|
||||
"MCP tool 入参 schema"
|
||||
]
|
||||
},
|
||||
"usage": "node packages/tools/cli/bin/ua.js <index|list|info <包名|组件id>|docs|tokens|style|verify|commit-lint|audit:visual|doctor|mcp>;仓库内等价于 npm run index / docs / style / verify / audit:visual / doctor",
|
||||
"usage": "node packages/tools/cli/bin/ua.js <index|list|info <包名|组件id>|docs|tokens|style|verify|commit-lint|audit:visual|audit:contrast|doctor|mcp>;仓库内等价于 npm run index / docs / style / verify / audit:visual / audit:contrast / doctor",
|
||||
"conventions": [
|
||||
"零依赖:只用 node 标准库,Node >= 18",
|
||||
"清单是单一来源:代码注释、文档站、llms-full.txt、MCP 全部从 ua.manifest.json 派生",
|
||||
@@ -36,12 +37,13 @@
|
||||
"docs": {
|
||||
"intro": "工程层工具包(private,不发布)。所有 Agent 与人都通过它读取组件库:清单驱动索引、文档、MCP 三件套,外加 style / verify 两道门禁。",
|
||||
"steps": [
|
||||
"接手先跑 ua doctor(node 版本 / 依赖 / git 钩子 / .ua 元数据一次检完,秒级),再跑 npm run check 确认基线全绿(build → demo → style → typecheck → smoke → index → verify → lint-iterations → audit:visual)",
|
||||
"接手先跑 ua doctor(node 版本 / 依赖 / git 钩子 / .ua 元数据一次检完,秒级),再跑 npm run check 确认基线全绿(build → demo → style → typecheck → smoke → index → verify → lint-iterations → audit:visual → audit:contrast)",
|
||||
"查组件:ua list 看全量,ua search <关键词> 按 id/标题/参数名/参数说明/步骤五层 + 配方三层(标题/配方组件/要点)模糊找(命中非 id 标题会标出命中字段,配方命中行直接给 ua recipes <编号> 指路;拼错会回相近 id),ua info <组件id> 看 props/步骤/示例(ua info <包名> 看单包 api 与约定,加 --json 出原始清单)",
|
||||
"查配方:ua recipes 列全量(编号 / 标题 / 用到的组件),ua recipes <编号> 看骨架代码与要点(--json 给脚本用;MCP 里对应 list_recipes / get_recipe)",
|
||||
"查令牌:ua tokens 打印全部 --ua-* 变量(颜色带暗色值),--json 输出结构化数组;写样式前先查,禁止硬编码。令牌分十三组:颜色 / 混色浓度 / 圆角 / 间距 / 字号 / 字重行高 / 边框 / 阴影 / 动效 / 位移 / 焦点环 / 控件高度 / 层级,color-mix 混入浓度用 --ua-alpha-soft/tint/line/mid/strong/heavy,弹层阴影用 --ua-shadow-1..4,过渡用 --ua-duration-* + --ua-ease,transform 位移用 --ua-translate-*(负号 calc(-1 * var(...))),焦点环用 --ua-focus-ring(_inset)",
|
||||
"改完清单跑 npm run docs:同时刷新人类文档站与 llms-full.txt(Agent 首选喂料)",
|
||||
"改完样式跑 npm run audit:visual:各类硬编码计数不得高于 .ua/visual-baseline.json(棘轮只减不增);字面量可用 npm run visual:migrate 幂等改写成令牌",
|
||||
"改完颜色令牌跑 npm run audit:contrast:16 组文字/底色配对在亮暗两套主题下都必须达标(正文 7 / 常规 4.5 / 禁用 2.6),跌破即退出码 1——调一个色值就能让全站辅助文字掉到 4.4,人眼看不出但门禁看得出",
|
||||
"在 ~/.workbuddy/mcp.json 注册 ua mcp 后,Agent 可直接调 list_components / get_component / search_components / get_tokens / list_recipes / get_recipe"
|
||||
],
|
||||
"components": []
|
||||
|
||||
Reference in New Issue
Block a user