iter(g239): 焦点描边令牌化,outline 收敛为三档复合令牌并纳入审计第十四类

This commit is contained in:
HIsheR
2026-10-26 21:37:57 +08:00
parent 9e85ef3f12
commit 53338960be
7 changed files with 73 additions and 25 deletions
+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": "39f5304", "commit": "9e85ef3",
"generatedAt": "2026-10-26T13:15:16.867Z", "generatedAt": "2026-10-26T13:36:51.222Z",
"lines": 5186, "lines": 5241,
"total": 0, "total": 0,
"counts": { "counts": {
"font-size-literal": 0, "font-size-literal": 0,
@@ -18,6 +18,7 @@
"z-literal": 0, "z-literal": 0,
"border-literal": 0, "border-literal": 0,
"mix-literal": 0, "mix-literal": 0,
"translate-literal": 0 "translate-literal": 0,
"outline-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` 统计皮肤包里十三类硬编码视觉字面量(字号 / 动效时长 / 阴影 / 圆角 / 焦点环 / 内距 / 控件高度 / 字重 / 行高 / 层级 / 边框色 / 混色浓度 / 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` 会把它列成空组件表)。 **门禁一条龙**:`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 条且五维度全覆盖——视觉类条目收尾前照单过一遍,改清单时别把条目删漏。
+3
View File
@@ -2435,6 +2435,9 @@ node packages/tools/cli/bin/ua.js <index|list|info <包名|组件id>|docs|tokens
- --ua-focus-ring-danger: 0 0 0 3px color-mix(in srgb, var(--ua-color-danger) 18%, transparent)(暗色 0 0 0 3px color-mix(in srgb, var(--ua-color-danger) 30%, transparent)) - --ua-focus-ring-danger: 0 0 0 3px color-mix(in srgb, var(--ua-color-danger) 18%, transparent)(暗色 0 0 0 3px color-mix(in srgb, var(--ua-color-danger) 30%, transparent))
- --ua-focus-ring-pulse: 0 0 0 5px color-mix(in srgb, var(--ua-color-accent) 4%, transparent)(暗色 0 0 0 5px color-mix(in srgb, var(--ua-color-accent) 8%, transparent)) - --ua-focus-ring-pulse: 0 0 0 5px color-mix(in srgb, var(--ua-color-accent) 4%, transparent)(暗色 0 0 0 5px color-mix(in srgb, var(--ua-color-accent) 8%, transparent))
- --ua-focus-ring-selected: 0 0 0 1.5px var(--ua-color-accent-soft) - --ua-focus-ring-selected: 0 0 0 1.5px var(--ua-color-accent-soft)
- --ua-focus-outline: 2px solid var(--ua-color-accent)
- --ua-focus-outline-inset: 1.5px solid var(--ua-color-accent-soft)
- --ua-focus-outline-soft: 2px solid color-mix(in srgb, var(--ua-color-accent) var(--ua-alpha-mid), transparent)
- --ua-control-h-sm: 28px - --ua-control-h-sm: 28px
- --ua-control-h-md: 34px - --ua-control-h-md: 34px
- --ua-control-h-lg: 40px - --ua-control-h-lg: 40px
+6
View File
@@ -125,6 +125,9 @@
--ua-focus-ring-danger: 0 0 0 3px color-mix(in srgb, var(--ua-color-danger) 18%, transparent); /* 错误强调环:校验失败时叠在被标红控件外侧,比 danger 描边更外扩一圈 */ --ua-focus-ring-danger: 0 0 0 3px color-mix(in srgb, var(--ua-color-danger) 18%, transparent); /* 错误强调环:校验失败时叠在被标红控件外侧,比 danger 描边更外扩一圈 */
--ua-focus-ring-pulse: 0 0 0 5px color-mix(in srgb, var(--ua-color-accent) 4%, transparent); /* 呼吸光晕档:与焦点环交替出现做脉冲动画(时间线条目) */ --ua-focus-ring-pulse: 0 0 0 5px color-mix(in srgb, var(--ua-color-accent) 4%, transparent); /* 呼吸光晕档:与焦点环交替出现做脉冲动画(时间线条目) */
--ua-focus-ring-selected: 0 0 0 1.5px var(--ua-color-accent-soft); /* 选中环(非焦点):细一档 + 柔和色,2px 实色在色板/网格里像加粗边框 */ --ua-focus-ring-selected: 0 0 0 1.5px var(--ua-color-accent-soft); /* 选中环(非焦点):细一档 + 柔和色,2px 实色在色板/网格里像加粗边框 */
--ua-focus-outline: 2px solid var(--ua-color-accent); /* 外焦点描边(outline 路线):键盘可见性标准档 */
--ua-focus-outline-inset: 1.5px solid var(--ua-color-accent-soft); /* 内嵌焦点描边:密集列表细一档 + 柔和色,2px 实线像粗边框 */
--ua-focus-outline-soft: 2px solid color-mix(in srgb, var(--ua-color-accent) var(--ua-alpha-mid), transparent); /* 柔和外描边:大面积控件用淡化主题色 */
/* ---- 控件高度:三档,略放大以提升可点面积 ---- */ /* ---- 控件高度:三档,略放大以提升可点面积 ---- */
--ua-control-h-sm: 28px; /* 小控件高度:紧凑工具栏、标签内嵌按钮 */ --ua-control-h-sm: 28px; /* 小控件高度:紧凑工具栏、标签内嵌按钮 */
@@ -182,4 +185,7 @@
--ua-focus-ring-danger: 0 0 0 3px color-mix(in srgb, var(--ua-color-danger) 30%, transparent); /* 暗色错误强调环 */ --ua-focus-ring-danger: 0 0 0 3px color-mix(in srgb, var(--ua-color-danger) 30%, transparent); /* 暗色错误强调环 */
--ua-focus-ring-pulse: 0 0 0 5px color-mix(in srgb, var(--ua-color-accent) 8%, transparent); /* 暗色呼吸光晕:暗底上低透明度看不见,加倍 */ --ua-focus-ring-pulse: 0 0 0 5px color-mix(in srgb, var(--ua-color-accent) 8%, transparent); /* 暗色呼吸光晕:暗底上低透明度看不见,加倍 */
--ua-focus-ring-selected: 0 0 0 1.5px var(--ua-color-accent-soft); /* 暗色选中环:随 accent 翻转并与亮色同口径 */ --ua-focus-ring-selected: 0 0 0 1.5px var(--ua-color-accent-soft); /* 暗色选中环:随 accent 翻转并与亮色同口径 */
--ua-focus-outline: 2px solid var(--ua-color-accent); /* 暗色外焦点描边:随 accent 翻转,与亮色同口径 */
--ua-focus-outline-inset: 1.5px solid var(--ua-color-accent-soft); /* 暗色内嵌焦点描边:accent-soft 随底色重混 */
--ua-focus-outline-soft: 2px solid color-mix(in srgb, var(--ua-color-accent) var(--ua-alpha-mid), transparent); /* 暗色柔和外描边:浓度档与亮色一致 */
} }
+12
View File
@@ -246,6 +246,18 @@
"ringSelected": { "ringSelected": {
"light": "0 0 0 2px var(--ua-color-accent)", "light": "0 0 0 2px var(--ua-color-accent)",
"dark": "0 0 0 2px var(--ua-color-accent)" "dark": "0 0 0 2px var(--ua-color-accent)"
},
"outline": {
"light": "2px solid var(--ua-color-accent)",
"dark": "2px solid var(--ua-color-accent)"
},
"outlineInset": {
"light": "1.5px solid var(--ua-color-accent-soft)",
"dark": "1.5px solid var(--ua-color-accent-soft)"
},
"outlineSoft": {
"light": "2px solid color-mix(in srgb, var(--ua-color-accent) var(--ua-alpha-mid), transparent)",
"dark": "2px solid color-mix(in srgb, var(--ua-color-accent) var(--ua-alpha-mid), transparent)"
} }
}, },
"control": { "control": {
+20 -20
View File
@@ -3,7 +3,7 @@
/* —— 键盘焦点可见性:组件普遍用 outline:none 自绘,这里统一兜底,避免键盘用户丢失焦点指示 —— */ /* —— 键盘焦点可见性:组件普遍用 outline:none 自绘,这里统一兜底,避免键盘用户丢失焦点指示 —— */
[class^='ua-']:focus-visible, [class^='ua-']:focus-visible,
[class*=' ua-']:focus-visible { [class*=' ua-']:focus-visible {
outline: 2px solid var(--ua-color-accent); outline: var(--ua-focus-outline);
outline-offset: 2px; outline-offset: 2px;
} }
/* —— 控件高度三档:所有可交互控件统一走 --ua-control-h-*,size 档只改高度 —— */ /* —— 控件高度三档:所有可交互控件统一走 --ua-control-h-*,size 档只改高度 —— */
@@ -485,7 +485,7 @@ body.ua-col-resizing {
但两者的密集列表换成内嵌描边后更整齐;与 cascader 项保持同一口径 */ 但两者的密集列表换成内嵌描边后更整齐;与 cascader 项保持同一口径 */
.ua-option:focus-visible, .ua-option:focus-visible,
.ua-menu-item:focus-visible { .ua-menu-item:focus-visible {
outline: 1.5px solid var(--ua-color-accent-soft); /* 内嵌焦点环:细一档 + 柔和色,2px 实色在密集列表里像粗边框 */ outline: var(--ua-focus-outline-inset); /* 内嵌焦点环:细一档 + 柔和色,2px 实色在密集列表里像粗边框 */
outline-offset: -1.5px; outline-offset: -1.5px;
} }
/* 选中态:低视觉比重——淡色底 + 弱化文字,不抢焦点 */ /* 选中态:低视觉比重——淡色底 + 弱化文字,不抢焦点 */
@@ -884,7 +884,7 @@ body.ua-col-resizing {
transform: scale(0.97); transform: scale(0.97);
} }
.ua-tag.is-checkable:focus-visible { .ua-tag.is-checkable:focus-visible {
outline: 2px solid color-mix(in srgb, var(--ua-color-accent) var(--ua-alpha-mid), transparent); outline: var(--ua-focus-outline-soft);
outline-offset: 2px; outline-offset: 2px;
} }
.ua-tag.is-checked { .ua-tag.is-checked {
@@ -1138,7 +1138,7 @@ button.ua-step-dot:hover {
gap: var(--ua-space-05); gap: var(--ua-space-05);
} }
.ua-rate:focus-visible { .ua-rate:focus-visible {
outline: 2px solid color-mix(in srgb, var(--ua-color-accent) var(--ua-alpha-mid), transparent); outline: var(--ua-focus-outline-soft);
outline-offset: 3px; outline-offset: 3px;
border-radius: var(--ua-radius-sm); border-radius: var(--ua-radius-sm);
} }
@@ -1676,7 +1676,7 @@ button.ua-step-dot:hover {
.ua-tab:focus-visible { .ua-tab:focus-visible {
/* 页签条是 overflow-x:auto 的滚动容器,外偏移焦点环会被裁掉, /* 页签条是 overflow-x:auto 的滚动容器,外偏移焦点环会被裁掉,
这里改用与 seg-btn 一致的内嵌描边,键盘用户才真看得见焦点 */ 这里改用与 seg-btn 一致的内嵌描边,键盘用户才真看得见焦点 */
outline: 1.5px solid var(--ua-color-accent-soft); /* 内嵌焦点环:细一档 + 柔和色,2px 实色在密集列表里像粗边框 */ outline: var(--ua-focus-outline-inset); /* 内嵌焦点环:细一档 + 柔和色,2px 实色在密集列表里像粗边框 */
outline-offset: -1.5px; outline-offset: -1.5px;
color: var(--ua-color-accent); color: var(--ua-color-accent);
} }
@@ -1717,7 +1717,7 @@ button.ua-step-dot:hover {
} }
.ua-tab-close:focus-visible { .ua-tab-close:focus-visible {
opacity: 1; opacity: 1;
outline: 2px solid var(--ua-color-accent); outline: var(--ua-focus-outline);
outline-offset: 1px; outline-offset: 1px;
} }
.ua-tab-close:hover { .ua-tab-close:hover {
@@ -2441,7 +2441,7 @@ button.ua-step-dot:hover {
color: var(--ua-color-text); color: var(--ua-color-text);
} }
.ua-seg-btn:focus-visible { .ua-seg-btn:focus-visible {
outline: 1.5px solid var(--ua-color-accent-soft); /* 内嵌焦点环:细一档 + 柔和色,2px 实色在密集列表里像粗边框 */ outline: var(--ua-focus-outline-inset); /* 内嵌焦点环:细一档 + 柔和色,2px 实色在密集列表里像粗边框 */
outline-offset: -1.5px; outline-offset: -1.5px;
} }
.ua-seg-btn.is-active { .ua-seg-btn.is-active {
@@ -3250,7 +3250,7 @@ button.ua-step-dot:hover {
cursor: not-allowed; cursor: not-allowed;
} }
.ua-cascader-opt:focus-visible { .ua-cascader-opt:focus-visible {
outline: 1.5px solid var(--ua-color-accent-soft); /* 内嵌焦点环:细一档 + 柔和色,2px 实色在密集列表里像粗边框 */ outline: var(--ua-focus-outline-inset); /* 内嵌焦点环:细一档 + 柔和色,2px 实色在密集列表里像粗边框 */
outline-offset: -1.5px; outline-offset: -1.5px;
} }
.ua-cascader-arrow { .ua-cascader-arrow {
@@ -3305,7 +3305,7 @@ button.ua-step-dot:hover {
background: var(--ua-color-accent); background: var(--ua-color-accent);
} }
.ua-splitter-bar:focus-visible { .ua-splitter-bar:focus-visible {
outline: 2px solid var(--ua-color-accent); outline: var(--ua-focus-outline);
outline-offset: 1px; outline-offset: 1px;
} }
@media (prefers-reduced-motion: reduce) { @media (prefers-reduced-motion: reduce) {
@@ -3526,7 +3526,7 @@ button.ua-step-dot:hover {
cursor: not-allowed; cursor: not-allowed;
} }
.ua-datepicker-day:focus-visible { .ua-datepicker-day:focus-visible {
outline: 1.5px solid var(--ua-color-accent-soft); /* 内嵌焦点环:细一档 + 柔和色,2px 实色在密集列表里像粗边框 */ outline: var(--ua-focus-outline-inset); /* 内嵌焦点环:细一档 + 柔和色,2px 实色在密集列表里像粗边框 */
outline-offset: -1.5px; outline-offset: -1.5px;
} }
.ua-datepicker-foot { .ua-datepicker-foot {
@@ -3603,7 +3603,7 @@ button.ua-step-dot:hover {
transform: translateY(calc(-1 * var(--ua-translate-2))); transform: translateY(calc(-1 * var(--ua-translate-2)));
} }
.ua-backtop:focus-visible { .ua-backtop:focus-visible {
outline: 2px solid var(--ua-color-accent); outline: var(--ua-focus-outline);
outline-offset: 3px; outline-offset: 3px;
} }
.ua-backtop-ring { .ua-backtop-ring {
@@ -4091,7 +4091,7 @@ button.ua-step-dot:hover {
pointer-events: none; pointer-events: none;
} }
.ua-anchor-link:focus-visible { .ua-anchor-link:focus-visible {
outline: 2px solid var(--ua-color-accent); outline: var(--ua-focus-outline);
outline-offset: 2px; outline-offset: 2px;
} }
@media (prefers-reduced-motion: reduce) { @media (prefers-reduced-motion: reduce) {
@@ -4160,7 +4160,7 @@ button.ua-step-dot:hover {
background: var(--ua-color-tint-accent); background: var(--ua-color-tint-accent);
} }
.ua-select-clear:focus-visible { .ua-select-clear:focus-visible {
outline: 2px solid var(--ua-color-accent); outline: var(--ua-focus-outline);
outline-offset: 1px; outline-offset: 1px;
} }
.ua-select-clear[hidden] { .ua-select-clear[hidden] {
@@ -4183,7 +4183,7 @@ button.ua-step-dot:hover {
background: var(--ua-color-tint-accent-weak); background: var(--ua-color-tint-accent-weak);
} }
.ua-crumb-fold:focus-visible { .ua-crumb-fold:focus-visible {
outline: 2px solid var(--ua-color-accent); outline: var(--ua-focus-outline);
outline-offset: 1px; outline-offset: 1px;
} }
@@ -4205,7 +4205,7 @@ button.ua-step-dot:hover {
text-decoration: underline; text-decoration: underline;
} }
.ua-toast-action:focus-visible { .ua-toast-action:focus-visible {
outline: 2px solid var(--ua-color-accent); outline: var(--ua-focus-outline);
outline-offset: 2px; outline-offset: 2px;
} }
@@ -4245,7 +4245,7 @@ button.ua-step-dot:hover {
background: var(--ua-color-tint-accent-weak); background: var(--ua-color-tint-accent-weak);
} }
.ua-calendar-nav:focus-visible { .ua-calendar-nav:focus-visible {
outline: 2px solid var(--ua-color-accent); outline: var(--ua-focus-outline);
outline-offset: 1px; outline-offset: 1px;
} }
.ua-calendar-week, .ua-calendar-week,
@@ -4292,7 +4292,7 @@ button.ua-step-dot:hover {
transform: none; transform: none;
} }
.ua-calendar-day:focus-visible { .ua-calendar-day:focus-visible {
outline: 2px solid var(--ua-color-accent); outline: var(--ua-focus-outline);
outline-offset: 1px; outline-offset: 1px;
} }
.ua-calendar-mark { .ua-calendar-mark {
@@ -4475,7 +4475,7 @@ h5.ua-type-title { font-size: var(--ua-font-size-md); }
transform: translateY(0) scale(0.97); transform: translateY(0) scale(0.97);
} }
.ua-fab:focus-visible { .ua-fab:focus-visible {
outline: 2px solid var(--ua-color-accent); outline: var(--ua-focus-outline);
outline-offset: 2px; outline-offset: 2px;
} }
.ua-fab-icon { .ua-fab-icon {
@@ -4677,7 +4677,7 @@ h5.ua-type-title { font-size: var(--ua-font-size-md); }
text-decoration: underline; text-decoration: underline;
} }
.ua-link:focus-visible { .ua-link:focus-visible {
outline: 2px solid var(--ua-color-accent); outline: var(--ua-focus-outline);
outline-offset: 2px; outline-offset: 2px;
border-radius: var(--ua-radius-sm); border-radius: var(--ua-radius-sm);
} }
@@ -4730,7 +4730,7 @@ h5.ua-type-title { font-size: var(--ua-font-size-md); }
background: var(--ua-color-tint-accent-weak); background: var(--ua-color-tint-accent-weak);
} }
.ua-row-clickable:focus-visible { .ua-row-clickable:focus-visible {
outline: 1.5px solid var(--ua-color-accent-soft); /* 内嵌焦点环:细一档 + 柔和色,2px 实色在密集列表里像粗边框 */ outline: var(--ua-focus-outline-inset); /* 内嵌焦点环:细一档 + 柔和色,2px 实色在密集列表里像粗边框 */
outline-offset: -1.5px; /* 环画在行内,避免被表格滚动容器裁掉 */ outline-offset: -1.5px; /* 环画在行内,避免被表格滚动容器裁掉 */
} }
+26
View File
@@ -76,6 +76,11 @@ export const KIND_META = [
label: '位移字面值', label: '位移字面值',
hint: 'transform 的 translate 写死 px 位移,应改用 --ua-translate-* 档位(负号用 calc(-1 * var(...)),百分比居中豁免)', hint: 'transform 的 translate 写死 px 位移,应改用 --ua-translate-* 档位(负号用 calc(-1 * var(...)),百分比居中豁免)',
}, },
{
key: 'outline-literal',
label: '焦点描边字面值',
hint: 'outline 写死宽度与色值,应改用 --ua-focus-outline(_inset/_soft) 三档(outline: none 豁免)',
},
]; ];
/*** 作用:需要扫描的 CSS 属性白名单(其余属性本轮不动;用白名单顺带避开伪类选择器误判) ***/ /*** 作用:需要扫描的 CSS 属性白名单(其余属性本轮不动;用白名单顺带避开伪类选择器误判) ***/
@@ -485,6 +490,20 @@ function migrateLeading(core) {
return { kind: 'leading-literal', value: token }; return { kind: 'leading-literal', value: token };
} }
/*** 作用:outline 焦点描边字面值 → 三档复合令牌(none 是「移除描边」语义不入表;outline-offset 是逐控件几何微调,不走此表) ***/
const OUTLINE_TOKENS = new Map([
['2px solid var(--ua-color-accent)', 'var(--ua-focus-outline)'],
['1.5px solid var(--ua-color-accent-soft)', 'var(--ua-focus-outline-inset)'],
['2px solid color-mix(in srgb, var(--ua-color-accent) var(--ua-alpha-mid), transparent)', 'var(--ua-focus-outline-soft)'],
]);
/*** 作用:迁移单条 outline:整值命中三档即换令牌,其余写法留给门禁拦截 ***/
function migrateOutline(core) {
const token = OUTLINE_TOKENS.get(squash(core));
if (!token) return null;
return { kind: 'outline-literal', value: token };
}
/*** 作用:迁移单条 transform:px 位移逐个换 --ua-translate-* 档位,负号转 calc(-1 * var(...));百分比居中与 0 天然不入表 ***/ /*** 作用:迁移单条 transform:px 位移逐个换 --ua-translate-* 档位,负号转 calc(-1 * var(...));百分比居中与 0 天然不入表 ***/
function migrateTranslate(core) { function migrateTranslate(core) {
let hits = 0; let hits = 0;
@@ -540,6 +559,8 @@ export function migrateDeclaration(prop, rawValue) {
result = migrateZ(core); result = migrateZ(core);
} else if (prop === 'transform') { } else if (prop === 'transform') {
result = migrateTranslate(core); // 位移量走独立档位,不与内距梯子混用 result = migrateTranslate(core); // 位移量走独立档位,不与内距梯子混用
} else if (prop === 'outline') {
result = migrateOutline(core); // 焦点描边整值收敛三档
} else { } else {
result = migrateMix(core) ?? migrateBorder(prop, core); // 其余属性:先收敛 color-mix 浓度,再收敛纯黑 rgba 边 result = migrateMix(core) ?? migrateBorder(prop, core); // 其余属性:先收敛 color-mix 浓度,再收敛纯黑 rgba 边
} }
@@ -623,6 +644,11 @@ function detect(prop, rawValue, key) {
const stripped = value.replace(/var\(--ua-translate-[\w-]+\)/g, ' '); // 已令牌化的位移先剔除 const stripped = value.replace(/var\(--ua-translate-[\w-]+\)/g, ' '); // 已令牌化的位移先剔除
return (stripped.match(/-?\d+(\.\d+)?px\b/gi) ?? []).length; // 百分比(居中/轨道)与 0 不算漂移 return (stripped.match(/-?\d+(\.\d+)?px\b/gi) ?? []).length; // 百分比(居中/轨道)与 0 不算漂移
} }
if (key === 'outline-literal') {
if (prop !== 'outline') return 0;
if (value === 'none') return 0; // 移除描边是语义写法,不算漂移
return /^var\(/.test(value) ? 0 : /\d+(\.\d+)?px\b/.test(value) ? 1 : 0; // 整值已走令牌不算漂移
}
return 0; return 0;
} }