From 0eb67c48fe789eb5512d3da3435fcbc5dcae6021 Mon Sep 17 00:00:00 2001 From: HIsheR Date: Mon, 26 Oct 2026 21:16:22 +0800 Subject: [PATCH] =?UTF-8?q?iter(g233):=20=E4=BD=8D=E7=A7=BB=E9=87=8F?= =?UTF-8?q?=E4=BB=A4=E7=89=8C=E5=8C=96=EF=BC=8Ctransform=20=E4=BD=8D?= =?UTF-8?q?=E7=A7=BB=E6=94=B6=E6=95=9B=E4=B8=BA=20--ua-translate-*=20?= =?UTF-8?q?=E4=B8=83=E6=A1=A3=E5=B9=B6=E7=BA=B3=E5=85=A5=E5=AE=A1=E8=AE=A1?= =?UTF-8?q?=E5=8F=A3=E5=BE=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .ua/index.json | 4 +- .ua/visual-baseline.json | 9 ++-- AGENTS.md | 2 +- llms-full.txt | 11 ++++- packages/design/tokens/tokens.css | 7 +++ packages/design/tokens/tokens.json | 9 ++++ packages/skin/ui/ui.css | 66 ++++++++++++------------- packages/tools/cli/lib/visual-rules.mjs | 40 ++++++++++++++- packages/tools/cli/ua.manifest.json | 2 +- 9 files changed, 106 insertions(+), 44 deletions(-) diff --git a/.ua/index.json b/.ua/index.json index 1ba0069..e5c254c 100644 --- a/.ua/index.json +++ b/.ua/index.json @@ -1,6 +1,6 @@ { "$schema": "ui-arch-registry-index/0.1", - "generatedAt": "2026-10-26T12:45:44.604Z", + "generatedAt": "2026-10-26T13:15:52.353Z", "repoRoot": ".", "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 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,焦点环用 --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 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" diff --git a/.ua/visual-baseline.json b/.ua/visual-baseline.json index bb98653..210fe77 100644 --- a/.ua/visual-baseline.json +++ b/.ua/visual-baseline.json @@ -1,9 +1,9 @@ { "$schema": "视觉漂移基线:各类硬编码视觉字面量的允许上限,任一类超出即 ua audit:visual 退出码 1;迁移后用 --baseline 重写", "file": "packages/skin/ui/ui.css", - "commit": "4f2ee02", - "generatedAt": "2026-10-26T09:57:48.041Z", - "lines": 4874, + "commit": "39f5304", + "generatedAt": "2026-10-26T13:15:16.867Z", + "lines": 5186, "total": 0, "counts": { "font-size-literal": 0, @@ -17,6 +17,7 @@ "leading-literal": 0, "z-literal": 0, "border-literal": 0, - "mix-literal": 0 + "mix-literal": 0, + "translate-literal": 0 } } diff --git a/AGENTS.md b/AGENTS.md index 671641a..a18b705 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -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` 统计皮肤包里五类硬编码视觉字面量(字号 / 动效时长 / 阴影 / 圆角 / 焦点环)并与 `.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,提交前必过。其中 `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` 会把它列成空组件表)。 `ua verify` 另外校验 `ITERATION.md` 的《视觉回归抽查清单》:button / textField / select / modal / dataTable / toast 六段各 ≥3 条可勾选要点、每条维度标签必须落在边框 / 阴影 / 过渡 / 焦点环 / 暗色五类内、合计 ≥18 条且五维度全覆盖——视觉类条目收尾前照单过一遍,改清单时别把条目删漏。 diff --git a/llms-full.txt b/llms-full.txt index a909164..8a23e8c 100644 --- a/llms-full.txt +++ b/llms-full.txt @@ -1,6 +1,6 @@ # @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)。 ## 快速上手 @@ -43,7 +43,7 @@ 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 出原始清单) 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 首选喂料) 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 @@ -2300,6 +2300,13 @@ node packages/tools/cli/bin/ua.js |docs|tokens - --ua-ease: cubic-bezier(0.2, 0.8, 0.2, 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-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-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)) diff --git a/packages/design/tokens/tokens.css b/packages/design/tokens/tokens.css index dc4f9c5..c28ba76 100644 --- a/packages/design/tokens/tokens.css +++ b/packages/design/tokens/tokens.css @@ -110,6 +110,13 @@ --ua-ease: cubic-bezier(0.2, 0.8, 0.2, 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-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); /* 外焦点环:输入框 / 按钮 / 可聚焦控件 */ diff --git a/packages/design/tokens/tokens.json b/packages/design/tokens/tokens.json index da7efad..9320176 100644 --- a/packages/design/tokens/tokens.json +++ b/packages/design/tokens/tokens.json @@ -213,6 +213,15 @@ "easeOut": "cubic-bezier(0.16, 1, 0.3, 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": { "ring": { "light": "0 0 0 3px color-mix(in srgb, var(--ua-color-accent) 22%, transparent)", diff --git a/packages/skin/ui/ui.css b/packages/skin/ui/ui.css index 2d7fec6..5cd6f28 100644 --- a/packages/skin/ui/ui.css +++ b/packages/skin/ui/ui.css @@ -43,7 +43,7 @@ } .ua-btn:hover { 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); } .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); } .ua-card-hoverable:hover { - transform: translateY(-2px); + transform: translateY(calc(-1 * var(--ua-translate-2))); box-shadow: var(--ua-shadow-2); /* 悬停浮起:从 1 档升到 2 档,一档之差刚好可感 */ 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); box-shadow: var(--ua-shadow-2); opacity: 0; - transform: translateY(-4px); + transform: translateY(calc(-1 * var(--ua-translate-4))); 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); } .ua-select-pop.is-flip { top: auto; /* 空间不足翻到触发器上方 */ bottom: calc(100% + 4px); - transform: translateY(4px); /* 入场方向随翻转镜像:从下方 4px 滑入 */ + transform: translateY(var(--ua-translate-4)); /* 入场方向随翻转镜像:从下方 4px 滑入 */ } .ua-select-pop.ua-open { opacity: 1; @@ -529,7 +529,7 @@ body.ua-col-resizing { background: var(--ua-color-surface); box-shadow: var(--ua-shadow-2); opacity: 0; - transform: translateY(-4px); + transform: translateY(calc(-1 * var(--ua-translate-4))); 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); } @@ -550,7 +550,7 @@ body.ua-col-resizing { .ua-menu-pop.is-flip { top: auto; /* 空间不足翻到触发器上方 */ bottom: calc(100% + 4px); - transform: translateY(4px); /* 入场方向随翻转镜像:从下方 4px 滑入 */ + transform: translateY(var(--ua-translate-4)); /* 入场方向随翻转镜像:从下方 4px 滑入 */ } .ua-menu-pop.ua-open { opacity: 1; @@ -699,11 +699,11 @@ body.ua-col-resizing { } .ua-pop-top { bottom: calc(100% + 8px); - transform: translateY(4px); + transform: translateY(var(--ua-translate-4)); } .ua-pop-bottom { top: calc(100% + 8px); - transform: translateY(-4px); + transform: translateY(calc(-1 * var(--ua-translate-4))); } .ua-pop.ua-open { 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); } .ua-tag.is-checkable:hover { - transform: translateY(-1px); + transform: translateY(calc(-1 * var(--ua-translate-1))); color: var(--ua-color-text); } .ua-tag.is-checkable:active { @@ -1099,7 +1099,7 @@ button.ua-step-dot { button.ua-step-dot:hover { border-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 { 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 { from { opacity: 0; - transform: translateY(-6px); + transform: translateY(calc(-1 * var(--ua-translate-6))); } } .ua-alert-icon { @@ -1347,7 +1347,7 @@ button.ua-step-dot:hover { /* 关闭退场:淡出 + 轻微上移,配合 200ms 后摘除节点 */ .ua-alert-out { opacity: 0; - transform: translateY(-4px); + transform: translateY(calc(-1 * var(--ua-translate-4))); } .ua-alert-info { border-color: var(--ua-color-accent); @@ -1421,7 +1421,7 @@ button.ua-step-dot:hover { } .ua-page-btn:hover:not(:disabled) { border-color: var(--ua-color-accent); - transform: translateY(-1px); + transform: translateY(calc(-1 * var(--ua-translate-1))); } /* 按下:回位落底,取消悬停浮起并加深底色 */ .ua-page-btn:active:not(:disabled) { @@ -1511,7 +1511,7 @@ button.ua-step-dot:hover { border: 1px solid; font-size: var(--ua-font-size-md); 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); } .ua-toast.ua-open { @@ -1958,17 +1958,17 @@ button.ua-step-dot:hover { } /* 浮层入场:自上而下 4px 淡入(触发在下方、弹层向下展开);翻转态改用 ua-pop-rise 反向 */ @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; } } @keyframes ua-pop-rise { - from { opacity: 0; transform: translateY(4px); } + from { opacity: 0; transform: translateY(var(--ua-translate-4)); } to { opacity: 1; transform: none; } } @keyframes ua-pop-in { from { opacity: 0; - transform: translateY(10px) scale(0.96); + transform: translateY(var(--ua-translate-8)) scale(0.96); } } .ua-modal-head { @@ -2030,22 +2030,22 @@ button.ua-step-dot:hover { .ua-tip-top { left: 50%; bottom: calc(100% + 6px); - transform: translate(-50%, 3px); + transform: translate(-50%, var(--ua-translate-3)); } .ua-tip-bottom { left: 50%; top: calc(100% + 6px); - transform: translate(-50%, -3px); + transform: translate(-50%, calc(-1 * var(--ua-translate-3))); } .ua-tip-left { right: calc(100% + 6px); top: 50%; - transform: translate(3px, -50%); + transform: translate(var(--ua-translate-3), -50%); } .ua-tip-right { left: calc(100% + 6px); top: 50%; - transform: translate(-3px, -50%); + transform: translate(calc(-1 * var(--ua-translate-3)), -50%); } .ua-tip.ua-open { opacity: 1; @@ -2142,7 +2142,7 @@ button.ua-step-dot:hover { } .ua-image:hover, .ua-image:focus-visible { - transform: translateY(-2px); + transform: translateY(calc(-1 * var(--ua-translate-2))); box-shadow: var(--ua-shadow-2); outline: none; } @@ -2388,7 +2388,7 @@ button.ua-step-dot:hover { line-height: var(--ua-leading-normal); } @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); } } @keyframes ua-tl-pulse { @@ -2934,7 +2934,7 @@ button.ua-step-dot:hover { box-shadow: var(--ua-shadow-2); pointer-events: auto; 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); } .ua-notice-open { @@ -3589,7 +3589,7 @@ button.ua-step-dot:hover { border-radius: 50%; cursor: pointer; opacity: 0; - transform: translateY(8px) scale(0.9); + transform: translateY(var(--ua-translate-8)) scale(0.9); pointer-events: none; transition: opacity var(--ua-duration-base) var(--ua-ease), transform var(--ua-duration-base) var(--ua-ease); box-shadow: var(--ua-shadow-accent-strong); /* 悬浮强调件主题色投影 */ @@ -3600,7 +3600,7 @@ button.ua-step-dot:hover { pointer-events: auto; } .ua-backtop.is-visible:hover { - transform: translateY(-2px); + transform: translateY(calc(-1 * var(--ua-translate-2))); } .ua-backtop:focus-visible { 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); } .ua-colorpicker-cell:hover { - transform: translateY(-1px); + transform: translateY(calc(-1 * var(--ua-translate-1))); } .ua-colorpicker-cell.is-selected { box-shadow: var(--ua-focus-ring-selected); @@ -3821,7 +3821,7 @@ button.ua-step-dot:hover { gap: var(--ua-space-1); } @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); } } @media (prefers-reduced-motion: reduce) { @@ -4069,7 +4069,7 @@ button.ua-step-dot:hover { .ua-anchor-link:hover { color: var(--ua-color-text); background: var(--ua-color-tint-accent-weak); - transform: translateX(2px); + transform: translateX(var(--ua-translate-2)); } .ua-anchor-link.is-active { color: var(--ua-color-text); @@ -4276,7 +4276,7 @@ button.ua-step-dot:hover { } .ua-calendar-day:hover { background: var(--ua-color-tint-accent-weak); - transform: translateY(-1px); + transform: translateY(calc(-1 * var(--ua-translate-1))); } /* 按下:底色转中性深一档,浮起交由悬停态负责 */ .ua-calendar-day:active { @@ -4468,7 +4468,7 @@ h5.ua-type-title { font-size: var(--ua-font-size-md); } .ua-fab:hover { 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); /* 悬停抬升 = 最高层档 */ } .ua-fab:active { @@ -5076,14 +5076,14 @@ h5.ua-type-title { font-size: var(--ua-font-size-md); } background: var(--ua-color-surface); box-shadow: var(--ua-shadow-2); opacity: 0; - transform: translateY(-4px); + transform: translateY(calc(-1 * var(--ua-translate-4))); 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); } .ua-timepicker-pop.is-flip { top: auto; /* 空间不足翻到触发器上方 */ bottom: calc(100% + 4px); - transform: translateY(4px); /* 入场方向随翻转镜像 */ + transform: translateY(var(--ua-translate-4)); /* 入场方向随翻转镜像 */ } .ua-timepicker-pop.ua-open { opacity: 1; diff --git a/packages/tools/cli/lib/visual-rules.mjs b/packages/tools/cli/lib/visual-rules.mjs index 74d925d..d77c090 100644 --- a/packages/tools/cli/lib/visual-rules.mjs +++ b/packages/tools/cli/lib/visual-rules.mjs @@ -71,6 +71,11 @@ export const KIND_META = [ label: '混色浓度字面值', 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 属性白名单(其余属性本轮不动;用白名单顺带避开伪类选择器误判) ***/ @@ -117,6 +122,7 @@ const SCAN_PROPS = new Set([ 'border-right-color', 'border-bottom-color', 'border-left-color', + 'transform', ]); /*** 作用:边框类属性集合(轮廓与分割线共用一套色值口径,禁用字面色) ***/ @@ -264,6 +270,18 @@ const MIX_TOKENS = new Map([ ['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 之后的第一个顶层参数) ***/ function extractMixes(value) { const out = []; @@ -340,7 +358,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|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 = []; for (const m of code.matchAll(re)) { const prop = m[1].toLowerCase(); @@ -467,6 +485,19 @@ function migrateLeading(core) { 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 档;其余取值层级语义需按选择器人工归档,留给门禁拦截 ***/ function migrateZ(core) { 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); } else if (prop === 'z-index') { result = migrateZ(core); + } else if (prop === 'transform') { + result = migrateTranslate(core); // 位移量走独立档位,不与内距梯子混用 } else { result = migrateMix(core) ?? migrateBorder(prop, core); // 其余属性:先收敛 color-mix 浓度,再收敛纯黑 rgba 边 } @@ -585,6 +618,11 @@ function detect(prop, rawValue, key) { if (key === 'mix-literal') { 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; } diff --git a/packages/tools/cli/ua.manifest.json b/packages/tools/cli/ua.manifest.json index bbae595..0a01e12 100644 --- a/packages/tools/cli/ua.manifest.json +++ b/packages/tools/cli/ua.manifest.json @@ -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 标题会标出命中字段,配方命中行直接给 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,焦点环用 --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 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"