fix(webui): 回复球改成容器变形 + 摘掉邮件列表的入场动画 + 全局控件过渡不再补间阴影

用户连着报了三件(2026-09-15):

① 「邮件页面那个蓝色的圆形是聊天图标点击没有任何动画」
   上一版我只给了回复框一个 150ms 的 opacity 淡入 —— 观感上等于没有。用户要的形态与
   写信页一致:**球自己长成回复框**。现在蓝球带 data-morph-trigger、onClick 里就地记下
   自己的矩形(currentTarget 最准),回复框挂载时用 useLayoutEffect 在**首帧之前**把
   CSS 淡入关掉并接上 morph(220ms,只动 transform/opacity/border-radius)。

② 「邮件列表会闪」——**我上一版引入的回归**。
   我把 pane-rise 挂到了邮件列表上,而 view-switch 窗口在**选中一封邮件**时也会触发
   (narrowPane 是依赖之一)⇒ 每点一封信整个列表重放一次入场。已摘掉:列表这类
   "选中即变"的面不许挂入场动画,只有日历(真的换视图)保留 pane-rise。
   判据改成反向的"列表必须不动",把这个回归钉死。

③ 「主页点击按钮有明显的卡顿」。
   全局 `button,a,input,textarea,select,[role=button]` 的 transition 里带了一条
   `box-shadow var(--dur-base)` —— 阴影是**绘制**属性,每次 hover/active 都要把元素
   连同阴影覆盖区重绘,且它挂在所有交互元素上。已去掉该补间(阴影瞬切),
   判据钉"全局控件过渡不得含 box-shadow",个别表面仍可自己写。

另补一个真缺口:prefers-reduced-motion 原先只覆盖"窗口触发"那档,**没覆盖"挂载即播"
那档(裸 .rise-in)**,两处 morph 也没走这个开关。现已全部纳入;判据加严到"必须行首
就是 .rise-in"(第一版被 `html.view-switch .rise-in,` 满足掉,变异测试才发现假绿)。

判据合计 84 通过 0 失败(变异都验过:删裸 .rise-in → 红;把 box-shadow 加回 → 红;
列表挂回 pane-rise → 红;useLayoutEffect 换回 useEffect → 红)。
This commit is contained in:
2026-09-15 08:15:22 +08:00
parent 436de6ee4d
commit 110d457535
6 changed files with 119 additions and 13 deletions

View File

@ -281,6 +281,26 @@ check(
*/
const originSrc = code('src/lib/composeOrigin.ts');
const composeSrc2 = code('src/components/ComposePage.tsx');
/*
* 回复球也要 morph(2026-09-15 用户:「邮件页面那个蓝色的圆形是聊天图标点击没有任何动画」)。
* 上一版只给了它 150ms 淡入 —— 观感上等于没有;要的形态与写信页一致:球自己长成回复框。
* 判据钉:① 球带 morph 起点标记;② 起点就地取(onClick 里 currentTarget 最准);
* ③ 用 useLayoutEffect(首帧前换掉,否则先闪淡入再跳);④ 让位给 morph 时把 CSS 动画关掉
* (两条动画都动 transform/opacity,同挂会打架);⑤ 只动合成器属性。
*/
const mvSrc = code('src/components/MailView.tsx');
check(
'回复球带 morph 起点标记且就地取矩形',
/data-morph-trigger/.test(mvSrc) && /rememberOrigin\(e\.currentTarget\)/.test(mvSrc),
'蓝球没接 morph —— 点了只有一层几乎看不见的淡入'
);
check(
'回复框 morph 在首帧前接管(useLayoutEffect + 关掉 CSS 动画)',
/useLayoutEffect\(\(\) => \{[\s\S]{0,900}?el\.style\.animation = 'none'/.test(mvSrc),
'用 useEffect 或没关 CSS 动画 —— 会先闪一下淡入再跳一次'
);
check(
'写信页起点矩形:捕获阶段取 + 有时效',
/addEventListener\(\s*'click'/.test(originSrc) && /,\s*true\s*\)/.test(originSrc) && /TTL_MS/.test(originSrc),
@ -321,17 +341,36 @@ check(
/animation:\s*rise-in/.test(bareRise),
'回复框/转发面板的动画仍被绑在 view-switch 窗口上 —— 那两个动作根本不会触发窗口,等于没动画'
);
/*
* 点击卡顿(2026-09-15 用户:「主页点击按钮有明显的卡顿」)。
* 阴影是**绘制**属性:把它挂在"所有交互元素"的 transition 上,每次 hover/active 都要
* 重绘元素连同阴影区。判据钉"全局控件过渡不得含 box-shadow",
* 并留出旁路(个别表面自己写 transition 仍可以动画阴影)。
*/
const ctrlRule = (css.match(/button,\s*a,\s*input,\s*textarea,\s*select,\s*\[role='button'\]\s*\{[\s\S]*?\n\}/) || [''])[0];
check(
'常驻面板用窗口触发的 pane-rise(列表/日历)',
/pane-rise/.test(code('src/components/MailList.tsx')) &&
/pane-rise/.test(code('src/components/CalendarView.tsx')) &&
'全局控件过渡不含 box-shadow(绘制属性)',
ctrlRule.length > 0 && !/box-shadow/.test(ctrlRule),
'全局 button/a/input 的 transition 里又出现了 box-shadow —— 每次 hover/active 都重绘阴影区,点击会卡'
);
check(
'日历用窗口触发的 pane-rise,**邮件列表不许挂**',
/pane-rise/.test(code('src/components/CalendarView.tsx')) &&
!/pane-rise/.test(code('src/components/MailList.tsx')) &&
/html\.view-switch \.pane-rise\s*\{/.test(css),
'列表/日历没有入场动画,或者把"常驻面板"误挂到了挂载即播的档上(首屏会跟着闪)'
'邮件列表挂上了入场动画 —— view-switch 窗口在**选中邮件**时也会触发,' +
'于是每点一封信整个列表重播一次,观感就是"邮件列表会闪"(2026-09-15 用户报的就是这个)'
);
check(
'rise-in 也受 prefers-reduced-motion 管',
reducedBlocks.some(b => /view-switch \.rise-in/.test(b)) && reducedBlocks.some(b => /view-switch \.pane-rise/.test(b)),
'reduced-motion 块里漏了 rise-in —— 关掉动画的人仍会被它位移'
// 必须行首就是 .rise-in(否则 `html.view-switch .rise-in,` 会把这个判断满足掉 ——
// 第一版就是这么假绿的,变异测试抓到了)
reducedBlocks.some(b => /(^|\n)\s*\.rise-in\s*,/.test(b)) &&
reducedBlocks.some(b => /view-switch \.rise-in/.test(b)) &&
reducedBlocks.some(b => /view-switch \.pane-rise/.test(b)),
'reduced-motion 覆盖不全 —— 漏掉「挂载即播」那档(裸 .rise-in)时,关掉动画的人照样会看到它'
);
check(
'淡出规则同时带 -webkit- 前缀(Electron 里以哪个为准不由我们定)',