# 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-696|CSS 的平板上限 900px 与 JS 的 1023 不一致,901–1023 是混合态 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)` 的首页栅格规则。 **结果:901–1023px 之间,`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. 不得动 admin(antd + 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-696:CSS 平板上限对齐 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` 的当前最大号,可能已被别的轮次占用。