iter(g229): 混色浓度令牌化并入视觉棘轮

This commit is contained in:
HIsheR
2026-10-26 20:40:52 +08:00
parent 7579170912
commit 36c787a203
8 changed files with 162 additions and 58 deletions
+83 -2
View File
@@ -66,10 +66,20 @@ export const KIND_META = [
label: '边框色字面值',
hint: 'border/border-color 写死色值(rgba()/#hex),组件轮廓用 --ua-color-border、分割线用 --ua-border-subtle',
},
{
key: 'mix-literal',
label: '混色浓度字面值',
hint: 'color-mix 的混入浓度写死百分比,应改用 --ua-alpha-* 六档(soft/tint/line/mid/strong/heavy)',
},
];
/*** 作用:需要扫描的 CSS 属性白名单(其余属性本轮不动;用白名单顺带避开伪类选择器误判) ***/
const SCAN_PROPS = new Set([
'background-image',
'background',
'color',
'fill',
'stroke',
'font-size',
'transition',
'animation',
@@ -237,6 +247,74 @@ const LEADING_TOKENS = new Map([
/*** 作用:下拉类 z-index 字面值集合(历史上下拉/气泡弹层用的 20/30/40 档,统一归 --ua-z-dropdown) ***/
const Z_DROPDOWN_LIKE = new Set(['20', '30', '40']);
/*** 作用:混色浓度字面值 → --ua-alpha-* 档位(近档就近收敛:12→10、16/20/24→18、40→45、65/80→70,Δ≤6) ***/
const MIX_TOKENS = new Map([
['10', 'var(--ua-alpha-soft)'],
['12', 'var(--ua-alpha-soft)'],
['16', 'var(--ua-alpha-tint)'],
['18', 'var(--ua-alpha-tint)'],
['20', 'var(--ua-alpha-tint)'],
['24', 'var(--ua-alpha-tint)'],
['30', 'var(--ua-alpha-line)'],
['40', 'var(--ua-alpha-mid)'],
['45', 'var(--ua-alpha-mid)'],
['55', 'var(--ua-alpha-strong)'],
['65', 'var(--ua-alpha-heavy)'],
['70', 'var(--ua-alpha-heavy)'],
['80', 'var(--ua-alpha-heavy)'],
]);
/*** 作用:提取值里所有 color-mix(...) 块(配平括号,嵌套 color-mix 逐层都算一条);返回全块文本与首色参(in srgb 之后的第一个顶层参数) ***/
function extractMixes(value) {
const out = [];
const src = String(value);
for (let i = src.indexOf('color-mix('); i !== -1; i = src.indexOf('color-mix(', i + 1)) {
let depth = 0;
let end = -1;
for (let k = i + 'color-mix'.length; k < src.length; k++) { // 从函数名后的 ( 起配平
if (src[k] === '(') depth++;
else if (src[k] === ')') { depth--; if (!depth) { end = k; break; } }
}
if (end === -1) break; // 未闭合:整值残缺,交给浏览器报错
const block = src.slice(i, end + 1);
const inner = block.slice('color-mix('.length, -1); // 去掉外壳与收尾括号
const args = splitTopLevel(inner).map((s) => s.trim());
out.push({ block, start: i, arg1: args[1] ?? '' }); // args[0] 是插值方法(in srgb),首色参跟在其后
}
return out;
}
/*** 作用:首色参末尾的混入浓度:已是 --ua-alpha-* 令牌返回 null;写死百分比返回数字字符串(渐变停靠位不在 color-mix 首参末尾,天然豁免) ***/
function mixAmount(arg1) {
const s = String(arg1).trim();
if (/var\(--ua-alpha-[\w-]+\)\s*$/.test(s)) return null; // 已令牌化
const lit = s.match(/(\d+(?:\.\d+)?)%\s*$/);
return lit ? lit[1] : null;
}
/*** 作用:迁移单条声明里的所有 color-mix 浓度字面量:逐块把首色参尾部的 NN% 换成 --ua-alpha-* 档位(表外浓度保留交人工归档) ***/
function migrateMix(core) {
const src = String(core);
const mixes = extractMixes(src);
if (!mixes.length) return null;
let hits = 0;
let out = '';
let cursor = 0;
for (const { block, start, arg1 } of mixes) {
const amt = mixAmount(arg1);
const token = amt !== null ? MIX_TOKENS.get(amt) : null;
if (!token) continue;
const nextArg1 = arg1 ? arg1.replace(/(\d+(?:\.\d+)?)%\s*$/, token) : arg1; // 复用原有的「色 浓度」间隔空格
const nextBlock = nextArg1 === arg1 || !arg1 ? block : block.replace(arg1, nextArg1); // arg1 是 block 的精确子串,整串替换安全
hits++;
out += src.slice(cursor, start) + nextBlock;
cursor = start + block.length;
}
if (!hits) return null;
out += src.slice(cursor);
return { kind: 'mix-literal', value: out };
}
/*** 作用:压缩空白,便于用稳定字符串比对(不改语义) ***/
export function squash(value) {
return String(value).replace(/\s+/g, ' ').trim();
@@ -262,7 +340,7 @@ export function blankComments(src) {
/*** 作用:逐条产出「属性 + 值」及其在原文中的偏移;只认白名单属性且必须以分号收尾(避开伪类选择器) ***/
export function eachDeclaration(code) {
const re = /(border-top-color|border-right-color|border-bottom-color|border-left-color|border-color|border-top|border-right|border-bottom|border-left|border|padding-top|padding-right|padding-bottom|padding-left|padding|margin-top|margin-right|margin-bottom|margin-left|margin|row-gap|column-gap|gap|font-weight|line-height|z-index|min-height|max-height|height|transition-duration|animation-duration|transition|animation|font-size|box-shadow|border-radius|outline)\s*:\s*([^;{}]*);/gi;
const re = /(border-top-color|border-right-color|border-bottom-color|border-left-color|border-color|border-top|border-right|border-bottom|border-left|border|padding-top|padding-right|padding-bottom|padding-left|padding|margin-top|margin-right|margin-bottom|margin-left|margin|row-gap|column-gap|gap|font-weight|line-height|z-index|min-height|max-height|height|transition-duration|animation-duration|transition|animation|font-size|box-shadow|border-radius|outline|background-image|background|color|fill|stroke)\s*:\s*([^;{}]*);/gi;
const list = [];
for (const m of code.matchAll(re)) {
const prop = m[1].toLowerCase();
@@ -430,7 +508,7 @@ export function migrateDeclaration(prop, rawValue) {
} else if (prop === 'z-index') {
result = migrateZ(core);
} else {
result = migrateBorder(prop, core); // 边框类:只收敛纯黑 rgba 边
result = migrateMix(core) ?? migrateBorder(prop, core); // 其余属性:先收敛 color-mix 浓度,再收敛纯黑 rgba 边
}
if (!result) return null;
return { kind: result.kind, value: result.value + tail };
@@ -504,6 +582,9 @@ function detect(prop, rawValue, key) {
const stripped = value.replace(/var\(--ua-[\w-]+\)/g, ' '); // 已是令牌的色先剔除
return /(#[0-9a-f]{3,8}\b|rgba?\(|hsla?\()/i.test(stripped) ? 1 : 0;
}
if (key === 'mix-literal') {
return extractMixes(value).filter(({ arg1 }) => mixAmount(arg1) !== null).length; // 只认 color-mix 块内首色参的写死浓度;渐变停靠位与几何百分比天然不在口径内
}
return 0;
}
+1 -1
View File
@@ -39,7 +39,7 @@
"接手先跑 ua doctor(node 版本 / 依赖 / git 钩子 / .ua 元数据一次检完,秒级),再跑 npm run check 确认基线全绿(build → demo → style → typecheck → smoke → index → verify → lint-iterations → audit:visual)",
"查组件:ua list 看全量,ua search <关键词> 按 id/标题/参数名/参数说明/步骤模糊找(「处理中」这类说明里的词也能搜到,命中非 id 标题时会标出命中字段;拼错会回相近 id),ua info <组件id> 看 props/步骤/示例(ua info <包名> 看单包 api 与约定,加 --json 出原始清单)",
"查配方:ua recipes 列全量(编号 / 标题 / 用到的组件),ua recipes <编号> 看骨架代码与要点(--json 给脚本用;MCP 里对应 list_recipes / get_recipe)",
"查令牌:ua tokens 打印全部 --ua-* 变量(颜色带暗色值),--json 输出结构化数组;写样式前先查,禁止硬编码。令牌分十一组:颜色 / 圆角 / 间距 / 字号 / 字重行高 / 边框 / 阴影 / 动效 / 焦点环 / 控件高度 / 层级,弹层阴影用 --ua-shadow-1..4,过渡用 --ua-duration-* + --ua-ease,焦点环用 --ua-focus-ring(_inset)",
"查令牌:ua tokens 打印全部 --ua-* 变量(颜色带暗色值),--json 输出结构化数组;写样式前先查,禁止硬编码。令牌分十二组:颜色 / 混色浓度 / 圆角 / 间距 / 字号 / 字重行高 / 边框 / 阴影 / 动效 / 焦点环 / 控件高度 / 层级,color-mix 混入浓度用 --ua-alpha-soft/tint/line/mid/strong/heavy,弹层阴影用 --ua-shadow-1..4,过渡用 --ua-duration-* + --ua-ease,焦点环用 --ua-focus-ring(_inset)",
"改完清单跑 npm run docs:同时刷新人类文档站与 llms-full.txt(Agent 首选喂料)",
"改完样式跑 npm run audit:visual:各类硬编码计数不得高于 .ua/visual-baseline.json(棘轮只减不增);字面量可用 npm run visual:migrate 幂等改写成令牌",
"在 ~/.workbuddy/mcp.json 注册 ua mcp 后,Agent 可直接调 list_components / get_component / search_components / get_tokens / list_recipes / get_recipe"