iter(g255): 减少动效下归零全站悬停位移
This commit is contained in:
@@ -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');
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user