Files
Jyotisha/docs/tasks/TASK-mobile-touch-and-breakpoints-20260915.md
T
Jesse_ChenandClaude Fable 5 899f955d4b docs(tasks): fold two mobile blockers into the reading-load round, brief touch targets and breakpoints
2026-09-15 的 UI 走查(web + 移动端)结果落盘。

并入 TASK-chat-reading-load:折叠层里的四列表在窄屏没有重排规则
(767 的卡片化只认三列,width:100% 又让 overflow-x 永不触发),它是
折叠能不能落地的前置;全局 input/select 是 14px,iOS 聚焦会把整页放大,
命中整条注册与资料录入漏斗。

新单 TASK-mobile-touch-and-breakpoints(BUG-695~697):消息操作按钮
命中区 27×34 且相邻只隔 1px;CSS 平板上限 900px 与 sidebar-state.ts 的
1024 不一致,901–1023 是混合态;报告域 720/760/860 三刀互不对齐,
761–860 目录已塌、正文还是桌面。含断点白名单契约测试作为防复发。

另加 docs/testing/ios-keyboard-composer-20260915.md:html/body overflow
hidden + 100dvh 外壳 + sticky 输入框 + interactiveWidget 只有 Android 认,
是键盘遮挡的风险形状,但代码断不了真假,交真机确认后再决定是否立单。

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0155nFCgCHtoA7jhSDGmZmMu
2026-09-15 04:02:50 +00:00

