diff --git a/client/electron/src/components/AddressInput.tsx b/client/electron/src/components/AddressInput.tsx index acc535e..b8875e8 100644 --- a/client/electron/src/components/AddressInput.tsx +++ b/client/electron/src/components/AddressInput.tsx @@ -181,7 +181,7 @@ export default function AddressInput({ {open && items.length > 0 && (
{ + for (const e of readdirSync(dir, { withFileTypes: true })) { + const p = join(dir, e.name); + if (e.isDirectory()) walk(p); + else if (/\.tsx?$/.test(e.name) && !/\.test\./.test(e.name)) out.push(readFileSync(p, 'utf8')); + } + }; + walk(join(HERE, '..', 'src')); + return out.join('\n'); +} +const src = componentsSrc(); + +/** 取出所有 @keyframes 名 + 每条规则的 selector 文本 */ +const keyframes = [...css.matchAll(/@keyframes\s+([a-zA-Z0-9_-]+)/g)].map(m => m[1]); +const rules = [...css.matchAll(/(^|\n)([^{}\n][^{}]*)\{([^{}]*)\}/g)].map(m => ({ + sel: m[2].trim().replace(/\s+/g, ' '), + body: m[3] +})); + +/** + * 一条 selector 在**最终**要不要靠某个类名才能命中? + * 只取"最后一个复合选择器"上的类(`html.view-switch .pane-enter` → `pane-enter`), + * 祖先里的类(html.view-switch、.app-shell)是状态开关,不算穿的人。 + */ +function wearerClasses(sel) { + return sel + .split(',') + .map(part => part.trim().split(/\s+/).pop() || '') + .filter(last => last.startsWith('.')) + .map(last => last.split(/[\s.:[>]/)[0].replace(/^\./, '')) + .filter(Boolean); +} + +const dead = []; +const unwrapped = []; +for (const k of keyframes) { + const users = rules.filter(r => new RegExp(`animation:\\s*${k}\\b`).test(r.body)); + if (users.length === 0) { + dead.push(`${k}(没有任何规则用它)`); + continue; + } + // 这条 keyframes 的每一个使用者都必须"有人穿" —— 否则它还是不会播 + const worn = users.some(r => { + const cls = wearerClasses(r.sel); + if (cls.length === 0) return true; // 元素选择器/通配:不算死 + return cls.some(c => new RegExp(`["'\`\\s]${c}(?=["'\`\\s]|$)`).test(src) || src.includes(c)); + }); + if (!worn) unwrapped.push(`${k} ← ${users.map(u => u.sel).join(' | ')}`); +} + +check( + '每个 @keyframes 都有人穿(没有死动画)', + dead.length === 0 && unwrapped.length === 0, + `只定义了没人穿的动画(看着有、永远不播):${[...dead, ...unwrapped].join(';')}` +); + +check( + '弹层菜单带着菜单入场类', + /animate-menu-in/.test(rules.map(r => r.sel).join(',')) && + /popup-surface[^`]*animate-menu-in|animate-menu-in[^`]*popup-surface/.test(src), + '菜单入场类存在但没人穿 —— 下拉/候选菜单是"啪"地出现(2026-09-15 盘点抓到的第二处死动画)' +); + +// 只在**顶层规则**里查:reduced-motion 块里那句 `html.view-switch .glass-control` 是 +// "把它关掉"的名单,不是"让它动"的规则 —— 第一版没区分,判据自己假红。 +const topLevel = css.replace(/@media[^{]*\{(?:[^{}]|\{[^{}]*\})*\}/g, ''); +check( + '没有"整档控件一起动"的切视图规则', + !/html\.view-switch\s+\.glass-control/.test(topLevel), + 'view-switch 又把 .glass-control 整档带上了 —— 每次切视图几十个按钮/输入框一起动(闪与卡顿的现成来源)' +); + +const reducedBlocks = css.match(/@media \(prefers-reduced-motion: reduce\)\s*\{[\s\S]*?\n\}/g) || []; +check( + '挂载即播那档被 reduced-motion 显式关掉', + reducedBlocks.some(b => /(^|\n)\s*\.rise-in\s*,/.test(b)), + 'prefers-reduced-motion 覆盖不到 .rise-in —— 关掉动画的人照样会看到它' +); + +finish('动画盘点');