#!/usr/bin/env node /*** 作用:皮肤包视觉字面量 → 设计令牌的幂等迁移,并灌入视觉欠账条目(零新依赖,可重复运行) */ import fs from 'node:fs'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; import { UI_CSS_REL, migrateSource, countRows } from '../packages/tools/cli/lib/visual-rules.mjs'; // 仓库根:本脚本固定放在 /scripts/ 下,向上取一层即可 const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..'); // 样式表绝对路径 const CSS_FILE = path.join(ROOT, UI_CSS_REL); // 迭代待办文件绝对路径 const BACKLOG_FILE = path.join(ROOT, '.ua', 'backlog.json'); /*** 作用:把内容写盘,仅在与现有内容不同才写;返回是否真的写了(幂等的落盘口径) ***/ function writeIfChanged(file, content) { const exists = fs.existsSync(file); const old = exists ? fs.readFileSync(file, 'utf8') : ''; if (old === content) return false; fs.writeFileSync(file, content, 'utf8'); return true; } /*** 作用:迁移 ui.css 字面量到令牌,打印每类替换处数,并在无变化时明确说「无需改动」 ***/ function migrateCss() { const before = fs.readFileSync(CSS_FILE, 'utf8'); const { text, counts } = migrateSource(before); const rows = countRows(counts); const total = rows.reduce((sum, row) => sum + row.count, 0); for (const row of rows) console.log(`${row.label}(${row.key})替换 ${row.count} 处`); console.log(`合计 ${total} 处`); if (!writeIfChanged(CSS_FILE, text)) { console.log(`无需改动:${UI_CSS_REL} 已是令牌化状态`); return 0; } console.log(`已写回 ${UI_CSS_REL}`); return 0; } /*** 作用:按标题判重后追加视觉欠账条目(domain=visual,状态 open),id 取当前最大 g 编号往后排 ***/ function seedBacklog() { const data = JSON.parse(fs.readFileSync(BACKLOG_FILE, 'utf8')); const items = data.items ?? []; const seeds = visualSeeds(); const have = new Set(items.map((item) => item.title)); let maxId = 0; // 现存最大 g 编号:动态取,避免与并发迭代实例抢号 for (const item of items) { const m = /^g(\d+)$/.exec(item.id ?? ''); if (m) maxId = Math.max(maxId, Number(m[1])); } let added = 0; for (const seed of seeds) { if (have.has(seed.title)) continue; // 幂等:同标题已存在就不再插 maxId += 1; items.push({ id: `g${maxId}`, ...seed, status: 'open', notes: '', domain: 'visual' }); added += 1; } writeIfChanged(BACKLOG_FILE, `${JSON.stringify({ ...data, items }, null, 2)}\n`); console.log(added ? `已追加 ${added} 条视觉欠账(g${maxId - added + 1}…g${maxId})` : `无需改动:${seeds.length} 条视觉欠账均已存在`); return 0; } /*** 作用:给缺 domain 字段的存量条目按标题批量推断回填(不动 id / status / 其它字段与顺序) ***/ function backfillDomain() { const data = JSON.parse(fs.readFileSync(BACKLOG_FILE, 'utf8')); const items = data.items ?? []; let filled = 0; const tally = {}; // 各域回填条数,便于核对推断比例 for (const item of items) { if (item.domain) continue; const domain = inferDomain(item.title ?? ''); item.domain = domain; tally[domain] = (tally[domain] ?? 0) + 1; filled += 1; } writeIfChanged(BACKLOG_FILE, `${JSON.stringify({ ...data, items }, null, 2)}\n`); if (!filled) { console.log('无需改动:所有条目已有 domain'); return 0; } const detail = Object.entries(tally).map(([k, v]) => `${k} ${v}`).join(' / '); console.log(`已回填 ${filled} 条 domain(${detail})`); return 0; } /*** 作用:按标题关键词推断域:含「新增」判 new,含工具链关键词判 tooling,其余一律 tune ***/ function inferDomain(title) { if (title.includes('新增')) return 'new'; const tooling = ['门禁', 'CLI', 'MCP', '文档', '配方', '校验']; if (tooling.some((kw) => title.includes(kw))) return 'tooling'; return 'tune'; } /*** 作用:八条首批视觉欠账的定义(验收标准都写成可枚举或可机器验证的口径) ***/ function visualSeeds() { return [ { title: '弹层阴影语义化:剩余字面阴影归入 elevation 四档', motivation: '阴影已收敛出四档令牌,但仍有主题色投影(color-mix text/accent)与状态色环以字面值散落在弹层、气泡、选中态里,同名层级在不同组件用不同数值,观感不齐且改不动。', acceptance: 'ui.css 中 box-shadow 的 px 字面值全部归入 --ua-shadow-1..4 或语义色环(无主题色投影字面值);`ua audit:visual --json` 的 shadow-literal 计数相对 .ua/visual-baseline.json 至少减少 8 处并写入新基线;在 packages/skin/ui/ua.manifest.json 的 docs.intro 写明「下拉=shadow-2 / 浮层=shadow-3 / 弹窗=shadow-4 / 通知=shadow-4」对照表;npm run check 全绿。', }, { title: '内距令牌化:padding / gap 字面值降到基线以下', motivation: '间距只有 4/8/12/16 四档令牌,实际却大量写 6px/10px 这类中间值,同一类容器内距不统一,改密度只能全仓替换。', acceptance: 'packages/design/tokens/tokens.css 补齐内距档位(新增 6px/10px 两档或把它们归并到既有档并在注释说明),tokens.json 同步;visual-rules 增加 space-literal 类(padding/gap/margin 的 px 字面值),迁移脚本同步规则并幂等;该类计数写入新基线且比首次统计少 30 处以上;npm run check 全绿。', }, { title: '表单控件高度统一接入 control-h 三档', motivation: '控件高度令牌 --ua-control-h-sm/md/lg 已就位,但 button/textField/select/inputNumber 仍各自写死高度,同一行并排时高度不一致,也挡住「略放大提高可点面积」的落地。', acceptance: 'ui.css 中 .ua-btn / .ua-input / .ua-select / .ua-input-number 的高度声明全部引用 var(--ua-control-h-*);visual-rules 增加 control-h-literal 类且计数为 0;apps/demo/main.ts 中存在一行并排的三档高度演示(sec 锚点内真实调用四个组件);npm run check 全绿。', }, { title: '暗色主题阴影与焦点环适配', motivation: '暗色块只覆盖颜色,弹层阴影沿用亮色主题的低透明度黑投影,暗底上几乎看不出层级;焦点环在暗色下对比也不足。', acceptance: '[data-theme=\'dark\'] 覆盖 --ua-shadow-1..4(更高黑度)与 --ua-focus-ring 两个槽位;ua verify 增加一条校验:暗色块缺 shadow / focus 覆盖即报错;ua tokens 输出的暗色列能看到这 6 个令牌的暗色值;npm run check 全绿。', }, { title: '字重与行高统一到三档令牌', motivation: '标题、正文、辅助文字的字重与行高全靠各组件手写,同一层级的标题在不同弹窗里字重不同,多行文本行距忽松忽紧。', acceptance: 'visual-rules 增加 weight-literal 与 leading-literal 两类(font-weight / line-height 的字面值),迁移脚本把字面值改为 --ua-weight-* / --ua-leading-*;两类计数归零并写入新基线;标题/正文/辅助三档在注释里写明归属;npm run check 全绿。', }, { title: '层级令牌落地:z-index 字面值归零', motivation: '--ua-z-dropdown/sticky/overlay/modal/toast 五档已定义但无人使用,实际仍是各组件写 100/1000/9999,弹层互相盖住的问题只能靠试数值解决。', acceptance: 'visual-rules 增加 z-literal 类;迁移脚本把 ui.css 中全部 z-index 字面值替换为 var(--ua-z-*);该类计数为 0 并写入新基线;ua tokens 能看到五档层级;npm run check 全绿。', }, { title: '边框细节统一到 subtle 与基础色', motivation: '分割线、输入框、卡片边框混用基础边框色与更深的黑边,1px 线在不同组件上粗细观感不一,密集表格里分割线过重。', acceptance: '分割线改用 --ua-border-subtle、输入框与卡片边框用 --ua-color-border,ui.css 中不再出现硬编码黑边(rgba(0,0,0,…) 作为边框值);visual-rules 增加 border-literal 类且计数为 0;表格与输入框在 demo 中肉眼无粗细跳变;npm run check 全绿。', }, { title: '视觉回归抽查清单:六个代表组件的可勾选验收项', motivation: '视觉类改动只能靠人眼抽查,每次凭记忆看一遍,漏看是常态;把六个高频组件的观感要点固化成清单,后续视觉条目可直接复用。', acceptance: '在 ITERATION.md 或 packages/skin/ui/ua.manifest.json 增加《视觉回归抽查清单》,button / textField / select / modal / dataTable / toast 六个组件各 ≥3 条可勾选要点(合计 ≥18 条);清单条目必须覆盖边框、阴影、过渡、焦点环、暗色五项;ua verify 校验清单存在且 18 条要点齐全;npm run check 全绿。', }, ]; } /*** 作用:命令行入口:默认迁移样式表,--seed 灌视觉欠账,--backfill-domain 回填域字段 ***/ function main(argv) { const flags = new Set(argv); if (flags.has('--help') || flags.has('-h')) { console.log('用法: node scripts/visual-migrate.mjs [--seed] [--backfill-domain]'); console.log(' 默认 把 ui.css 的视觉字面量迁移成 --ua-* 令牌(幂等,无变化不写盘)'); console.log(' --seed 按标题判重追加 8 条 domain=visual 的欠账条目(幂等)'); console.log(' --backfill-domain 给缺 domain 的存量条目按标题推断回填(幂等)'); return 0; } try { if (flags.has('--seed')) seedBacklog(); if (flags.has('--backfill-domain')) backfillDomain(); if (!flags.has('--seed') && !flags.has('--backfill-domain')) migrateCss(); } catch (err) { console.error(`visual-migrate 失败:${err.message}`); return 1; } return 0; } process.exit(main(process.argv.slice(2)));