fix: 窄屏实测修复 —— 删抽屉、44px 命中区、触摸设备可见的次要动作

用 playwright 连本机共享 Chromium,在 390px(iPhone 14 Pro)与 320px
(iPhone SE)量真实盒子。之前的窄屏适配是「照着规则写对」,实测发现
一个功能性 bug 加五处可用性问题。

## 抽屉式侧栏遮挡底部导航(真 bug)

抽屉是 `fixed left-0 top-0 bottom-0 z-50`,铺满整个视口高度;底部导航没有
z-index。抽屉打开时点最左那一项「收件」,elementFromPoint 命中的是抽屉里的
SVG,不是导航按钮 —— 按钮在那里、尺寸也够、CSS 规则也没写错,只有命中测试
才能发现。

**删掉抽屉而不是给导航加 z-index**:抽屉装的六项(收件/发件/联系/用户/新建/
管理)与底部导航完全重复,唯一独有的是退出登录。为一个按钮维护一套 fixed
层级加遮罩不划算,而它还附带「Esc 关不掉」「底层未锁滚」两个毛病。

退出登录移到「我的」页 —— 它与密码、密钥同属「账号自身」,而那页此前根本
没有退出入口。`NavToggle` 与 uiStore 的 navOpen/toggleNav/closeNav 一并删除。

## 触摸命中区:新增 .tap

详情页那排工具按钮视觉高度只有 15-16px(实测「标记已读」48x16、「对话树」
54x16、「转发」42x16、「抄送」20x15),移动端下限是 44x44。

直接加 padding 会把本来就挤的头部撑散、320px 下换行,因此用居中的透明伪元素
扩大命中区:**视觉一像素不动**。只在 max-width:767px 生效 —— 桌面用鼠标精度
足够,而扩大后的命中区在密排工具栏里会互相重叠,点一个可能命中隔壁那个。

覆盖 MailView / ContactPanel / WorkCard / ModelScopePanel / AdminUsersPage /
ComposePage / ThreadView / KeyPanel / QuotaPanel / Attachments / BackButton。

## 看不见却按得动的按钮:新增 .reveal

`opacity-0 group-hover:opacity-100` 在没有 hover 的设备上永远是 opacity:0,
**但仍然接收点击** —— 实测联系人列表里 elementFromPoint 命中的就是那个看不见的
「归档」。一个看不见却按得动的破坏性按钮比没有按钮更糟:人以为点的是卡片,
实际归档了一条会话。

改为默认可见,只在 `(hover: hover) and (pointer: fine)` 时隐藏。
单看 hover 会把带触摸板的平板算进去。

## 对话树

- 缩进随屏宽自适应:固定「每级 20px、上限 8 级」= 最多 160px,320px 屏还要
  去掉 px-4 的 32px 与连接线 18px,卡片只剩 110px,发件人一行直接被 truncate
  吃掉。窄屏改为每级 10px、上限 5 级
- 补返回出口:原先只有「关闭」。两者语义不同 —— 返回退出整个详情栏回到列表,
  关闭只收起树、留在这封邮件上

## 把实测脚本留进仓库

`web/test/manual/`(`npm run test:narrow` / `test:wide`),不进 npm test ——
要一个跑着的浏览器加一个活的 Gateway。

留着而不是用完即删,是因为结构性断言守不住「按钮实际多大、点下去命中谁」,
而这次最严重的 bug 恰好只有 elementFromPoint 能发现。helper 里两个函数专门
为此:tapTargets() 量 .tap 的真实命中区(伪元素尺寸,不是 boundingBox),
hitTest() 验每个元素点下去是否命中自己。

## 验证

- 窄屏 13 项 + 宽屏 5 项全通过。宽屏回归特意验了两件只该在窄屏生效的事:
  .tap 伪元素 content 为 none、没有返回按钮
- narrow-layout.test.mjs 从 20 条扩到 28 条,逐条钉住上面每个修复
- 无横向溢出:390px 与 320px 下 scrollWidth === clientWidth
- 生产已部署
This commit is contained in:
2026-09-02 23:53:59 +08:00
parent 9e5c557cdf
commit 342282b92c
26 changed files with 759 additions and 134 deletions

View File

@ -11,3 +11,59 @@
"Hiragino Sans GB", "Microsoft YaHei", sans-serif;
}
}
@layer utilities {
/*
* .tap —— 保证 44x44 的触摸命中区,但不改变视觉尺寸。
*
* 移动端 44x44 是通行下限Apple HIG 与 Material 都取这个数),而这些图标/
* 小字按钮视觉上只有 15-24px 高 —— 实测「标记已读」48x16、「转发」42x16、
* 「抄送」20x15。直接加 padding 会把本来就挤的头部撑散,在 320px 屏上还会换行。
*
* 改用居中的透明伪元素扩大命中区:视觉一像素不动,手指够得到。
*
* 只在窄屏生效:桌面用鼠标,精度足够;而扩大后的命中区在密排的工具栏里
* 会互相重叠,点一个可能命中隔壁那个。
*/
@media (max-width: 767px) {
.tap {
position: relative;
}
.tap::after {
content: '';
position: absolute;
top: 50%;
left: 50%;
width: 100%;
height: 100%;
min-width: 44px;
min-height: 44px;
transform: translate(-50%, -50%);
}
}
/*
* .reveal —— 悬停才显形的次要动作(写信 / 归档)。
*
* 原先直接写 `opacity-0 group-hover:opacity-100`。**触摸设备没有 hover**
* 于是这些按钮永远是透明的,却仍然接收点击 —— 实测在联系人列表上
* elementFromPoint 命中的就是那个看不见的「归档」。一个看不见却按得动的
* 破坏性按钮比没有按钮更糟:人以为自己点的是卡片,实际归档了一条会话。
*
* 因此默认可见,只在**真的支持悬停**的设备上才隐藏。判据用
* `(hover: hover) and (pointer: fine)`:单看 hover 会把带触摸板的平板算进去。
*/
.reveal {
opacity: 1;
}
@media (hover: hover) and (pointer: fine) {
.reveal {
opacity: 0;
transition: opacity 150ms;
}
.group:hover .reveal,
.reveal:focus-within {
opacity: 1;
}
}
}