260 lines
15 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# TASK · 消息操作触控目标 + 断点错位(BUG-695~697
- 日期:2026-09-15
- 基线 commit`origin/staging` @ `41473261`
- 执行分支:`codex/mobile-touch-and-breakpoints-20260915`
- 工作树:`.worktrees/mobile-touch-and-breakpoints-20260915`
- **串行依赖:必须排在 `TASK-chat-reading-load-20260915.md` 之后。** 两轮都改 `frontend/src/app/globals.css`,不得并行。开工前确认前一轮已合入 `staging`,并以合入后的 staging 为基线,而不是 `41473261`
---
## 1. 来源
2026-09-15 的 UI 走查(web + 移动端)在首页与聊天两件事之外,另外查实三处。其中两处已并入 `TASK-chat-reading-load-20260915.md`(折叠层宽表、输入框 14px),本单处理剩下的两类。
走查同时确认了一批**做得好、本轮不要动**的东西,列在 §6,避免执行方顺手"优化"掉。
---
## 2. 事故实证
行号会漂移,定位以符号名为准。核对于 `origin/staging@41473261`
### 2.1 BUG-695|消息操作按钮触控目标 27×34,相邻只隔 1px
`frontend/src/app/globals.css`
```css
.message-actions { gap: 1px; margin: -4px 0 8px 42px; }
.message-actions button { width: 26px; height: 26px; flex: 0 0 26px; }
/* Visual icon stays 26px. Neighbours sit 27px centre-to-centre (26px + 1px gap)
and follow-ups start after an 8px bottom margin, so a 44×44 overlay would
steal clicks. This is the largest non-overlapping hit: 27×34. */
.message-actions button::after { width: 27px; height: 34px; }
```
复制 / 点赞 / 点踩 / 重新生成四个按钮并排,中心间距 27px。WCAG 2.5.8 的 24×24 下限过了,但离触屏推荐的 44×44 差很远;在手机上点赞很容易点成点踩。
**注释本身写清楚了这是被布局逼出来的**:往下 8px 就是 follow-up 药丸,44×44 的覆盖层会抢走它们的点击。所以这不是「把 27 改成 44」一行能解决的,得先给这一行让出空间。
同类但优先级低得多的还有一处:`.markdown-code-copy { min-height: 28px }`。本产品的回答里代码块极少,顺手一起处理,做不了不阻塞。
### 2.2 BUG-696CSS 的平板上限 900px 与 JS 的 1023 不一致,9011023 是混合态
JS`frontend/src/lib/sidebar-state.ts``sidebarViewportForWidth`):
```
width < 768 → mobile
width < 1024 → tablet
否则 → desktop
```
侧栏宽度跟着它走(`globals.css``[data-viewport="tablet"]…``--sidebar-width-tablet: 240px`desktop 用 288px)。
但 CSS 里另有两处用 `max-width: 900px` 当平板上限:
- `.code-form { grid-template-columns: 1fr 1fr; }`
- `.model-selector-trigger { max-width: 100%; grid-column: 1; grid-row: 1; }`
以及两处 `(min-width: 768px) and (max-width: 900px)` 的首页栅格规则。
**结果:9011023px 之间,`data-viewport` 还是 `tablet`(侧栏 240px 窄版),但所有 `max-width: 900px` 的规则已经失效**,模型选择器和首页栅格回到桌面排法。这个区间正好是 iPad 横屏(1024 刚好落在 desktop 侧)、桌面窗口拖到一半、以及不少 Android 平板。
### 2.3 BUG-697|报告域三个断点互不对齐,761–860 是混合态
| 断点 | 规则 | 文件位置 |
| --- | --- | --- |
| `max-width: 720px` | `.report-center-shell` 内边距、`.report-center-hero` 单栏、按钮满宽 | 报告中心 |
| `max-width: 760px` | `.personal-report-reader-body` 内边距、`.personal-report-document` 内边距、封面单栏 | 报告阅读区 |
| `max-width: 860px` | `.personal-report-md-layout` 单栏、`.personal-report-toc` 静态化、目录切抽屉 | 报告目录 |
在 761–860px:**目录已经塌成抽屉了,正文还是桌面内边距**。在 721–760:阅读区已窄、报告中心还是桌面。
另外整份样式表的宽度切点共有:`430 / 480 / 620 / 639·640 / 640·641 / 720 / 760 / 767·768 / 860 / 900 / 1024`。其中 `639·640`(会员)与 `640·641`(校正)是两组各自自洽的邻接对,相差 1px,不造成错位但属于噪音;`430``480` 是两个小屏断点;`620` 是出生资料录入卡独有的。
---
## 3. 决策记录(产品已授权)
产品负责人 2026-09-15 确认按下述范围执行:
1. **BUG-695 修**:把消息操作的触控目标做到触屏下 44×44,代价是让出垂直空间。
2. **BUG-696 / 697 修的是「错位」,不是「统一」。** 本轮**只**消除 §2.2 和 §2.3 两个混合区间,以及把噪音断点收敛。**不做全表断点重构**——本会话没有浏览器,全表重排的视觉回归验证不了,收益也远小于风险。
3. **收敛的方式是约定 + 契约测试,不引 PostCSS 插件。** CSS 自定义属性不能用在媒体查询条件里,而本仓 Next.js 配置没有 `postcss-custom-media`。本轮不为此加构建依赖(`frontend/AGENTS.md` §7.7:不得顺手升级依赖)。
4. **视觉验收是环境缺口。** 本轮没有浏览器与真机,断点改动的目视确认写成 `docs/testing/` 清单交给产品,**不得在 PROGRESS 里写成「通过」**。
---
## 4. 硬红线
1. `./node_modules/.bin/tsc --noEmit` 0 错;`npm run lint` **0 error**
2. 测试总数不得低于开工时 `origin/staging` 的实测;改既有断言写「原值 / 新值 / 原因」三栏。
3. `next build``/` 仍是 `○ Static`;首屏 gzip ±2% 内。
4. **不得改任何组件的 JSX 结构来迁就断点**——本轮是样式与命中区的调整。唯一允许的结构改动是 BUG-695 需要的那点垂直空间,且必须在 `frontend/DESIGN.md` 里写明。
5. **不得动 `sidebarViewportForWidth` 的 768 / 1024**。它是 JS 侧真相,CSS 向它对齐,不是反过来。
6. 不得动 adminantd + Refine`[class^="ant-"]` 排除项照旧)。
7. 不得顺手升级依赖、不得引入 PostCSS 插件、不得顺手修不在本单里的 warning。
8. 改 UI 的同一提交内更新 `frontend/DESIGN.md`
---
## 5. 任务分解
### 任务 1 · BUG-695:消息操作触控目标
**1.1** 在触屏下(`@media (pointer: coarse)`)把 `.message-actions button` 的命中区做到 **44×44**,并让出它需要的空间:
- 加大 `.message-actions` 的下外边距(现为 `8px`),或加大 `.conversation-follow-ups` 的上外边距,使 44px 的覆盖层不与 follow-up 药丸重叠。
- 按钮之间的 `gap``1px` 放大到至少 `var(--space-1)`,使相邻命中区不互相吞。
- **视觉图标保持 26×26**,只放大命中区与间距。桌面端(`pointer: fine`)保持现状不变。
**1.2** 保留 `::after` 覆盖层那条注释的意图,但**更新注释内容**——它现在描述的是旧结论(「44×44 会抢走点击」),改完之后必须说明新的几何关系为什么不会抢。留着过期注释比没有注释更坏。
**1.3** `.markdown-code-copy``min-height``(pointer: coarse)` 下提到 44px。做不了不阻塞,写进 PROGRESS。
**验收标准**
- `(pointer: coarse)``.message-actions button` 的有效命中区 ≥ 44×44,且相邻按钮命中区不重叠、不与 follow-up 药丸重叠。
- `(pointer: fine)` 下的几何与改动前逐值一致。
- 新增 CSS 契约测试断言上述两条(本仓已有大量 `globals.css` 文本断言的先例,照同样方式写)。
- 桌面端四个按钮的视觉排布肉眼无变化——这一条是**真机/浏览器项**,写进 `docs/testing/`
---
### 任务 2 · BUG-696CSS 平板上限对齐 JS 的 1024
**2.1** 把下列 `max-width: 900px` 改为 `max-width: 1023px`,使 CSS 的平板区间与 `sidebarViewportForWidth``[768, 1024)` 完全重合:
- `.code-form { grid-template-columns: 1fr 1fr; }`
- `.model-selector-trigger { max-width: 100%; grid-column: 1; grid-row: 1; }`
**2.2** 两处 `(min-width: 768px) and (max-width: 900px)` 的首页栅格规则同样改为 `and (max-width: 1023px)`
⚠️ 其中一处是 `.starter-list``.starter-hero` 相关。**如果前一轮(`TASK-chat-reading-load-20260915`)已经删掉了主题卡**,先确认这些选择器是否还有作用域;已成孤儿的规则直接删除,不要改了断点再留着。
**2.3** 改完检查 901–1023 区间内这几处的排版是否合理:模型选择器(是否还挤)、首页两张产品卡(单栏还是双栏)、兑换码表单。**这是浏览器项**,写进清单。
**验收标准**
- `grep -n "max-width: 900px" frontend/src/app/globals.css` 无结果。
- 新增契约测试:断言 `globals.css` 中不出现 `900px` 作为宽度断点,且 `sidebar-state.ts` 的阈值仍是 `768` / `1024`
- 901 / 1023 / 1024 三个宽度下的目视确认写进 `docs/testing/`
---
### 任务 3 · BUG-697:报告域三断点收敛为一个
**3.1**`max-width: 720px`(报告中心)与 `max-width: 760px`(报告阅读区)**合并到 `max-width: 767px`**,与全局移动断点对齐。两组规则内容不变,只改条件并合并到同一个块里(或保持两块但条件一致,择一,写进 PROGRESS)。
**3.2** `max-width: 860px`(报告目录塌成抽屉)**保留为有理由的例外**,但必须在 `globals.css` 里就地加注释说明理由——目录是三栏布局的第三栏,它需要比正文更早塌陷,860 是内容宽度决定的,不是设备宽度。没有理由就一并并入 1023。执行方按实际布局判断并在 PROGRESS 里给结论。
**3.3** 噪音收敛(纯整理,行为不得变):
- `max-width: 639px` + `min-width: 640px`(会员)与 `max-width: 640px` + `min-width: 641px`(校正)统一为 **640/641 这一组**,即会员那对改成 `max-width: 640px` / `min-width: 641px`。差 1px,没有真实用户影响,只为让白名单更短。
- `max-width: 430px`toaster + 校正面)并入 `max-width: 480px`,**除非**并入后校正面在 431–480 之间明显变坏;若是,保留 430 并就地注释理由。
- `max-width: 620px`(出生资料录入卡)并入 `640px`
**3.4**`globals.css` 顶部新增一段注释,声明**允许出现的宽度断点白名单**及各自含义:
```
480 小屏手机
640/641 内容级栅格塌陷(会员方案卡、校正候选卡、录入卡)
767/768 移动 / 平板,与 sidebar-state.ts 对齐
860 报告目录(第三栏,内容宽度决定)——若 3.2 判定无理由则删除本行
1023/1024 平板 / 桌面,与 sidebar-state.ts 对齐
```
**验收标准**
- `globals.css` 里出现的宽度断点值,全部在 3.4 的白名单内。
- 新增契约测试:从 `globals.css` 抽出所有 `(max-width: Npx)` / `(min-width: Npx)` 的 N,断言集合等于白名单。**这条测试是本单最重要的防复发措施**——它让下一个人加断点时必须先改白名单。
- 761 / 800 / 860 三个宽度下报告页的目视确认写进 `docs/testing/`
---
### 任务 4 · 测试与文档
**4.1** 开工先量基线,写进 PROGRESS:
```
cd .worktrees/mobile-touch-and-breakpoints-20260915/frontend
npm ci
npm test 2>&1 | tail -20 # pass / fail / tests 三个数字
```
**4.2** 新增测试至少覆盖:任务 1 的命中区几何、任务 2 的 900px 消失 + JS 阈值不变、任务 3 的断点白名单。
**4.3** 文档:
- `frontend/DESIGN.md`:新增「断点」一节,写白名单与各自含义,并写明 CSS 必须向 `sidebar-state.ts` 对齐而不是反过来;触控目标一节写明「触屏 44×44、桌面视觉 26×26」的双轨规则。
- `docs/BUG_HISTORY.md`:新增 BUG-695 / 696 / 697 三条,字段齐全(状态、现象、触发条件、根因、修复、验证、防复发、关联记录、修复版本)。视觉部分只有清单没有真机证据时,状态写 `resolved` 但在「验证」里如实写明「代码级契约测试通过,浏览器级验收见 `docs/testing/`」。
- `docs/testing/mobile-touch-and-breakpoints-20260915.md`:把任务 1.3 / 2.3 / 3.x 里标了「浏览器项 / 真机项」的条目写成可照做的清单(给出具体宽度、具体页面、看什么)。
- `CHANGELOG.md`:一条日期 + 一句话标题 + 要点。
- `docs/tasks/PROGRESS-mobile-touch-and-breakpoints-20260915.md`
---
## 6. 走查确认「已经做得好」的部分——本轮不得改动
执行方容易顺手"优化"掉这些,逐条列明:
| 项 | 现状 | 不要动的理由 |
| --- | --- | --- |
| `env(safe-area-inset-*)` | 侧栏、chat-header、composer、auth 面板全覆盖 | 刘海与 home bar 已适配正确 |
| `min-width: 0` 纪律 | 全表贯彻,无固定大宽度 | 这是页面不横向溢出的原因 |
| `dvh` | 15 处,`vh` 仅 3 处做降级兜底 | 已是正确形态 |
| `@media (hover: hover)` 6 块 | 全部只承担颜色 / 位移装饰 | 没有 hover-only 的功能,触屏无损 |
| 44px 触控纪律 | 侧栏汉堡、会话菜单、关闭、额度、模型选择器、follow-up 药丸、composer 发送全是 44 | 只有 `.message-actions``.markdown-code-copy` 破例,即本单任务 1 |
| 深色主题 | 32 对对比度矩阵 + 契约测试 | 加任何颜色都要过那张矩阵 |
| `prefers-reduced-motion` / `prefers-contrast` / `forced-colors` / `print` | 均有 | 完整 |
| `.rectification-timeline``padding-inline` | 移动端已有覆盖(`--space-4 + --assistant-content-inset`),高度 64→56 | 内边距是为对齐头像栏,不是失误 |
---
## 7. 让步顺序
1. 任务 3.2(860 例外)判不清就**保留 860 并注释**,不要强行并入——错并会让报告目录在 768–860 之间挤在正文旁。
2. 任务 3.3 的三条噪音收敛,任何一条并入后观感变坏就**保留原值并就地注释理由**,白名单相应加一行。收敛是为了可维护,不值得拿观感换。
3. 任务 1 若让出垂直空间后消息之间的节奏明显变松,**先只放大 `gap` 与相邻间距、命中区做到 44 宽 × 现有高**,把高度留到下一轮,写进 PROGRESS。
4. 任务 2 的 901–1023 目视若发现模型选择器在新区间反而更差,**回退该条并在 PROGRESS 说明**,但 `.code-form` 的对齐照做。
5. **绝不让步**:不得改 `sidebarViewportForWidth`;不得引 PostCSS 插件;不得为断点重构去改组件 JSX;断点白名单契约测试必须落地。
---
## 8. 开工前置命令
```bash
cd /workspace/Jyotisha
git status -sb | head -1
git fetch origin --prune
# 确认前置轮次已合入
git log --oneline origin/staging -5 # 应能看到 chat-reading-load 的实现提交
git worktree add -b codex/mobile-touch-and-breakpoints-20260915 \
.worktrees/mobile-touch-and-breakpoints-20260915 origin/staging
cd .worktrees/mobile-touch-and-breakpoints-20260915/frontend
npm ci
npm test 2>&1 | tail -20
./node_modules/.bin/tsc --noEmit
npm run lint
```
纯前端改动,不要求 `scripts/pre_work_check.py`
交付:
```bash
git push origin HEAD:staging
```
推完核对远端 SHA。
---
## 9. BUG 编号
- 本单占用 **BUG-695、BUG-696、BUG-697**
- `TASK-chat-reading-load-20260915.md` 若在执行中需要新编号,从 **BUG-698** 起。
- 开工时仍需核对 `docs/BUG_HISTORY.md` 的当前最大号,可能已被别的轮次占用。