iter(g233): 位移量令牌化,transform 位移收敛为 --ua-translate-* 七档并纳入审计口径

This commit is contained in:
HIsheR
2026-10-26 21:16:22 +08:00
parent 39f53041e4
commit 0eb67c48fe
9 changed files with 106 additions and 44 deletions
+2 -2
View File
@@ -1,6 +1,6 @@
{ {
"$schema": "ui-arch-registry-index/0.1", "$schema": "ui-arch-registry-index/0.1",
"generatedAt": "2026-10-26T12:45:44.604Z", "generatedAt": "2026-10-26T13:15:52.353Z",
"repoRoot": ".", "repoRoot": ".",
"packages": [ "packages": [
{ {
@@ -3884,7 +3884,7 @@
"接手先跑 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)",
"查组件:ua list 看全量,ua search <关键词> 按 id/标题/参数名/参数说明/步骤五层 + 配方三层(标题/配方组件/要点)模糊找(命中非 id 标题会标出命中字段,配方命中行直接给 ua recipes <编号> 指路;拼错会回相近 id),ua info <组件id> 看 props/步骤/示例(ua info <包名> 看单包 api 与约定,加 --json 出原始清单)", "查组件: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 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,焦点环用 --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,transform 位移用 --ua-translate-*(负号 calc(-1 * var(...))),焦点环用 --ua-focus-ring(_inset)",
"改完清单跑 npm run docs:同时刷新人类文档站与 llms-full.txt(Agent 首选喂料)", "改完清单跑 npm run docs:同时刷新人类文档站与 llms-full.txt(Agent 首选喂料)",
"改完样式跑 npm run audit:visual:各类硬编码计数不得高于 .ua/visual-baseline.json(棘轮只减不增);字面量可用 npm run visual:migrate 幂等改写成令牌", "改完样式跑 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" "在 ~/.workbuddy/mcp.json 注册 ua mcp 后,Agent 可直接调 list_components / get_component / search_components / get_tokens / list_recipes / get_recipe"
+5 -4
View File
@@ -1,9 +1,9 @@
{ {
"$schema": "视觉漂移基线:各类硬编码视觉字面量的允许上限,任一类超出即 ua audit:visual 退出码 1;迁移后用 --baseline 重写", "$schema": "视觉漂移基线:各类硬编码视觉字面量的允许上限,任一类超出即 ua audit:visual 退出码 1;迁移后用 --baseline 重写",
"file": "packages/skin/ui/ui.css", "file": "packages/skin/ui/ui.css",
"commit": "4f2ee02", "commit": "39f5304",
"generatedAt": "2026-10-26T09:57:48.041Z", "generatedAt": "2026-10-26T13:15:16.867Z",
"lines": 4874, "lines": 5186,
"total": 0, "total": 0,
"counts": { "counts": {
"font-size-literal": 0, "font-size-literal": 0,
@@ -17,6 +17,7 @@
"leading-literal": 0, "leading-literal": 0,
"z-literal": 0, "z-literal": 0,
"border-literal": 0, "border-literal": 0,
"mix-literal": 0 "mix-literal": 0,
"translate-literal": 0
} }
} }
+1 -1
View File
@@ -27,7 +27,7 @@
- `apps/demo`(@ui-arch/demo):交互演示,产物 apps/demo/dist/demo.html - `apps/demo`(@ui-arch/demo):交互演示,产物 apps/demo/dist/demo.html
- `apps/docs`:文档站,纯产物(ua docs 生成) - `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` 统计皮肤包里五类硬编码视觉字面量(字号 / 动效时长 / 阴影 / 圆角 / 焦点环)并与 `.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 的 `<!-- ua:components -->` 区块与「分组:」行都覆盖清单组件集合(漏写/多写/重复都报错),并交叉校验 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,提交前必过。其中 `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 的 `<!-- ua:components -->` 区块与「分组:」行都覆盖清单组件集合(漏写/多写/重复都报错),并交叉校验 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 条且五维度全覆盖——视觉类条目收尾前照单过一遍,改清单时别把条目删漏。 `ua verify` 另外校验 `ITERATION.md` 的《视觉回归抽查清单》:button / textField / select / modal / dataTable / toast 六段各 ≥3 条可勾选要点、每条维度标签必须落在边框 / 阴影 / 过渡 / 焦点环 / 暗色五类内、合计 ≥18 条且五维度全覆盖——视觉类条目收尾前照单过一遍,改清单时别把条目删漏。
+9 -2
View File
@@ -1,6 +1,6 @@
# @ui-arch 组件库 Agent 文档(无图形版) # @ui-arch 组件库 Agent 文档(无图形版)
> 自动生成于 2026-10-26T12:45:44.604Z,数据源:各包 ua.manifest.json,勿手改。人类版文档站:apps/docs/index.html。 > 自动生成于 2026-10-26T13:15:52.353Z,数据源:各包 ua.manifest.json,勿手改。人类版文档站:apps/docs/index.html。
> 组件 = 普通函数,传 props 返回 DOM;状态用 signal 双向绑定:读 signal() 写 signal(v)。 > 组件 = 普通函数,传 props 返回 DOM;状态用 signal 双向绑定:读 signal() 写 signal(v)。
## 快速上手 ## 快速上手
@@ -43,7 +43,7 @@
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)
2. 查组件:ua list 看全量,ua search <关键词> 按 id/标题/参数名/参数说明/步骤五层 + 配方三层(标题/配方组件/要点)模糊找(命中非 id 标题会标出命中字段,配方命中行直接给 ua recipes <编号> 指路;拼错会回相近 id),ua info <组件id> 看 props/步骤/示例(ua info <包名> 看单包 api 与约定,加 --json 出原始清单) 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) 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,焦点环用 --ua-focus-ring(_inset) 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 首选喂料) 5. 改完清单跑 npm run docs:同时刷新人类文档站与 llms-full.txt(Agent 首选喂料)
6. 改完样式跑 npm run audit:visual:各类硬编码计数不得高于 .ua/visual-baseline.json(棘轮只减不增);字面量可用 npm run visual:migrate 幂等改写成令牌 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. 在 ~/.workbuddy/mcp.json 注册 ua mcp 后,Agent 可直接调 list_components / get_component / search_components / get_tokens / list_recipes / get_recipe
@@ -2300,6 +2300,13 @@ node packages/tools/cli/bin/ua.js <index|list|info <包名|组件id>|docs|tokens
- --ua-ease: cubic-bezier(0.2, 0.8, 0.2, 1) - --ua-ease: cubic-bezier(0.2, 0.8, 0.2, 1)
- --ua-ease-out: cubic-bezier(0.16, 1, 0.3, 1) - --ua-ease-out: cubic-bezier(0.16, 1, 0.3, 1)
- --ua-ease-spring: cubic-bezier(0.34, 1.3, 0.5, 1) - --ua-ease-spring: cubic-bezier(0.34, 1.3, 0.5, 1)
- --ua-translate-1: 1px
- --ua-translate-2: 2px
- --ua-translate-3: 3px
- --ua-translate-4: 4px
- --ua-translate-6: 6px
- --ua-translate-8: 8px
- --ua-translate-12: 12px
- --ua-focus-ring: 0 0 0 3px color-mix(in srgb, var(--ua-color-accent) 22%, transparent)(暗色 0 0 0 3px color-mix(in srgb, var(--ua-color-accent) 34%, transparent)) - --ua-focus-ring: 0 0 0 3px color-mix(in srgb, var(--ua-color-accent) 22%, transparent)(暗色 0 0 0 3px color-mix(in srgb, var(--ua-color-accent) 34%, transparent))
- --ua-focus-ring-inset: inset 0 0 0 2px color-mix(in srgb, var(--ua-color-accent) 40%, transparent)(暗色 inset 0 0 0 2px color-mix(in srgb, var(--ua-color-accent) 52%, transparent)) - --ua-focus-ring-inset: inset 0 0 0 2px color-mix(in srgb, var(--ua-color-accent) 40%, transparent)(暗色 inset 0 0 0 2px color-mix(in srgb, var(--ua-color-accent) 52%, transparent))
- --ua-focus-ring-warning: 0 0 0 3px color-mix(in srgb, var(--ua-color-warning) 25%, transparent)(暗色 0 0 0 3px color-mix(in srgb, var(--ua-color-warning) 34%, transparent)) - --ua-focus-ring-warning: 0 0 0 3px color-mix(in srgb, var(--ua-color-warning) 25%, transparent)(暗色 0 0 0 3px color-mix(in srgb, var(--ua-color-warning) 34%, transparent))
+7
View File
@@ -110,6 +110,13 @@
--ua-ease: cubic-bezier(0.2, 0.8, 0.2, 1); /* 标准缓动:先快后缓,控件交互默认用这条 */ --ua-ease: cubic-bezier(0.2, 0.8, 0.2, 1); /* 标准缓动:先快后缓,控件交互默认用这条 */
--ua-ease-out: cubic-bezier(0.16, 1, 0.3, 1); /* 出场缓动:元素进入视口 / 弹层出现时使用 */ --ua-ease-out: cubic-bezier(0.16, 1, 0.3, 1); /* 出场缓动:元素进入视口 / 弹层出现时使用 */
--ua-ease-spring: cubic-bezier(0.34, 1.3, 0.5, 1); /* 回弹缓动:弹窗本体、悬浮按钮这类需要一点过冲的入场 */ --ua-ease-spring: cubic-bezier(0.34, 1.3, 0.5, 1); /* 回弹缓动:弹窗本体、悬浮按钮这类需要一点过冲的入场 */
--ua-translate-1: 1px; /* 位移 1 档:hover 微上浮(行内小件) */
--ua-translate-2: 2px; /* 位移 2 档:hover 上浮标准档(按钮 / 卡片 / 悬浮件) */
--ua-translate-3: 3px; /* 位移 3 档:贴边入场偏移(tooltip 箭头侧滑等几何配对位移) */
--ua-translate-4: 4px; /* 位移 4 档:小型弹层入场(下拉菜单 / 气泡滑入) */
--ua-translate-6: 6px; /* 位移 6 档:入场位移中档(alert / 通知条滑入) */
--ua-translate-8: 8px; /* 位移 8 档:浮动件入场(悬浮按钮弹出 / 大弹层滑入;既有 10px 收敛到此档) */
--ua-translate-12: 12px; /* 位移 12 档:长距离滑入(toast 侧边进出场 / 开关行程) */
/* ---- 焦点环:统一外环与内嵌两种形态,全部由强调色派生 ---- */ /* ---- 焦点环:统一外环与内嵌两种形态,全部由强调色派生 ---- */
--ua-focus-ring: 0 0 0 3px color-mix(in srgb, var(--ua-color-accent) 22%, transparent); /* 外焦点环:输入框 / 按钮 / 可聚焦控件 */ --ua-focus-ring: 0 0 0 3px color-mix(in srgb, var(--ua-color-accent) 22%, transparent); /* 外焦点环:输入框 / 按钮 / 可聚焦控件 */
+9
View File
@@ -213,6 +213,15 @@
"easeOut": "cubic-bezier(0.16, 1, 0.3, 1)", "easeOut": "cubic-bezier(0.16, 1, 0.3, 1)",
"easeSpring": "cubic-bezier(0.34, 1.3, 0.5, 1)" "easeSpring": "cubic-bezier(0.34, 1.3, 0.5, 1)"
}, },
"translate": {
"1": "1px",
"2": "2px",
"3": "3px",
"4": "4px",
"6": "6px",
"8": "8px",
"12": "12px"
},
"focus": { "focus": {
"ring": { "ring": {
"light": "0 0 0 3px color-mix(in srgb, var(--ua-color-accent) 22%, transparent)", "light": "0 0 0 3px color-mix(in srgb, var(--ua-color-accent) 22%, transparent)",
+33 -33
View File
@@ -43,7 +43,7 @@
} }
.ua-btn:hover { .ua-btn:hover {
filter: brightness(1.08); /* 悬停提亮 + 主题色投影 + 内高光,三层叠出光泽 */ filter: brightness(1.08); /* 悬停提亮 + 主题色投影 + 内高光,三层叠出光泽 */
transform: translateY(-1px); transform: translateY(calc(-1 * var(--ua-translate-1)));
box-shadow: var(--ua-shadow-accent), var(--ua-shadow-highlight); box-shadow: var(--ua-shadow-accent), var(--ua-shadow-highlight);
} }
.ua-btn:active { .ua-btn:active {
@@ -127,7 +127,7 @@
transition: transform var(--ua-duration-fast) var(--ua-ease), box-shadow var(--ua-duration-fast) var(--ua-ease), border-color var(--ua-duration-fast) var(--ua-ease); transition: transform var(--ua-duration-fast) var(--ua-ease), box-shadow var(--ua-duration-fast) var(--ua-ease), border-color var(--ua-duration-fast) var(--ua-ease);
} }
.ua-card-hoverable:hover { .ua-card-hoverable:hover {
transform: translateY(-2px); transform: translateY(calc(-1 * var(--ua-translate-2)));
box-shadow: var(--ua-shadow-2); /* 悬停浮起:从 1 档升到 2 档,一档之差刚好可感 */ box-shadow: var(--ua-shadow-2); /* 悬停浮起:从 1 档升到 2 档,一档之差刚好可感 */
border-color: color-mix(in srgb, var(--ua-color-border) var(--ua-alpha-strong), transparent); /* 阴影接手层次,边框同步减淡,两者配合而非叠加 */ border-color: color-mix(in srgb, var(--ua-color-border) var(--ua-alpha-strong), transparent); /* 阴影接手层次,边框同步减淡,两者配合而非叠加 */
} }
@@ -412,14 +412,14 @@ body.ua-col-resizing {
background: var(--ua-color-surface); background: var(--ua-color-surface);
box-shadow: var(--ua-shadow-2); box-shadow: var(--ua-shadow-2);
opacity: 0; opacity: 0;
transform: translateY(-4px); transform: translateY(calc(-1 * var(--ua-translate-4)));
visibility: hidden; visibility: hidden;
transition: opacity var(--ua-duration-fast) var(--ua-ease), transform var(--ua-duration-fast) var(--ua-ease), visibility var(--ua-duration-fast) var(--ua-ease); transition: opacity var(--ua-duration-fast) var(--ua-ease), transform var(--ua-duration-fast) var(--ua-ease), visibility var(--ua-duration-fast) var(--ua-ease);
} }
.ua-select-pop.is-flip { .ua-select-pop.is-flip {
top: auto; /* 空间不足翻到触发器上方 */ top: auto; /* 空间不足翻到触发器上方 */
bottom: calc(100% + 4px); bottom: calc(100% + 4px);
transform: translateY(4px); /* 入场方向随翻转镜像:从下方 4px 滑入 */ transform: translateY(var(--ua-translate-4)); /* 入场方向随翻转镜像:从下方 4px 滑入 */
} }
.ua-select-pop.ua-open { .ua-select-pop.ua-open {
opacity: 1; opacity: 1;
@@ -529,7 +529,7 @@ body.ua-col-resizing {
background: var(--ua-color-surface); background: var(--ua-color-surface);
box-shadow: var(--ua-shadow-2); box-shadow: var(--ua-shadow-2);
opacity: 0; opacity: 0;
transform: translateY(-4px); transform: translateY(calc(-1 * var(--ua-translate-4)));
visibility: hidden; visibility: hidden;
transition: opacity var(--ua-duration-fast) var(--ua-ease), transform var(--ua-duration-fast) var(--ua-ease), visibility var(--ua-duration-fast) var(--ua-ease); transition: opacity var(--ua-duration-fast) var(--ua-ease), transform var(--ua-duration-fast) var(--ua-ease), visibility var(--ua-duration-fast) var(--ua-ease);
} }
@@ -550,7 +550,7 @@ body.ua-col-resizing {
.ua-menu-pop.is-flip { .ua-menu-pop.is-flip {
top: auto; /* 空间不足翻到触发器上方 */ top: auto; /* 空间不足翻到触发器上方 */
bottom: calc(100% + 4px); bottom: calc(100% + 4px);
transform: translateY(4px); /* 入场方向随翻转镜像:从下方 4px 滑入 */ transform: translateY(var(--ua-translate-4)); /* 入场方向随翻转镜像:从下方 4px 滑入 */
} }
.ua-menu-pop.ua-open { .ua-menu-pop.ua-open {
opacity: 1; opacity: 1;
@@ -699,11 +699,11 @@ body.ua-col-resizing {
} }
.ua-pop-top { .ua-pop-top {
bottom: calc(100% + 8px); bottom: calc(100% + 8px);
transform: translateY(4px); transform: translateY(var(--ua-translate-4));
} }
.ua-pop-bottom { .ua-pop-bottom {
top: calc(100% + 8px); top: calc(100% + 8px);
transform: translateY(-4px); transform: translateY(calc(-1 * var(--ua-translate-4)));
} }
.ua-pop.ua-open { .ua-pop.ua-open {
opacity: 1; opacity: 1;
@@ -877,7 +877,7 @@ body.ua-col-resizing {
transition: transform var(--ua-duration-fast) var(--ua-ease), border-color var(--ua-duration-fast) var(--ua-ease), background var(--ua-duration-fast) var(--ua-ease), color var(--ua-duration-fast) var(--ua-ease); transition: transform var(--ua-duration-fast) var(--ua-ease), border-color var(--ua-duration-fast) var(--ua-ease), background var(--ua-duration-fast) var(--ua-ease), color var(--ua-duration-fast) var(--ua-ease);
} }
.ua-tag.is-checkable:hover { .ua-tag.is-checkable:hover {
transform: translateY(-1px); transform: translateY(calc(-1 * var(--ua-translate-1)));
color: var(--ua-color-text); color: var(--ua-color-text);
} }
.ua-tag.is-checkable:active { .ua-tag.is-checkable:active {
@@ -1099,7 +1099,7 @@ button.ua-step-dot {
button.ua-step-dot:hover { button.ua-step-dot:hover {
border-color: var(--ua-color-accent); border-color: var(--ua-color-accent);
color: var(--ua-color-accent); color: var(--ua-color-accent);
transform: translateY(-1px); transform: translateY(calc(-1 * var(--ua-translate-1)));
} }
.ua-step-done .ua-step-dot { .ua-step-done .ua-step-dot {
border-color: color-mix(in srgb, var(--ua-color-accent) var(--ua-alpha-strong), transparent); border-color: color-mix(in srgb, var(--ua-color-accent) var(--ua-alpha-strong), transparent);
@@ -1299,7 +1299,7 @@ button.ua-step-dot:hover {
@keyframes ua-slide-in { @keyframes ua-slide-in {
from { from {
opacity: 0; opacity: 0;
transform: translateY(-6px); transform: translateY(calc(-1 * var(--ua-translate-6)));
} }
} }
.ua-alert-icon { .ua-alert-icon {
@@ -1347,7 +1347,7 @@ button.ua-step-dot:hover {
/* 关闭退场:淡出 + 轻微上移,配合 200ms 后摘除节点 */ /* 关闭退场:淡出 + 轻微上移,配合 200ms 后摘除节点 */
.ua-alert-out { .ua-alert-out {
opacity: 0; opacity: 0;
transform: translateY(-4px); transform: translateY(calc(-1 * var(--ua-translate-4)));
} }
.ua-alert-info { .ua-alert-info {
border-color: var(--ua-color-accent); border-color: var(--ua-color-accent);
@@ -1421,7 +1421,7 @@ button.ua-step-dot:hover {
} }
.ua-page-btn:hover:not(:disabled) { .ua-page-btn:hover:not(:disabled) {
border-color: var(--ua-color-accent); border-color: var(--ua-color-accent);
transform: translateY(-1px); transform: translateY(calc(-1 * var(--ua-translate-1)));
} }
/* 按下:回位落底,取消悬停浮起并加深底色 */ /* 按下:回位落底,取消悬停浮起并加深底色 */
.ua-page-btn:active:not(:disabled) { .ua-page-btn:active:not(:disabled) {
@@ -1511,7 +1511,7 @@ button.ua-step-dot:hover {
border: 1px solid; border: 1px solid;
font-size: var(--ua-font-size-md); font-size: var(--ua-font-size-md);
opacity: 0; opacity: 0;
transform: translateX(12px); transform: translateX(var(--ua-translate-12));
transition: opacity var(--ua-duration-base) var(--ua-ease-out), transform var(--ua-duration-base) var(--ua-ease-out); transition: opacity var(--ua-duration-base) var(--ua-ease-out), transform var(--ua-duration-base) var(--ua-ease-out);
} }
.ua-toast.ua-open { .ua-toast.ua-open {
@@ -1958,17 +1958,17 @@ button.ua-step-dot:hover {
} }
/* 浮层入场:自上而下 4px 淡入(触发在下方、弹层向下展开);翻转态改用 ua-pop-rise 反向 */ /* 浮层入场:自上而下 4px 淡入(触发在下方、弹层向下展开);翻转态改用 ua-pop-rise 反向 */
@keyframes ua-pop-drop { @keyframes ua-pop-drop {
from { opacity: 0; transform: translateY(-4px); } from { opacity: 0; transform: translateY(calc(-1 * var(--ua-translate-4))); }
to { opacity: 1; transform: none; } to { opacity: 1; transform: none; }
} }
@keyframes ua-pop-rise { @keyframes ua-pop-rise {
from { opacity: 0; transform: translateY(4px); } from { opacity: 0; transform: translateY(var(--ua-translate-4)); }
to { opacity: 1; transform: none; } to { opacity: 1; transform: none; }
} }
@keyframes ua-pop-in { @keyframes ua-pop-in {
from { from {
opacity: 0; opacity: 0;
transform: translateY(10px) scale(0.96); transform: translateY(var(--ua-translate-8)) scale(0.96);
} }
} }
.ua-modal-head { .ua-modal-head {
@@ -2030,22 +2030,22 @@ button.ua-step-dot:hover {
.ua-tip-top { .ua-tip-top {
left: 50%; left: 50%;
bottom: calc(100% + 6px); bottom: calc(100% + 6px);
transform: translate(-50%, 3px); transform: translate(-50%, var(--ua-translate-3));
} }
.ua-tip-bottom { .ua-tip-bottom {
left: 50%; left: 50%;
top: calc(100% + 6px); top: calc(100% + 6px);
transform: translate(-50%, -3px); transform: translate(-50%, calc(-1 * var(--ua-translate-3)));
} }
.ua-tip-left { .ua-tip-left {
right: calc(100% + 6px); right: calc(100% + 6px);
top: 50%; top: 50%;
transform: translate(3px, -50%); transform: translate(var(--ua-translate-3), -50%);
} }
.ua-tip-right { .ua-tip-right {
left: calc(100% + 6px); left: calc(100% + 6px);
top: 50%; top: 50%;
transform: translate(-3px, -50%); transform: translate(calc(-1 * var(--ua-translate-3)), -50%);
} }
.ua-tip.ua-open { .ua-tip.ua-open {
opacity: 1; opacity: 1;
@@ -2142,7 +2142,7 @@ button.ua-step-dot:hover {
} }
.ua-image:hover, .ua-image:hover,
.ua-image:focus-visible { .ua-image:focus-visible {
transform: translateY(-2px); transform: translateY(calc(-1 * var(--ua-translate-2)));
box-shadow: var(--ua-shadow-2); box-shadow: var(--ua-shadow-2);
outline: none; outline: none;
} }
@@ -2388,7 +2388,7 @@ button.ua-step-dot:hover {
line-height: var(--ua-leading-normal); line-height: var(--ua-leading-normal);
} }
@keyframes ua-tl-in { @keyframes ua-tl-in {
from { opacity: 0; transform: translateY(-4px); } from { opacity: 0; transform: translateY(calc(-1 * var(--ua-translate-4))); }
to { opacity: 1; transform: translateY(0); } to { opacity: 1; transform: translateY(0); }
} }
@keyframes ua-tl-pulse { @keyframes ua-tl-pulse {
@@ -2934,7 +2934,7 @@ button.ua-step-dot:hover {
box-shadow: var(--ua-shadow-2); box-shadow: var(--ua-shadow-2);
pointer-events: auto; pointer-events: auto;
opacity: 0; opacity: 0;
transform: translateY(8px); transform: translateY(var(--ua-translate-8));
transition: opacity var(--ua-duration-base) var(--ua-ease), transform var(--ua-duration-base) var(--ua-ease); transition: opacity var(--ua-duration-base) var(--ua-ease), transform var(--ua-duration-base) var(--ua-ease);
} }
.ua-notice-open { .ua-notice-open {
@@ -3589,7 +3589,7 @@ button.ua-step-dot:hover {
border-radius: 50%; border-radius: 50%;
cursor: pointer; cursor: pointer;
opacity: 0; opacity: 0;
transform: translateY(8px) scale(0.9); transform: translateY(var(--ua-translate-8)) scale(0.9);
pointer-events: none; pointer-events: none;
transition: opacity var(--ua-duration-base) var(--ua-ease), transform var(--ua-duration-base) var(--ua-ease); transition: opacity var(--ua-duration-base) var(--ua-ease), transform var(--ua-duration-base) var(--ua-ease);
box-shadow: var(--ua-shadow-accent-strong); /* 悬浮强调件主题色投影 */ box-shadow: var(--ua-shadow-accent-strong); /* 悬浮强调件主题色投影 */
@@ -3600,7 +3600,7 @@ button.ua-step-dot:hover {
pointer-events: auto; pointer-events: auto;
} }
.ua-backtop.is-visible:hover { .ua-backtop.is-visible:hover {
transform: translateY(-2px); transform: translateY(calc(-1 * var(--ua-translate-2)));
} }
.ua-backtop:focus-visible { .ua-backtop:focus-visible {
outline: 2px solid var(--ua-color-accent); outline: 2px solid var(--ua-color-accent);
@@ -3710,7 +3710,7 @@ button.ua-step-dot:hover {
transition: transform var(--ua-duration-fast) var(--ua-ease), box-shadow var(--ua-duration-fast) var(--ua-ease); transition: transform var(--ua-duration-fast) var(--ua-ease), box-shadow var(--ua-duration-fast) var(--ua-ease);
} }
.ua-colorpicker-cell:hover { .ua-colorpicker-cell:hover {
transform: translateY(-1px); transform: translateY(calc(-1 * var(--ua-translate-1)));
} }
.ua-colorpicker-cell.is-selected { .ua-colorpicker-cell.is-selected {
box-shadow: var(--ua-focus-ring-selected); box-shadow: var(--ua-focus-ring-selected);
@@ -3821,7 +3821,7 @@ button.ua-step-dot:hover {
gap: var(--ua-space-1); gap: var(--ua-space-1);
} }
@keyframes ua-tour-in { @keyframes ua-tour-in {
from { opacity: 0; transform: translateY(4px); } from { opacity: 0; transform: translateY(var(--ua-translate-4)); }
to { opacity: 1; transform: translateY(0); } to { opacity: 1; transform: translateY(0); }
} }
@media (prefers-reduced-motion: reduce) { @media (prefers-reduced-motion: reduce) {
@@ -4069,7 +4069,7 @@ button.ua-step-dot:hover {
.ua-anchor-link:hover { .ua-anchor-link:hover {
color: var(--ua-color-text); color: var(--ua-color-text);
background: var(--ua-color-tint-accent-weak); background: var(--ua-color-tint-accent-weak);
transform: translateX(2px); transform: translateX(var(--ua-translate-2));
} }
.ua-anchor-link.is-active { .ua-anchor-link.is-active {
color: var(--ua-color-text); color: var(--ua-color-text);
@@ -4276,7 +4276,7 @@ button.ua-step-dot:hover {
} }
.ua-calendar-day:hover { .ua-calendar-day:hover {
background: var(--ua-color-tint-accent-weak); background: var(--ua-color-tint-accent-weak);
transform: translateY(-1px); transform: translateY(calc(-1 * var(--ua-translate-1)));
} }
/* 按下:底色转中性深一档,浮起交由悬停态负责 */ /* 按下:底色转中性深一档,浮起交由悬停态负责 */
.ua-calendar-day:active { .ua-calendar-day:active {
@@ -4468,7 +4468,7 @@ h5.ua-type-title { font-size: var(--ua-font-size-md); }
.ua-fab:hover { .ua-fab:hover {
color: var(--ua-color-accent); color: var(--ua-color-accent);
border-color: var(--ua-color-accent); border-color: var(--ua-color-accent);
transform: translateY(-2px); transform: translateY(calc(-1 * var(--ua-translate-2)));
box-shadow: var(--ua-shadow-4); /* 悬停抬升 = 最高层档 */ box-shadow: var(--ua-shadow-4); /* 悬停抬升 = 最高层档 */
} }
.ua-fab:active { .ua-fab:active {
@@ -5076,14 +5076,14 @@ h5.ua-type-title { font-size: var(--ua-font-size-md); }
background: var(--ua-color-surface); background: var(--ua-color-surface);
box-shadow: var(--ua-shadow-2); box-shadow: var(--ua-shadow-2);
opacity: 0; opacity: 0;
transform: translateY(-4px); transform: translateY(calc(-1 * var(--ua-translate-4)));
visibility: hidden; visibility: hidden;
transition: opacity var(--ua-duration-fast) var(--ua-ease), transform var(--ua-duration-fast) var(--ua-ease), visibility var(--ua-duration-fast) var(--ua-ease); transition: opacity var(--ua-duration-fast) var(--ua-ease), transform var(--ua-duration-fast) var(--ua-ease), visibility var(--ua-duration-fast) var(--ua-ease);
} }
.ua-timepicker-pop.is-flip { .ua-timepicker-pop.is-flip {
top: auto; /* 空间不足翻到触发器上方 */ top: auto; /* 空间不足翻到触发器上方 */
bottom: calc(100% + 4px); bottom: calc(100% + 4px);
transform: translateY(4px); /* 入场方向随翻转镜像 */ transform: translateY(var(--ua-translate-4)); /* 入场方向随翻转镜像 */
} }
.ua-timepicker-pop.ua-open { .ua-timepicker-pop.ua-open {
opacity: 1; opacity: 1;
+39 -1
View File
@@ -71,6 +71,11 @@ export const KIND_META = [
label: '混色浓度字面值', label: '混色浓度字面值',
hint: 'color-mix 的混入浓度写死百分比,应改用 --ua-alpha-* 六档(soft/tint/line/mid/strong/heavy)', hint: 'color-mix 的混入浓度写死百分比,应改用 --ua-alpha-* 六档(soft/tint/line/mid/strong/heavy)',
}, },
{
key: 'translate-literal',
label: '位移字面值',
hint: 'transform 的 translate 写死 px 位移,应改用 --ua-translate-* 档位(负号用 calc(-1 * var(...)),百分比居中豁免)',
},
]; ];
/*** 作用:需要扫描的 CSS 属性白名单(其余属性本轮不动;用白名单顺带避开伪类选择器误判) ***/ /*** 作用:需要扫描的 CSS 属性白名单(其余属性本轮不动;用白名单顺带避开伪类选择器误判) ***/
@@ -117,6 +122,7 @@ const SCAN_PROPS = new Set([
'border-right-color', 'border-right-color',
'border-bottom-color', 'border-bottom-color',
'border-left-color', 'border-left-color',
'transform',
]); ]);
/*** 作用:边框类属性集合(轮廓与分割线共用一套色值口径,禁用字面色) ***/ /*** 作用:边框类属性集合(轮廓与分割线共用一套色值口径,禁用字面色) ***/
@@ -264,6 +270,18 @@ const MIX_TOKENS = new Map([
['80', 'var(--ua-alpha-heavy)'], ['80', 'var(--ua-alpha-heavy)'],
]); ]);
/*** 作用:transform 位移字面值 → --ua-translate-* 档位(10px 就近收敛到 8 档,Δ≤2px 入场不可感;负号由迁移规则转 calc) ***/
const TRANSLATE_TOKENS = new Map([
['1px', 'var(--ua-translate-1)'],
['2px', 'var(--ua-translate-2)'],
['3px', 'var(--ua-translate-3)'],
['4px', 'var(--ua-translate-4)'],
['6px', 'var(--ua-translate-6)'],
['8px', 'var(--ua-translate-8)'],
['10px', 'var(--ua-translate-8)'],
['12px', 'var(--ua-translate-12)'],
]);
/*** 作用:提取值里所有 color-mix(...) 块(配平括号,嵌套 color-mix 逐层都算一条);返回全块文本与首色参(in srgb 之后的第一个顶层参数) ***/ /*** 作用:提取值里所有 color-mix(...) 块(配平括号,嵌套 color-mix 逐层都算一条);返回全块文本与首色参(in srgb 之后的第一个顶层参数) ***/
function extractMixes(value) { function extractMixes(value) {
const out = []; const out = [];
@@ -340,7 +358,7 @@ export function blankComments(src) {
/*** 作用:逐条产出「属性 + 值」及其在原文中的偏移;只认白名单属性且必须以分号收尾(避开伪类选择器) ***/ /*** 作用:逐条产出「属性 + 值」及其在原文中的偏移;只认白名单属性且必须以分号收尾(避开伪类选择器) ***/
export function eachDeclaration(code) { 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|background-image|background|color|fill|stroke)\s*:\s*([^;{}]*);/gi; const re = /(transform|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 = []; const list = [];
for (const m of code.matchAll(re)) { for (const m of code.matchAll(re)) {
const prop = m[1].toLowerCase(); const prop = m[1].toLowerCase();
@@ -467,6 +485,19 @@ function migrateLeading(core) {
return { kind: 'leading-literal', value: token }; return { kind: 'leading-literal', value: token };
} }
/*** 作用:迁移单条 transform:px 位移逐个换 --ua-translate-* 档位,负号转 calc(-1 * var(...));百分比居中与 0 天然不入表 ***/
function migrateTranslate(core) {
let hits = 0;
const next = String(core).replace(/(-?\d+(?:\.\d+)?)px\b/gi, (full, num) => {
const token = TRANSLATE_TOKENS.get(`${Math.abs(Number(num))}px`);
if (!token) return full; // 表外取值留给后续条目评估
hits++;
return Number(num) < 0 ? `calc(-1 * ${token})` : token;
});
if (!hits) return null;
return { kind: 'translate-literal', value: next };
}
/*** 作用:迁移单条层级:下拉类常见字面值(20/30/40)归 dropdown 档;其余取值层级语义需按选择器人工归档,留给门禁拦截 ***/ /*** 作用:迁移单条层级:下拉类常见字面值(20/30/40)归 dropdown 档;其余取值层级语义需按选择器人工归档,留给门禁拦截 ***/
function migrateZ(core) { function migrateZ(core) {
const token = Z_DROPDOWN_LIKE.has(squash(core)) ? 'var(--ua-z-dropdown)' : null; const token = Z_DROPDOWN_LIKE.has(squash(core)) ? 'var(--ua-z-dropdown)' : null;
@@ -507,6 +538,8 @@ export function migrateDeclaration(prop, rawValue) {
result = migrateLeading(core); result = migrateLeading(core);
} else if (prop === 'z-index') { } else if (prop === 'z-index') {
result = migrateZ(core); result = migrateZ(core);
} else if (prop === 'transform') {
result = migrateTranslate(core); // 位移量走独立档位,不与内距梯子混用
} else { } else {
result = migrateMix(core) ?? migrateBorder(prop, core); // 其余属性:先收敛 color-mix 浓度,再收敛纯黑 rgba 边 result = migrateMix(core) ?? migrateBorder(prop, core); // 其余属性:先收敛 color-mix 浓度,再收敛纯黑 rgba 边
} }
@@ -585,6 +618,11 @@ function detect(prop, rawValue, key) {
if (key === 'mix-literal') { if (key === 'mix-literal') {
return extractMixes(value).filter(({ arg1 }) => mixAmount(arg1) !== null).length; // 只认 color-mix 块内首色参的写死浓度;渐变停靠位与几何百分比天然不在口径内 return extractMixes(value).filter(({ arg1 }) => mixAmount(arg1) !== null).length; // 只认 color-mix 块内首色参的写死浓度;渐变停靠位与几何百分比天然不在口径内
} }
if (key === 'translate-literal') {
if (prop !== 'transform') return 0;
const stripped = value.replace(/var\(--ua-translate-[\w-]+\)/g, ' '); // 已令牌化的位移先剔除
return (stripped.match(/-?\d+(\.\d+)?px\b/gi) ?? []).length; // 百分比(居中/轨道)与 0 不算漂移
}
return 0; 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 doctor(node 版本 / 依赖 / git 钩子 / .ua 元数据一次检完,秒级),再跑 npm run check 确认基线全绿(build → demo → style → typecheck → smoke → index → verify → lint-iterations → audit:visual)",
"查组件:ua list 看全量,ua search <关键词> 按 id/标题/参数名/参数说明/步骤五层 + 配方三层(标题/配方组件/要点)模糊找(命中非 id 标题会标出命中字段,配方命中行直接给 ua recipes <编号> 指路;拼错会回相近 id),ua info <组件id> 看 props/步骤/示例(ua info <包名> 看单包 api 与约定,加 --json 出原始清单)", "查组件: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 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,焦点环用 --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,transform 位移用 --ua-translate-*(负号 calc(-1 * var(...))),焦点环用 --ua-focus-ring(_inset)",
"改完清单跑 npm run docs:同时刷新人类文档站与 llms-full.txt(Agent 首选喂料)", "改完清单跑 npm run docs:同时刷新人类文档站与 llms-full.txt(Agent 首选喂料)",
"改完样式跑 npm run audit:visual:各类硬编码计数不得高于 .ua/visual-baseline.json(棘轮只减不增);字面量可用 npm run visual:migrate 幂等改写成令牌", "改完样式跑 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" "在 ~/.workbuddy/mcp.json 注册 ua mcp 后,Agent 可直接调 list_components / get_component / search_components / get_tokens / list_recipes / get_recipe"