diff --git a/.ua/index.json b/.ua/index.json index 9fed55f..0f052f4 100644 --- a/.ua/index.json +++ b/.ua/index.json @@ -1,6 +1,6 @@ { "$schema": "ui-arch-registry-index/0.1", - "generatedAt": "2026-10-26T13:40:30.157Z", + "generatedAt": "2026-10-26T13:53:12.662Z", "repoRoot": ".", "packages": [ { @@ -4055,6 +4055,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": [ @@ -4062,7 +4063,7 @@ "MCP tool 入参 schema" ] }, - "usage": "node packages/tools/cli/bin/ua.js |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 |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 派生", @@ -4071,12 +4072,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": [] diff --git a/AGENTS.md b/AGENTS.md index 17686bd..36dd7e3 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -7,7 +7,7 @@ 1. **包管理器必须用 pnpm**:包间依赖用 `workspace:*` 协议,npm 不支持。安装依赖:`pnpm install`。 2. **构建工具**:TypeScript 5(`tsc`)。若环境未全局安装,可使用托管工作区的副本:`C:/Users/shawn/.workbuddy/binaries/node/workspace/node_modules/typescript`(本仓库历史即如此构建)。 3. **Node**:>= 18,ESM(`"type": "module"`)。 -4. **环境自检 + 验证基线**:先跑 `node packages/tools/cli/bin/ua.js doctor`(秒级检查 node 版本 / 依赖安装 / git 钩子 / .ua 元数据可解析,任一失败退出码 1),通过后再跑 `npm run check`(build → demo → style → typecheck → smoke → index → verify → lint-iterations → audit:visual)确认基线全绿,再开始改代码。 +4. **环境自检 + 验证基线**:先跑 `node packages/tools/cli/bin/ua.js doctor`(秒级检查 node 版本 / 依赖安装 / git 钩子 / .ua 元数据可解析,任一失败退出码 1),通过后再跑 `npm run check`(build → demo → style → typecheck → smoke → index → verify → lint-iterations → audit:visual → audit:contrast)确认基线全绿,再开始改代码。 ## 编码规范(强制) @@ -27,7 +27,7 @@ - `apps/demo`(@ui-arch/demo):交互演示,产物 apps/demo/dist/demo.html - `apps/docs`:文档站,纯产物(ua docs 生成) -**门禁一条龙**:`npm run check` = build + demo + style + typecheck + smoke + index + verify + lint-iterations + audit:visual,提交前必过。其中 `smoke` 是两段内核断言:`@ui-arch/frame` 14 条(信号读写与同值短路、effect 注销与条件分支重追踪、computed 链式派生、多订阅者扇出)+ `@ui-arch/table-core` 21 条(排序 / 筛选 / 分页 / 选择的派生管线);改内核语义前先跑它,别靠组件 demo 肉眼验。`ua audit:visual` 统计皮肤包里十四类硬编码视觉字面量(字号 / 动效时长 / 阴影 / 圆角 / 焦点环 / 内距 / 控件高度 / 字重 / 行高 / 层级 / 边框色 / 混色浓度 / transform 位移 / 焦点描边)并与 `.ua/visual-baseline.json` 做棘轮:任一类超过基线即退出码 1 并指出疑似引入的提交;迁移脚本 `npm run visual:migrate` 可把字面量幂等改写成令牌。`ua verify` 校验清单 ↔ 文档 ↔ 演示一致(docs.components 的 id 必须在 api.exports 有对应条目、props 必须带 n/t/d、每个文档字段名必须在组件实现源码里真实出现(改名后忘改文档会直接报错)、反向也校验 `src/types.ts` 里 `<组件>Props`(可用清单的 `propsType` 覆盖接口名,`false` 表示无入参)每个字段都在文档 props 表登记(新加字段忘了写文档会报错)、demo 锚点必须在 `apps/demo/main.ts` 中存在,且该组件必须被 demo 真实调用 `id(` / `id.`),检查皮肤包样式表无硬编码颜色(`#hex` 一律禁止,必须走 `--ua-*` 令牌)与令牌引用有效(`var(--ua-*)` 必须是 tokens.css 已定义或组件自设的变量)、核对每个 `--ua-color-*` 都在 `[data-theme='dark']` 里有覆盖值且暗色块没有凭空多出的变量名(新增颜色令牌漏写暗色值直接报错)、核对 `tokens.json` 的 `color` 键与 tokens.css 的 `--ua-color-*` 同名同值(名字按 camelCase → kebab 折算,亮色值与暗色值都比,双向查漏——只改一处时文档色板会凭空多/少一个令牌,过去只能靠人眼发现)、核对 `@keyframes` 没有「帧体完全相同但名字不同」的复制粘贴(同一段动效只该有一份声明,改名复制会让方向 / 时长各写各的,翻转态就出现过「朝反方向入场」),核对 README 的 `` 区块与「分组:」行都覆盖清单组件集合(漏写/多写/重复都报错),并交叉校验 backlog 的 done 条目与 `.ua/iterations.log` 的 done 留痕一一对应(另外查 backlog id 不重复、g 编号严格递增、同一 id 只有一条 done 留痕,且留痕 ts 带 +08:00、时间线不回退、不落在未来——都是过去手工插条目/手写时间戳踩过的坑);配方还要求编号连续、每条都有代码块与要点,且代码块里至少认出一个已注册组件 id(拼错组件名时 `ua recipes` 会把它列成空组件表)。 +**门禁一条龙**:`npm run check` = build + demo + style + typecheck + smoke + index + verify + lint-iterations + audit:visual + audit:contrast,提交前必过。其中 `smoke` 是两段内核断言:`@ui-arch/frame` 14 条(信号读写与同值短路、effect 注销与条件分支重追踪、computed 链式派生、多订阅者扇出)+ `@ui-arch/table-core` 21 条(排序 / 筛选 / 分页 / 选择的派生管线);改内核语义前先跑它,别靠组件 demo 肉眼验。`ua audit:visual` 统计皮肤包里十四类硬编码视觉字面量(字号 / 动效时长 / 阴影 / 圆角 / 焦点环 / 内距 / 控件高度 / 字重 / 行高 / 层级 / 边框色 / 混色浓度 / transform 位移 / 焦点描边)并与 `.ua/visual-baseline.json` 做棘轮:任一类超过基线即退出码 1 并指出疑似引入的提交;迁移脚本 `npm run visual:migrate` 可把字面量幂等改写成令牌。`ua audit:contrast` 走另一条线:把令牌里 16 组「文字色 × 底色」配对(正文/次级/辅助文字 × 页面底/表面/凹陷底、强调色与其反白、语义三色、禁用态豁免档)按 WCAG 公式算成对比度(支持 `color-mix` 与 `var()` 求值,半透明底先压到页面底色上),亮暗两套主题各算一遍,低于阈值(正文 7 / 常规 4.5 / 禁用 2.6)即退出码 1;`--json` 出逐对明细,`--list` 只列配对表。改任一颜色令牌后它会立刻告诉你「改完谁读不清了」,不必等人工目测暗色。`ua verify` 校验清单 ↔ 文档 ↔ 演示一致(docs.components 的 id 必须在 api.exports 有对应条目、props 必须带 n/t/d、每个文档字段名必须在组件实现源码里真实出现(改名后忘改文档会直接报错)、反向也校验 `src/types.ts` 里 `<组件>Props`(可用清单的 `propsType` 覆盖接口名,`false` 表示无入参)每个字段都在文档 props 表登记(新加字段忘了写文档会报错)、demo 锚点必须在 `apps/demo/main.ts` 中存在,且该组件必须被 demo 真实调用 `id(` / `id.`),检查皮肤包样式表无硬编码颜色(`#hex` 一律禁止,必须走 `--ua-*` 令牌)与令牌引用有效(`var(--ua-*)` 必须是 tokens.css 已定义或组件自设的变量)、核对每个 `--ua-color-*` 都在 `[data-theme='dark']` 里有覆盖值且暗色块没有凭空多出的变量名(新增颜色令牌漏写暗色值直接报错)、核对 `tokens.json` 的 `color` 键与 tokens.css 的 `--ua-color-*` 同名同值(名字按 camelCase → kebab 折算,亮色值与暗色值都比,双向查漏——只改一处时文档色板会凭空多/少一个令牌,过去只能靠人眼发现)、核对 `@keyframes` 没有「帧体完全相同但名字不同」的复制粘贴(同一段动效只该有一份声明,改名复制会让方向 / 时长各写各的,翻转态就出现过「朝反方向入场」),核对 README 的 `` 区块与「分组:」行都覆盖清单组件集合(漏写/多写/重复都报错),并交叉校验 backlog 的 done 条目与 `.ua/iterations.log` 的 done 留痕一一对应(另外查 backlog id 不重复、g 编号严格递增、同一 id 只有一条 done 留痕,且留痕 ts 带 +08:00、时间线不回退、不落在未来——都是过去手工插条目/手写时间戳踩过的坑);配方还要求编号连续、每条都有代码块与要点,且代码块里至少认出一个已注册组件 id(拼错组件名时 `ua recipes` 会把它列成空组件表)。 `ua verify` 另外校验 `ITERATION.md` 的《视觉回归抽查清单》:button / textField / select / modal / dataTable / toast 六段各 ≥3 条可勾选要点、每条维度标签必须落在边框 / 阴影 / 过渡 / 焦点环 / 暗色五类内、合计 ≥18 条且五维度全覆盖——视觉类条目收尾前照单过一遍,改清单时别把条目删漏。 diff --git a/ITERATION.md b/ITERATION.md index bbde0b3..f4d0902 100644 --- a/ITERATION.md +++ b/ITERATION.md @@ -44,7 +44,7 @@ qa/probe ──门禁通过──▶ qa/stable ──人工审阅──▶ main 1. **同步**:`git fetch --all --prune`,切到 `qa/stable` 并 `git pull`;从其最新提交强制重建 `qa/probe` 并切换过去。 2. **选题**:读 `.ua/backlog.json`,先取 `status === "open"` 且 `domain === "visual"` 的条目(有多条取编号最小的一条);没有视觉欠账时再按数组顺序取第一条 `status === "open"` 的条目;若全部关闭,按《选题规则》自行补充一条新点子后继续。 3. **实现**:按 `CODING.md` 编码规范实现,保持低侵入,单文件 < 200 行,涉及组件需同步 manifest/docs/llms-full.txt。 -4. **门禁**:跑 `npm run check`(build → demo → style → typecheck → smoke → index → verify → lint-iterations → audit:visual)。 +4. **门禁**:跑 `npm run check`(build → demo → style → typecheck → smoke → index → verify → lint-iterations → audit:visual → audit:contrast)。 - **通过**:提交到 `qa/probe`,提交信息按 `COMMITTING.md` 规范(`iter(<条目id>): <中文祈使句>`,元数据变更用 `chore(backlog): 记录 完成`;commit-msg 钩子自动校验),推送 `qa/probe`,fast-forward 合并到 `qa/stable` 并推送;将 backlog 条目 `status` 置为 `done` 并随迭代提交。 - **失败**:尝试修复一次;仍失败则放弃本轮——`qa/probe` 直接重置回 `qa/stable`,backlog 条目在 `notes` 里追加失败原因、`status` 保持 `open`,提交并推送该元数据变更(仅改 `.ua/backlog.json`,不碰代码)。 5. **留痕**:在 `.ua/iterations.log` 追加一行 JSON,**字段统一**:`{"ts":"本地ISO时间","id":"条目id","result":"done|aborted","commit":"短哈希","summary":"一句话"}`。硬性规范:`ts` 必须来自 `date` 命令实测(本地时间,带 +08:00 偏移,**禁止手写或用 UTC 换算**);时长结论必须来自两次时间查询之差,禁止使用「大约/接近」等估算词;并发实例格式不得自创变体。随本轮提交。`ua lint-iterations` 会把每行 `commit` 拿到 git 历史里回查:哈希必须真实存在、主题必须指向同一条目(`iter()`,早期无 scope 的 `iter: ` 也认),伪造或抄错哈希直接判违规。 @@ -62,7 +62,7 @@ qa/probe ──门禁通过──▶ qa/stable ──人工审阅──▶ main 1. **域轮换**:连续 3 轮迭代中,至少 1 轮的条目 `domain` 为 `visual`。若已连续 3 轮都不是 visual,下一轮必须从 open 的 visual 条目里选。 2. **视觉欠账优先**:只要存在 `status === "open"` 且 `domain === "visual"` 的条目,它的优先级高于 `domain === "new"` 的条目——先清视觉欠账,再加新组件(视觉地基不牢,新组件只是把漂移再复制一份)。 -3. **新增组件的入库门槛**:`domain === "new"` 的条目,验收标准里必须写明「样式只消费 `--ua-*` 令牌,禁止新增硬编码阴影/时长/字号」,否则该条不得入库。视觉类改进的验收标准要能落到 `npm run audit:visual` 的计数上(涨了就是没做完)。 +3. **新增组件的入库门槛**:`domain === "new"` 的条目,验收标准里必须写明「样式只消费 `--ua-*` 令牌,禁止新增硬编码阴影/时长/字号」,否则该条不得入库。视觉类改进的验收标准要能落到 `npm run audit:visual` 的计数上(涨了就是没做完),涉及颜色令牌的还要落到 `npm run audit:contrast`(对比度跌破阈值即没做完)。 ## 视觉回归抽查清单(人工,视觉类条目收尾时逐条过) diff --git a/README.md b/README.md index b78e55b..8f54ca0 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ ```bash pnpm install # 包间依赖用 workspace:*,npm 不支持 -npm run check # 门禁一条龙:build → demo → style → typecheck → smoke → index → verify → lint-iterations → audit:visual +npm run check # 门禁一条龙:build → demo → style → typecheck → smoke → index → verify → lint-iterations → audit:visual → audit:contrast npm run demo # 打开演示:apps/demo/dist/demo.html npm run docs # 生成人类文档站 apps/docs/index.html + Agent 全量文档 llms-full.txt npm run changelog # 依据 git 提交史重写 CHANGELOG.md(发版前跑;changelog:check 可查漂移) diff --git a/llms-full.txt b/llms-full.txt index 3a70046..d8c9b35 100644 --- a/llms-full.txt +++ b/llms-full.txt @@ -1,6 +1,6 @@ # @ui-arch 组件库 Agent 文档(无图形版) -> 自动生成于 2026-10-26T13:40:30.157Z,数据源:各包 ua.manifest.json,勿手改。人类版文档站:apps/docs/index.html。 +> 自动生成于 2026-10-26T13:53:12.662Z,数据源:各包 ua.manifest.json,勿手改。人类版文档站:apps/docs/index.html。 > 组件 = 普通函数,传 props 返回 DOM;状态用 signal 双向绑定:读 signal() 写 signal(v)。 ## 快速上手 @@ -40,13 +40,14 @@ ### @ui-arch/cli -1. 接手先跑 ua doctor(node 版本 / 依赖 / git 钩子 / .ua 元数据一次检完,秒级),再跑 npm run check 确认基线全绿(build → demo → style → typecheck → smoke → index → verify → lint-iterations → audit:visual) +1. 接手先跑 ua doctor(node 版本 / 依赖 / git 钩子 / .ua 元数据一次检完,秒级),再跑 npm run check 确认基线全绿(build → demo → style → typecheck → smoke → index → verify → lint-iterations → audit:visual → audit:contrast) 2. 查组件:ua list 看全量,ua search <关键词> 按 id/标题/参数名/参数说明/步骤五层 + 配方三层(标题/配方组件/要点)模糊找(命中非 id 标题会标出命中字段,配方命中行直接给 ua recipes <编号> 指路;拼错会回相近 id),ua info <组件id> 看 props/步骤/示例(ua info <包名> 看单包 api 与约定,加 --json 出原始清单) 3. 查配方:ua recipes 列全量(编号 / 标题 / 用到的组件),ua recipes <编号> 看骨架代码与要点(--json 给脚本用;MCP 里对应 list_recipes / get_recipe) 4. 查令牌: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) 5. 改完清单跑 npm run docs:同时刷新人类文档站与 llms-full.txt(Agent 首选喂料) 6. 改完样式跑 npm run audit:visual:各类硬编码计数不得高于 .ua/visual-baseline.json(棘轮只减不增);字面量可用 npm run visual:migrate 幂等改写成令牌 -7. 在 ~/.workbuddy/mcp.json 注册 ua mcp 后,Agent 可直接调 list_components / get_component / search_components / get_tokens / list_recipes / get_recipe +7. 改完颜色令牌跑 npm run audit:contrast:16 组文字/底色配对在亮暗两套主题下都必须达标(正文 7 / 常规 4.5 / 禁用 2.6),跌破即退出码 1——调一个色值就能让全站辅助文字掉到 4.4,人眼看不出但门禁看得出 +8. 在 ~/.workbuddy/mcp.json 注册 ua mcp 后,Agent 可直接调 list_components / get_component / search_components / get_tokens / list_recipes / get_recipe ## @ui-arch/ui(skin-dom)v0.1.0 @@ -2317,12 +2318,13 @@ API 速览: - 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 一行用法: ``` -node packages/tools/cli/bin/ua.js |docs|tokens|style|verify|commit-lint|audit:visual|doctor|mcp>;仓库内等价于 npm run index / docs / style / verify / audit:visual / doctor +node packages/tools/cli/bin/ua.js |docs|tokens|style|verify|commit-lint|audit:visual|audit:contrast|doctor|mcp>;仓库内等价于 npm run index / docs / style / verify / audit:visual / audit:contrast / doctor ``` 约定: @@ -2341,7 +2343,7 @@ node packages/tools/cli/bin/ua.js |docs|tokens - --ua-color-surface-3: color-mix(in srgb, var(--ua-color-text) 8%, var(--ua-color-bg))(暗色 color-mix(in srgb, var(--ua-color-text) 10%, var(--ua-color-bg))) - --ua-color-surface-inset: color-mix(in srgb, var(--ua-color-text) 5%, var(--ua-color-bg))(暗色 color-mix(in srgb, var(--ua-color-text) 6%, var(--ua-color-bg))) - --ua-color-text: #1c1e21(暗色 #e7e9ec) -- --ua-color-text-muted: #6b7280(暗色 #888780) +- --ua-color-text-muted: #676d78(暗色 #888780) - --ua-color-text-secondary: #4b5158(暗色 #b4bac2) - --ua-color-text-faint: #9aa0a8(暗色 #62676f) - --ua-color-overlay: rgba(0, 0, 0, 0.45)(暗色 rgba(0, 0, 0, 0.6)) @@ -2350,7 +2352,7 @@ node packages/tools/cli/bin/ua.js |docs|tokens - --ua-color-scrollbar: rgba(0, 0, 0, 0.22)(暗色 rgba(255, 255, 255, 0.24)) - --ua-color-scrollbar-hover: rgba(0, 0, 0, 0.38)(暗色 rgba(255, 255, 255, 0.42)) - --ua-color-highlight: rgba(255, 255, 255, 0.5)(暗色 rgba(255, 255, 255, 0.08)) -- --ua-color-accent: #185fa5(暗色 #378add) +- --ua-color-accent: #185fa5(暗色 #4a9aeb) - --ua-color-accent-contrast: #ffffff(暗色 #042c53) - --ua-color-accent-soft: color-mix(in srgb, var(--ua-color-accent) 72%, var(--ua-color-bg))(暗色 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) var(--ua-alpha-soft), transparent)(暗色 color-mix(in srgb, var(--ua-color-accent) 14%, transparent)) diff --git a/llms.txt b/llms.txt index 97166e3..f03d035 100644 --- a/llms.txt +++ b/llms.txt @@ -143,8 +143,9 @@ - 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 -- 用法: `node packages/tools/cli/bin/ua.js |docs|tokens|style|verify|commit-lint|audit:visual|doctor|mcp>;仓库内等价于 npm run index / docs / style / verify / audit:visual / doctor` +- 用法: `node packages/tools/cli/bin/ua.js |docs|tokens|style|verify|commit-lint|audit:visual|audit:contrast|doctor|mcp>;仓库内等价于 npm run index / docs / style / verify / audit:visual / audit:contrast / doctor` - 约定: 零依赖:只用 node 标准库,Node >= 18 - 约定: 清单是单一来源:代码注释、文档站、llms-full.txt、MCP 全部从 ua.manifest.json 派生 - 约定: 门禁只增不改:新增校验必须能被人手复现,报错要指出文件与字段 diff --git a/package.json b/package.json index 99ba004..dda3cc4 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,7 @@ "style": "node packages/tools/cli/bin/ua.js style", "index": "node packages/tools/cli/bin/ua.js index", "docs": "node packages/tools/cli/bin/ua.js index && node packages/tools/cli/bin/ua.js docs", - "check": "npm run build && npm run demo && npm run style && npm run typecheck && npm run smoke && npm run index && npm run verify && npm run lint-iterations && npm run audit:visual", + "check": "npm run build && npm run demo && npm run style && npm run typecheck && npm run smoke && npm run index && npm run verify && npm run lint-iterations && npm run audit:visual && npm run audit:contrast", "ua": "node packages/tools/cli/bin/ua.js", "verify": "node packages/tools/cli/bin/ua.js verify", "lint-iterations": "node packages/tools/cli/bin/ua.js lint-iterations", @@ -20,7 +20,8 @@ "visual:migrate": "node scripts/visual-migrate.mjs", "changelog": "node packages/tools/cli/bin/ua.js changelog", "changelog:check": "node packages/tools/cli/bin/ua.js changelog --check", - "sync-counts": "node packages/tools/cli/bin/ua.js sync-counts" + "sync-counts": "node packages/tools/cli/bin/ua.js sync-counts", + "audit:contrast": "node packages/tools/cli/bin/ua.js audit:contrast" }, "devDependencies": { "typescript": "^5.6.0" diff --git a/packages/design/tokens/tokens.css b/packages/design/tokens/tokens.css index 0c13ccf..cd1e1c1 100644 --- a/packages/design/tokens/tokens.css +++ b/packages/design/tokens/tokens.css @@ -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); /* 暗色淡底标准档:暗底上淡底不可见,透明度升一档 */ diff --git a/packages/design/tokens/tokens.json b/packages/design/tokens/tokens.json index 5037140..c51d406 100644 --- a/packages/design/tokens/tokens.json +++ b/packages/design/tokens/tokens.json @@ -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", diff --git a/packages/tools/cli/bin/ua.js b/packages/tools/cli/bin/ua.js index be764d4..5a7725e 100644 --- a/packages/tools/cli/bin/ua.js +++ b/packages/tools/cli/bin/ua.js @@ -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 |info |docs|tokens [--json]|style|sync-counts|verify|doctor|commit-lint|lint-iterations|audit:visual [--json|--baseline]|changelog|mcp>'); + console.log('usage: ua |info |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); } diff --git a/packages/tools/cli/lib/contrast-audit.mjs b/packages/tools/cli/lib/contrast-audit.mjs new file mode 100644 index 0000000..78e1426 --- /dev/null +++ b/packages/tools/cli/lib/contrast-audit.mjs @@ -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'); +} diff --git a/packages/tools/cli/ua.manifest.json b/packages/tools/cli/ua.manifest.json index 0a01e12..42865a6 100644 --- a/packages/tools/cli/ua.manifest.json +++ b/packages/tools/cli/ua.manifest.json @@ -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 |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 |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": []