iter(g255): 减少动效下归零全站悬停位移
This commit is contained in:
@@ -5295,3 +5295,17 @@ h5.ua-type-title { font-size: var(--ua-font-size-md); }
|
||||
width: 100%; /* 不确定态降级为满宽淡条:不再左右扫,语义仍是「进行中」 */
|
||||
}
|
||||
}
|
||||
|
||||
/* ---- 减少动效:悬停上浮 / 按压回缩一律归零 ----
|
||||
位移是最容易诱发前庭不适的一类动效,且它只提供「手感」、不表达状态;
|
||||
颜色 / 描边 / 阴影过渡保留,用户要减的是「动」,不是反馈。
|
||||
用 ua- 类通配写一条全局规则,新增组件不再需要各自补降级(audit:motion 认这条作全量兜底)。 */
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
[class^='ua-']:hover,
|
||||
[class*=' ua-']:hover,
|
||||
[class^='ua-']:active,
|
||||
[class*=' ua-']:active {
|
||||
transform: none !important; /* 上浮 / 缩放 / 回缩全部归零 */
|
||||
translate: none !important; /* 少数组件用独立 translate 属性做位移,一并归零 */
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1712,12 +1712,13 @@ function cmdAuditMotion(args) {
|
||||
console.error(`audit:motion 失败:${err.message}`);
|
||||
process.exit(1);
|
||||
}
|
||||
const failing = result.gaps.length + result.shift.gaps.length; // 两类缺口都算失败:无限动画 + 悬停/按压位移
|
||||
if (args.includes('--json')) {
|
||||
console.log(JSON.stringify({ ok: result.gaps.length === 0, total: result.total, failing: result.gaps.length, entries: result.entries }, null, 2));
|
||||
console.log(JSON.stringify({ ok: failing === 0, total: result.total, failing, entries: result.entries, shift: result.shift }, null, 2));
|
||||
} else {
|
||||
console.log(formatMotionReport(result));
|
||||
}
|
||||
process.exit(result.gaps.length === 0 ? 0 : 1);
|
||||
process.exit(failing === 0 ? 0 : 1);
|
||||
}
|
||||
|
||||
/*** 迭代留痕校验:.ua/iterations.log 每行 JSON 字段与格式必须符合 ITERATION.md 规范 ***/
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
/*** @ui-arch 动效兜底审计:带 `infinite` 的动画必须在 prefers-reduced-motion 下被显式关掉
|
||||
/*** @ui-arch 动效兜底审计:跑起来不停的和「一动就位移的」都必须在 prefers-reduced-motion 下被显式关掉
|
||||
*
|
||||
* 为什么单独成门禁:`audit:visual` 只管字面量、`verify` 只管「动效时长走令牌」,没人管「无限循环动画有没有降级」。
|
||||
* 循环动画(转圈、扫光、进度条流纹、时间线脉冲)是前庭敏感用户的硬伤,漏一条就等于把整页交给抖动;
|
||||
* 有限时长的过渡(hover 上浮之类)危害小得多,不在本门禁范围——只盯 infinite 才有明确可执行的判定。
|
||||
* 为什么单独成门禁:`audit:visual` 只管字面量、`verify` 只管「动效时长走令牌」,没人管「动效有没有降级」。
|
||||
* 两类必查:① 无限循环动画(转圈、扫光、进度条流纹、时间线脉冲)——前庭敏感用户的硬伤,漏一条整页都在抖;
|
||||
* ② 悬停 / 按压位移(translate)——位移是最容易诱发不适的一类,纯装饰手感,减动效时必须归零。
|
||||
* 只查这两类而不是所有过渡:颜色 / 阴影 / 透明度变化不产生空间位移,砍掉它们反而丢了状态反馈。
|
||||
*/
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
@@ -57,14 +58,30 @@ export function auditMotion(root) {
|
||||
.flatMap((r) => splitSelectors(r.selector)),
|
||||
);
|
||||
const entries = infinite.flatMap((r) => splitSelectors(r.selector).map((selector) => ({ selector, covered: offline.has(selector) })));
|
||||
const shift = auditShift(rules);
|
||||
return { total: entries.length, entries, gaps: entries.filter((e) => !e.covered), shift };
|
||||
}
|
||||
|
||||
/*** 作用:悬停 / 按压位移的降级审计——逐个位移规则找对应降级,或以 ua- 通配写的全局归零规则兜底 ***/
|
||||
function auditShift(rules) {
|
||||
const isShift = (body) => /transform\s*:[^;]*translate|translate\s*:/.test(body); // 位移两种写法:transform 里的 translate / 独立的 translate 属性
|
||||
const isState = (selector) => /:(hover|active)\b/.test(selector); // 只认交互态位移:布局位移(抽屉开合)不在降级范围
|
||||
const nulled = rules.filter((r) => r.reduced && /transform\s*:\s*none|translate\s*:\s*none/.test(r.body));
|
||||
const exact = new Set(nulled.flatMap((r) => splitSelectors(r.selector)));
|
||||
const global = nulled.some((r) => isState(r.selector) && /\[class\^='?ua-'?\]|\[class\*='? ua-'?\]/.test(r.selector)); // 通配 ua- 类的全局规则视为覆盖全部组件
|
||||
const entries = rules
|
||||
.filter((r) => !r.reduced && isState(r.selector) && isShift(r.body))
|
||||
.flatMap((r) => splitSelectors(r.selector).filter(isState).map((selector) => ({ selector, covered: global || exact.has(selector) })));
|
||||
return { total: entries.length, entries, gaps: entries.filter((e) => !e.covered) };
|
||||
}
|
||||
|
||||
/*** 作用:把审计结果打成文本表(人读模式) ***/
|
||||
export function formatMotionReport({ entries, gaps }) {
|
||||
export function formatMotionReport({ entries, gaps, shift }) {
|
||||
const lines = ['无限动画的减少动效兜底:'];
|
||||
for (const e of entries) lines.push(` ${e.covered ? 'OK ' : 'MISS'} ${e.selector}`);
|
||||
lines.push('');
|
||||
lines.push(gaps.length ? `缺兜底 ${gaps.length} 处:${gaps.map((g) => g.selector).join(' / ')}` : `全部 ${entries.length} 处无限动画都有 prefers-reduced-motion 兜底`);
|
||||
lines.push('', '悬停 / 按压位移的减少动效兜底:');
|
||||
const ok = shift.entries.filter((e) => e.covered).length;
|
||||
lines.push(shift.gaps.length ? ` MISS 未归零 ${shift.gaps.length} 处:${shift.gaps.map((g) => g.selector).join(' / ')}` : ` 全部 ${ok} 处位移在减少动效下归零(悬停上浮 / 按压回缩)`);
|
||||
return lines.join('\n');
|
||||
}
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
"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 audit:motion [--json]:动效兜底门禁,扫描皮肤样式表里带 infinite 的动画,逐个核对是否在 prefers-reduced-motion 块里 animation: none,缺兜底退出码 1(--json 出逐条明细)",
|
||||
"ua audit:motion [--json]:动效兜底门禁,两类必查——① 带 infinite 的动画逐个核对是否在 prefers-reduced-motion 块里 animation: none;② 悬停 / 按压的位移(translate)是否在减少动效下归零(认 ua- 类通配的全局规则)。缺兜底退出码 1(--json 出逐条明细)",
|
||||
"ua mcp:零依赖 stdio MCP server,暴露 list_components / get_component / search_components / get_tokens / list_recipes / get_recipe;get_component 支持 format: md 直接取可粘贴的 Markdown 片段"
|
||||
],
|
||||
"keyTypes": [
|
||||
@@ -48,7 +48,7 @@
|
||||
"改完清单跑 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,人眼看不出但门禁看得出",
|
||||
"改完动效跑 npm run audit:motion:带 infinite 的动画必须在 prefers-reduced-motion 块里被关掉,缺一条即退出码 1——循环动画漏兜底会把「减少动效」用户整页交给抖动",
|
||||
"改完动效跑 npm run audit:motion:带 infinite 的动画必须在 prefers-reduced-motion 块里被关掉,悬停 / 按压的位移(translate)必须归零,缺一条即退出码 1——循环动画漏兜底会把「减少动效」用户整页交给抖动,位移漏兜底则每次悬停都在推搡前庭",
|
||||
"在 ~/.workbuddy/mcp.json 注册 ua mcp 后,Agent 可直接调 list_components / get_component / search_components / get_tokens / list_recipes / get_recipe;get_component 传 format: md 拿到与 ua info --md 同款的 Markdown 片段"
|
||||
],
|
||||
"components": []
|
||||
|
||||
Reference in New Issue
Block a user