fix(web): 手机上时间轴读数不再被裁,「跳到最新」不再压住选项
375px 下读数第四项被 ellipsis 裁成「已…」、第五项整项不可见:compact
段仍加 44px 头像缩进(每侧 60px,可用内容只剩 255px),而 cfb41daf 又把
读数从四项变成五项,按 13px 逐字估宽需要 383px。compact 段改为只取
`space-4`、`column-gap` 收到 `space-2`、第五项 `已对照 N 件` 在 767px 以下
`display: none`(仍在 DOM 与轴 aria-label 里),四项约 292px / 可用 343px。
桌面段、条高 64/56px、滚动锚定逻辑均未动。
「跳到最新」浮层占 composer 上方 56px 带,而 `.message-list` 底部留白恰好
也是 56px,末条选项贴在浮层按钮正下方、正中间点不动。留白改为
`calc(clearance + space-3)`,静止时末条下方 84px = 56px 带 + 28px 空气;
`.conversation` 追加同高 `scroll-padding-block-end`。浮层位置、居中与可见
条件未动。
顺带记一条待产品裁决的口径冲突:DESIGN §10 禁列表写明「已对照 N 件经历」
不上条、仍归交付卡,`cfb41daf` 却把它加上且未改 DESIGN;本轮只在手机上把
它移出可见行,桌面保留原状。
BUG-918、BUG-919
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JUei7K13cYxLHE3Axe4A45
This commit is contained in:
co-authored by
Claude Fable 5.1
parent
5113d457b7
commit
662672c153
@@ -9,6 +9,9 @@
|
||||
如果你 2026-09-16 到 09-17 之间在校正里遇到过风格题,那一次的答案可能计错了分,不要采用当时的区间;补一件带年月的事触发重算,或新开一段校正。
|
||||
|
||||
Skill 版本不变。
|
||||
## 2026-09-17 — 手机上时间轴读数不再被裁,「跳到最新」不再压住选项
|
||||
|
||||
窄屏(375 / 390 宽)打开生时校正时,上方时间轴那一行「范围 · 宽度 · 代表分钟 · 已答 N 题」四项完整显示,不再出现「已…」这种被裁一半的字。为腾出宽度,手机上去掉了这条与头像列对齐的 44px 缩进,并把「已对照 N 件」这项收回交付卡(桌面不变;读屏软件在两种宽度下读到的内容都一样)。「跳到最新」胶囊与对话末条之间留出空隙,不再压在选项卡最后一行上。Skill 版本不变。
|
||||
|
||||
## 2026-09-16 — 正文念出的判别题必有卡;性格描述不再自相矛盾
|
||||
|
||||
|
||||
@@ -11938,3 +11938,35 @@
|
||||
- 相关记录:BUG-915、BUG-675、BUG-674
|
||||
- 复发自:无
|
||||
- 修复版本:待发布
|
||||
|
||||
## BUG-918 | 校正时间轴读数在 375px 手机上被裁成「已…」
|
||||
|
||||
- 状态:resolved
|
||||
- 首次发现:2026-09-17
|
||||
- 最近更新:2026-09-17
|
||||
- 影响面:`frontend/src/app/globals.css` `.rectification-timeline` / `.rectification-timeline__readout` 的 `@media (max-width: 767px)` 段、`frontend/src/components/rectification-timeline.tsx`
|
||||
- 用户现象:iPhone 上常驻时间轴一行读作「04:48–05:07 20 分钟 代表分钟 04:53 已…」,第四项被省略号裁断,第五项完全不显示。
|
||||
- 触发条件:视口宽 375 CSS px(iPhone SE 2/3、13 mini)打开生时校正,Case 已有代表分钟、已答题数与已对照事件数(读数五项齐全)。
|
||||
- 根因:两条叠加。一是读数按「四项短文本」设计,`.rectification-timeline` 在 767px 以下仍取 `calc(var(--space-4) + var(--assistant-content-inset))`,即每侧 16 + 44 = 60px,375px 的可用内容宽只剩 255px;条不在消息列里,这 44px 头像沟槽在手机上买不到任何可见对齐。二是 cfb41daf 又给读数追加了第五项 `已对照 N 件`,把四项预算变成五项。按 13px 逐字保守估宽,五项加四个 12px 间隙需要约 383px,超出 255px 约 128px,于是 `__answered` 命中自身的 `text-overflow: ellipsis` 显示为「已…」,`__dated` 被条的 `overflow: hidden` 整项裁掉。
|
||||
- 修复:compact 段(`@media (max-width: 767px)`,与 `.is-compact` 的 768px 断点同源)内 `.rectification-timeline` 的 `padding-inline` 改为 `var(--space-4)` 单项;读数 `column-gap` 收到 `var(--space-2)`;`.rectification-timeline__dated` 在该段 `display: none`。桌面规则与条高(64 / 56px)一字未动,滚动锚定逻辑未动。组件仍无条件渲染全部五个 span,轴的 `aria-label` 仍由五项文本拼成,被隐藏的一项对辅助技术不丢失。
|
||||
- 验证:`frontend/tests/rectification-mobile-timeline-readout-20260917.test.tsx` 6 条(渲染断言五项文本与 `aria-label` 一致;按保守逐字宽度断言 375 与 390 下四项 + 3×8px 间隙约 292px ≤ 343px / 358px,并断言旧口径 383px > 255px;compact 段声明断言去 inset、收 gap、隐藏第五项、未放开 nowrap 与条高;桌面段断言仍带 inset 与 13px)。`frontend/tests/rectification-timeline-20260909.test.ts` 的移动端 `padding-inline` 断言同步改写并附原值 / 新值 / 原因。全套 `npm test` 3420 tests / 31 fail,失败清单与基线(3414 / 31,无 Docker 集)逐条相同;`tsc --noEmit` 0 错、`npm run lint` 0 error、`next build --webpack` 后 `/` 仍 `○ Static`、首屏 gzip 620106 → 620128 字节(+0.004%)。
|
||||
- 防复发:新增测试把「compact 段不得含 `--assistant-content-inset`」「第五项在 compact 下隐藏」与宽度预算写成断言;`globals.css` 与组件注释都改掉了原来「四项短文本在 375px 下还有余量」的错误说法,并写明再加一项必须重算预算而不是直接追加 span。
|
||||
- 相关记录:BUG-478(跳到最新与滚动锚定归属)、BUG-919(同一次真机走查的另一条)。另记一条口径冲突待产品裁决:`frontend/DESIGN.md` §10 的禁列表明确写「已对照 N 件经历」不上条、仍归交付卡,而 cfb41daf 把它加到了条上且未改 DESIGN;本轮只在手机上把它移出可见行,桌面上是否也撤掉未擅自决定。
|
||||
- 复发自:无
|
||||
- 修复版本:待发布
|
||||
|
||||
## BUG-919 | 「跳到最新」浮层压住选项卡最后一个可点选项
|
||||
|
||||
- 状态:resolved
|
||||
- 首次发现:2026-09-17
|
||||
- 最近更新:2026-09-17
|
||||
- 影响面:`frontend/src/app/globals.css` `.rectification-workspace__chat .conversation` / `.message-list` 的底部留白
|
||||
- 用户现象:iPhone 上「跳到最新」胶囊浮在选项卡最后一行上,选项被遮掉一半,正中间不好点。
|
||||
- 触发条件:校正对话滚到尾部附近、浮层可见(距底 > 96px)时,末条内容是一张多行选项卡。
|
||||
- 根因:浮层 `.jump-to-latest` 是 `position: absolute; bottom: 100%` 挂在 composer 顶边上,占据 composer 上方 44px 触控高 + 自身 `space-3` = 56px 的带。而 `.message-list` 的底部留白恰好等于 `--rectification-jump-clearance`(同样 56px),与浮层带严丝合缝,末条内容顶到浮层正下方;容器 `pointer-events: none` 只放行 44px 按钮本体,按钮正好落在选项行中央,于是「遮一半 + 中间点不动」。
|
||||
- 修复:`.message-list` 底部留白改为 `calc(var(--rectification-jump-clearance) + var(--space-3))`;`.conversation` 追加 `scroll-padding-block-end: var(--rectification-jump-clearance)`,让任何 scroll-into-view 也落在带之上。连同 `.conversation` 自身的 `space-4`,静止时末条下方共 84px(56px 浮层带 + 28px 空气)。浮层的位置、居中对齐与可见条件(BUG-478 的归属)一字未动,`--rectification-jump-clearance` 的取值不变(时间轴 56px 条高仍与它同源),`use-conversation-scroll-anchor` 未动。
|
||||
- 验证:`frontend/tests/rectification-mobile-timeline-readout-20260917.test.tsx` 两条声明断言(留白表达式与 `scroll-padding-block-end`;浮层 `absolute` / `bottom: 100%` / `justify-content: center` / `pointer-events: none` / 44px 与 `conversationAnchorThreshold = 96` 未变)。全套门禁数字同 BUG-918。真机走查(375 / 390 宽)留在 `docs/testing/rectification-mobile-timeline-readout-20260917.md`:本机无 Chrome、无真机。
|
||||
- 防复发:测试锁住「留白 = 浮层带 + space-3」这条表达式,任何把它改回等于浮层带的提交会红。CSS 注释写明了为什么不做到「任何滚动位置都不重叠」——那需要留白 ≥ `conversationAnchorThreshold`(96px) + 56px = 152px,在 667px 高的手机上等于凭空吃掉一整行选项,而浮层只在读者已经向上滚过该阈值后才出现。
|
||||
- 相关记录:BUG-478(两个聊天面统一用 `useConversationScrollAnchor` 与 `JumpToLatestButton`,并把浮层改成居中)、7a4360d8(同一现象 2026-08-25 的旧修法是把胶囊右对齐,已被 BUG-478 的居中统一取代,本轮未推翻它)、BUG-918
|
||||
- 复发自:无
|
||||
- 修复版本:待发布
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
# 进度记录 · 校正页移动端:时间轴读数被裁、「跳到最新」压住选项(2026-09-17)
|
||||
|
||||
任务书:`docs/tasks/TASK-rectification-mobile-timeline-readout-20260917.md`
|
||||
分支:`codex/rectification-mobile-timeline-readout-20260917`,基线 `origin/staging` = `c32f81e7`(任务书写的 `8d7dfbf0` 已被 staging 超车,按指令取 `c32f81e7`)。
|
||||
BUG:918、919。纯前端样式,未跑 Python 预检(AGENTS §9 允许)。
|
||||
|
||||
未触碰(并行会话所有):`frontend/src/components/rectification-agentic-chat.tsx`、`frontend/src/lib/rectification-agentic/**`、Python、Skill。也未动时间轴固定高度(64 / 56px)与 `use-conversation-scroll-anchor`。
|
||||
|
||||
## 1. 做了什么
|
||||
|
||||
| 任务 | 文件 | 改动 |
|
||||
| --- | --- | --- |
|
||||
| T1 BUG-918 | `frontend/src/app/globals.css` | `@media (max-width: 767px)` 段:`.rectification-timeline` 的 `padding-inline` 由 `calc(var(--space-4) + var(--assistant-content-inset))` 改为 `var(--space-4)`;`.rectification-timeline__readout { column-gap: var(--space-2) }`;`.rectification-timeline__dated { display: none }`。桌面段未动。同时改掉基线注释里「四项短文本在 375px 下还有余量」这句被实证推翻的话。 |
|
||||
| T1 BUG-918 | `frontend/src/components/rectification-timeline.tsx` | 只加注释:五个 span 永远无条件渲染,由 CSS 决定窄屏显示几项;轴 `aria-label` 仍由五项拼成;再加第六项必须重算宽度预算。无行为改动。 |
|
||||
| T2 BUG-919 | `frontend/src/app/globals.css` | `.rectification-workspace__chat .message-list` 底部留白由 `var(--rectification-jump-clearance)` 改为 `calc(var(--rectification-jump-clearance) + var(--space-3))`;`.conversation` 追加 `scroll-padding-block-end: var(--rectification-jump-clearance)`。`--rectification-jump-clearance` 取值未变(时间轴 56px 条高仍与它同源,既有断言不受影响);浮层的位置、居中、可见条件未动。 |
|
||||
| T3 | `docs/BUG_HISTORY.md`、`CHANGELOG.md`、`frontend/DESIGN.md`、`docs/testing/…`、本文件 | 记录。 |
|
||||
|
||||
## 2. 宽度预算(T1 的算术)
|
||||
|
||||
jsdom / happy-dom 不在本套件里,`scrollWidth` 与 `clientWidth` 都量不到,所以这是**按逐字保守估宽的算术**,不是测量。每字宽度一律向上取整(数字取 0.6em,而 -apple-system / Helvetica 的 tabular 数字约 0.556em),font-size 13px。测试里 `readoutWidthPx` 就是这套系数。
|
||||
|
||||
单项估宽(px):`04:48–05:07` 77.5、`20 分钟` 45.2、`代表分钟 04:53` 91.1、`已答 6 题` 54.1、`已对照 3 件` 67.1。
|
||||
|
||||
| | 改前 | 改后 |
|
||||
| --- | --- | --- |
|
||||
| compact 每侧内边距 | 16 + 44 = 60px | 16px |
|
||||
| 375px 可用内容宽 | 255px | 343px |
|
||||
| 可见项数 / 列间距 | 5 项 / 12px | 4 项 / 8px |
|
||||
| 读数需要宽度 | 383.0px | 291.9px |
|
||||
| 375px 余量 | **−128.0px(被裁)** | **+51.1px** |
|
||||
| 390px 余量 | −113.0px | +66.1px |
|
||||
| 320px 余量 | −143.0px | −3.9px |
|
||||
|
||||
375 与 390 两个目标宽度都放得下。**320px 差约 4px**:按系统实际字宽(数字 0.556em)重算是 283.3px ≤ 288px,能放下;按上面的保守系数差一点点。真机 320px 若尾部被裁一点属已知边界,已写进 `docs/testing/` 清单第「已知边界」段,范围和代表分钟在任何情况下都不会被裁。
|
||||
|
||||
让步顺序:没有用到。任务书让步 1(只做去 inset + 缩 gap)算下来是 5 项 335.0 + 4×8 = 367.0px > 343px,仍然放不下,所以第五项必须离开可见行;采用了任务书 T1 里的「只保留一项」分支,而不是合并成「N 题 · N 件」——合并需要原始计数,而计数只以 `已答 6 题` / `已对照 3 件` 这种成品字符串到达组件,要么去改 `frontend/src/lib/rectification-timeline-scale.ts`(超出本单文件范围),要么在组件里正则抠数字(脆弱)。隐藏这一项另有 DESIGN 依据,见第 4 节。
|
||||
|
||||
## 3. 「跳到最新」的几何(T2)
|
||||
|
||||
浮层占 composer 上方 56px 的带:按钮 44px + 浮层自身 `padding-bottom: var(--space-3)`。基线 `.message-list` 底部留白恰好也是 56px,与带严丝合缝,末条内容顶到按钮正下方;容器 `pointer-events: none` 只放行 44px 按钮本体,而按钮居中,正落在选项行中央 → 「遮一半 + 中间点不动」。
|
||||
|
||||
改后:`.message-list` 68px + `.conversation` 16px = 静止时末条下方 84px = 56px 带 + 28px 空气。
|
||||
|
||||
**做不到、也没有假装做到的部分**:把「任何滚动位置都不重叠」做成保证,需要留白 ≥ `conversationAnchorThreshold`(96px) + 56px = 152px。推导:设末条内容底为 E、尾部留白 P、距底 d,浮层带覆盖内容坐标 `[E+P-d-56, E+P-d]`,与真实内容重叠 ⟺ `d > P - 56`;而浮层可见 ⟺ `d > 96`。所以要让「浮层刚出现的那一刻」不压内容就得 `P - 56 ≥ 96`。152px 在 667px 高的手机上等于凭空吃掉一整行选项,且浮层只在读者已经主动向上滚过 96px 后才出现,所以按 CSS 注释记录理由后不做。本轮保证的是:**贴底静止时(浮层隐藏)末条选项整行可点且与输入框有 28px 空隙**;浮层可见时它浮在已滚上去的内容上,与两个聊天面一致,点一下即回到底。
|
||||
|
||||
未改浮层可见条件(任务书要求),也未把胶囊改回右对齐——`tests/rectification-agentic-entry.test.ts` 锁着 `justify-content: center`(BUG-478 的统一决策),右对齐是 2026-08-25 `7a4360d8` 的旧修法,已被取代,不擅自推翻。
|
||||
|
||||
## 4. 顺带发现的口径冲突(未擅自裁决,需产品拍板)
|
||||
|
||||
`frontend/DESIGN.md` §10 的「以下一律不得加回来」表里明确写着「已对照 N 件经历」不上条、仍归交付卡,理由是它和「已答 N 题」不是同一个量。但 `cfb41daf`(2026-09-16,BUG-740~743)把 `datedEventLabel` 加到了读数上,且没有改 DESIGN。也就是说这第五项从进条那天起就与 DESIGN 的红线矛盾,而它正是压垮 375px 宽度预算的那一项。
|
||||
|
||||
本轮**只在 767px 以下把它移出可见行**(任务书授权范围内),桌面保留原状,并把这处冲突写进 BUG-918 的「相关记录」、DESIGN §10 和真机清单,等产品决定桌面是否也撤掉。没有单方面改桌面。
|
||||
|
||||
## 5. 测试
|
||||
|
||||
新增 `frontend/tests/rectification-mobile-timeline-readout-20260917.test.tsx`(6 条):
|
||||
|
||||
1. 渲染断言:五项文本按序全在 DOM,且轴 `aria-label` 含全部五项。
|
||||
2. 宽度预算断言:compact 可见四项 + 3×8px ≤ 343px(375)与 ≤ 358px(390);并反向断言旧口径 383px > 255px,把 Bug 的形状钉住。
|
||||
3. compact 段声明断言:`padding-inline: var(--space-4)` 且段内不含 `--assistant-content-inset`;`column-gap: var(--space-2)`;`__dated` `display: none`;段内不得出现 `__range` / `__width` / `__working`(这两个数字任何宽度都不许隐藏或缩);不得放开 `flex-wrap` / `white-space`;条高仍 56px。段的切取是按大括号配平取出那一个 `@media` 块,不是 `slice` 到文件尾。
|
||||
4. 桌面段断言:仍是 `calc(var(--space-8) + var(--assistant-content-inset))`,读数仍 `nowrap` + 13px。
|
||||
5. T2 断言:留白表达式、`scroll-padding-block-end`。
|
||||
6. 浮层未动断言:`absolute` / `bottom: 100%` / `justify-content: center` / `pointer-events: none` / 按钮 44px,以及 `conversationAnchorThreshold = 96` 与 anchor hook 里不出现 scroll-padding。
|
||||
|
||||
文件头写明了「本套件没有 DOM 实现,所以几何断言是算术不是测量,真机走查必做」。
|
||||
|
||||
改写的既有断言 1 条 —— `frontend/tests/rectification-timeline-20260909.test.ts`「the mobile bar stays within the 44px touch rhythm」:
|
||||
|
||||
- 原值:`assert.match(mobile, /padding-inline: calc\(var\(--space-4\) \+ var\(--assistant-content-inset\)\);/)`
|
||||
- 新值:`assert.match(mobile, /padding-inline: var\(--space-4\);/)`
|
||||
- 原因:BUG-918。这条断言锁的正是造成裁切的那 44px 头像沟槽。条不在消息列里,手机上对齐头像列没有可见收益;同一测试里桌面那条 inset 断言未动,`--rectification-jump-clearance` 断言未动。
|
||||
|
||||
没有弱化其它任何断言,没有删测试。
|
||||
|
||||
## 6. 门禁数字
|
||||
|
||||
| 项 | 基线(`c32f81e7`) | 改后 |
|
||||
| --- | --- | --- |
|
||||
| `./node_modules/.bin/tsc --noEmit` | — | **0 错** |
|
||||
| `npm run lint` | — | **0 error**(116 warning,与基线同为既有 `no-unused-vars`,未顺手修) |
|
||||
| `npm test` | 3414 tests / 31 fail | **3420 tests / 31 fail**(+6 新测试全绿) |
|
||||
| 失败清单 | 31 条无 Docker 集 | **逐条相同**(排序后 `diff` 无输出) |
|
||||
| `next build --webpack` | exit 0 | **exit 0** |
|
||||
| `/` 路由 | `○ Static` | **`○ Static`** |
|
||||
| 首屏 gzip | 620,106 B / 25 文件 | **620,128 B / 25 文件(+22 B, +0.004%)** |
|
||||
|
||||
- 失败清单 31 条全部是既有无 Docker / 无凭据集(database-*、staging sync、billing、ephemeris 401 等),与 `BLOCKED.md` 一致。
|
||||
- gzip 口径:把 `.next/server/app/index.html` 里引用到的每个 `/_next/static/*.js|css` 单独 `gzip -9` 后求和。唯一变化是首屏 CSS 分块 36,510 → 36,532 B。
|
||||
- Turbopack 拒绝软链 `node_modules`,按环境说明用 `--webpack`。
|
||||
|
||||
## 7. 环境缺口
|
||||
|
||||
- **无 Chrome、无真机**:375 / 390 宽下「读数四项完整」「胶囊不压选项」只能人工确认,清单在 `docs/testing/rectification-mobile-timeline-readout-20260917.md`(7 条 + 已知边界)。第 2 节的宽度是算术,不是真机测量;第 3 节的 28px 空隙同样没有真机截图。
|
||||
- **未部署**:本分支只推自己的分支,未推 staging,`deployment.gitCommit` 未核对。
|
||||
@@ -104,7 +104,7 @@
|
||||
| `TASK-rectification-year-focus-overlay-20260916.md` | `PROGRESS-rectification-year-focus-overlay-20260916.md` | 真机:定向健康题选 A 后打字给年月,助手追问「是你本人做的吗」,追问句下面挂着录入卡(`set-focus` 同号覆盖年月焦点题干、`event_date_entry` 仍在,BUG-908);事件入账后年月焦点没关(BUG-909,回放定 resolve 还是计划层);答短句撞「本轮达到步骤上限」(BUG-910,host 前置接无年月短句,先 investigating);persisted 问题块无 `--assistant-content-inset`(BUG-911)。**产品追加拍板:「哪一类事 + 发生年月」录入卡整个删掉,年月阶段直接打字(推翻 precision-gate 单 T3 与 SKILL.md:84)**。主体默认本人不追问,Skill 10.0.29 | 已验收 | `6aabbe38`(BUG-908~911,Skill 10.0.29):tsc 0 / lint 0 error / npm test 3414 条 31 红与基线 05cf20fb 逐条相同 / `/` Static / 首屏 gzip 620,502→620,107(−0.06%)/ 快速门 Python 段 798 绿。两条小项(host 前置排在会话校验之前、`answer.host_year_entry` 未登记)并入 dead-d9 修复单 T2 |
|
||||
| `TASK-rectification-dead-d9-choice-20260916.md` | `PROGRESS-rectification-dead-d9-choice-20260916.md` | 真机(6aabbe38):删卡流程已通,但**P0** D9 感情题没有自己的探针,`stampChoiceSchemaWithProbe` 退到最高增益探针,盖上了 **D24 学业六星座对照探针**(BUG-375 (1) 复发):答「没有」按 D24 计分,04:48–04:59 收窄不可信;GET 投影又匹配不上 → 无卡(BUG-912);打字答「没有」后平局直接出交付卡待回放定性(BUG-913);`NAKSHATRA_TRAITS` 整对塞进一个 option,卡头把相反两极列成共同点(BUG-914)。**串行在 year-focus-overlay 验收之后** | **验收未通过(P0)** | `227a7571`+`8d7dfbf0`:不借探针、一列一句、BUG-913 定性均过;但自建探针只在盖戳时注入内存 state,生产答题路径读持久化 state → `stale_probe` 报错、GET 无卡、idle 立刻 superseded(生产路径回放实证)。修复单见下一行 |
|
||||
| `TASK-rectification-dead-d9-choice-fix-20260916.md` | `PROGRESS-rectification-dead-d9-choice-fix-20260916.md` | **验收修复单**:自建分盘探针照 `withNakshatraBoundaryProbe` 模式在每个读 state 的地方(盖戳 / 答题 / `previousInferenceFromReceipt` / GET 投影 / idle 过期判定 / 计划承接)从 receipt 确定性重建(BUG-915);测试改走生产路径、删手工塞探针的 fixture;顺带 host 前置挪到会话校验之后、登记 phase(BUG-916);死卡不得配 `collect_waiting` 占位(BUG-917);**产品决策 1c**:风格题默认不作全额计分(沿用 09-09 ±1 拍板),无事件探针就走下一条线。**staging 现状:风格题答不了,先跳过**。§1d 已记 **产品决策 (b)** | 已实现待验收 | `codex/rectification-dead-d9-choice-fix-20260916`(BUG-915~917,Skill 未 bump):`withOwnedDistinguishProbes` 五处重建(盖戳 / 点选答题 / 打字答题 / GET 投影 / idle 过期;未在 `previousInferenceFromReceipt` 出口统一注入——会把自建探针喂进 contrastPacket 与 eventProbes,按让步 1 逐点包装 + 源码契约测试);按 (b) 删掉风格计分题入口,分层判别题只在有带年份事件探针时出题;死卡改走 `unavailable` 修复出口。tsc 0 / lint 0 error 116 warning / npm test 3421 条 31 红与基线 `c32f81e7` 逐条相同 / `/` `○ Static` / 产物 JS gzip 1,496,339→1,496,408(+0.005%)/ pytest 63 绿、快速门 Python 段 798 绿 |
|
||||
| `TASK-rectification-mobile-timeline-readout-20260917.md` | `PROGRESS-rectification-mobile-timeline-readout-20260917.md` | 手机截图:时间轴读数第四项被裁成「已…」(nowrap + inset 内边距,BUG-918);「跳到最新」浮层压住选项 C(BUG-919)。灰卡与「再说一件」矛盾归修复单 BUG-915/917 | 待领取 | — |
|
||||
| `TASK-rectification-mobile-timeline-readout-20260917.md` | `PROGRESS-rectification-mobile-timeline-readout-20260917.md` | 手机截图:时间轴读数第四项被裁成「已…」(nowrap + inset 内边距,BUG-918);「跳到最新」浮层压住选项 C(BUG-919)。灰卡与「再说一件」矛盾归修复单 BUG-915/917 | 已实现待验收 | `codex/rectification-mobile-timeline-readout-20260917`:纯 CSS + 注释 + 测试。T1 compact 段去掉 44px 头像缩进、`column-gap` 收到 `space-2`、第五项 `已对照 N 件` 在 767px 以下 `display: none`(仍在 DOM 与轴 `aria-label` 里)——**让步 1 算下来仍放不下**(去 inset + 缩 gap 后 5 项需 367px,375px 只有 343px 可用),故走 T1 的「只保留一项」分支;桌面段、条高 64/56px、滚动锚定全未动。T2 `.message-list` 底部留白改 `calc(clearance + space-3)`(静止时 84px = 56px 浮层带 + 28px 空气)+ `.conversation` `scroll-padding-block-end`;浮层位置/居中/可见条件未动。tsc 0 / lint 0 error / `npm test` 3420 条 31 红与基线 `c32f81e7`(3414 / 31)逐条相同 / `/` 仍 `○ Static` / 首屏 gzip 620,106 → 620,128 B(+0.004%)。**顺带发现口径冲突待产品拍板**:DESIGN §10 禁列表写明「已对照 N 件经历」不上条,`cfb41daf` 却把它加上了且没改 DESIGN,本轮只在手机上移出可见行。无 Chrome / 无真机,375 与 390 宽的读数完整性与浮层不遮选项留 `docs/testing/rectification-mobile-timeline-readout-20260917.md`(7 条 + 320px 已知边界) |
|
||||
|
||||
### 聊天主链路与首页
|
||||
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
# 真机清单 · 校正页移动端时间轴读数与「跳到最新」(2026-09-17)
|
||||
|
||||
在 staging 用受控账号、真机(不是桌面浏览器缩窗)走一遍生时校正。本机无 Chrome、无真机,以下全部做不了自动化,只能人工确认。不要把真实出生资料写进任何记录。
|
||||
|
||||
优先 375 CSS px(iPhone SE 2/3、13 mini),再复查 390(iPhone 12–16 基础款)。若手上只有 320 宽的老机(SE 1 代),第 2 条按「可接受被裁一点」看,见末尾说明。
|
||||
|
||||
1. **读数四项完整。** 进入校正、答到出现代表分钟之后,看上方那条时间轴第一行。应完整读出四项:`HH:MM–HH:MM`、`N 分钟`、`代表分钟 HH:MM`(或 `已采用 HH:MM`)、`已答 N 题`。**任何一项都不得出现省略号或半个字**,尤其不得再看到「已…」。
|
||||
2. **读数不换行、条不变高。** 这一行必须是一行。条的高度在整个流程里不得变化(56px),末尾不得出现第二行或把下面的刻度挤出去。横屏、放大字号(iOS 设置 → 显示与亮度 → 文字大小最大档)各看一次:这两种情况下允许尾部被裁,但**范围和代表分钟必须仍然完整可见**。
|
||||
3. **左边距与页面同源。** 读数和刻度的左起点应与页面左边距一致(约 16px),不再比正文多缩进一个头像位。桌面(宽 ≥ 768px)打开同一条校正,读数应仍然与助手正文左边线对齐,并且多出第五项「已对照 N 件」——手机上没有这一项是预期的。
|
||||
4. **读屏软件仍读到五项。** 打开 VoiceOver,聚焦到时间轴的轴本体(`role="img"`),朗读内容应包含范围、宽度、代表分钟、已答题数**和已对照件数**,即手机上看不见的那一项在朗读里仍然存在。
|
||||
5. **「跳到最新」不压选项。** 出现一张多行选项卡(判别题 A/B/C/D 或交付卡三列)之后,向上轻扫一段让「跳到最新」胶囊出现,再滚回底部。滚到底时:胶囊应消失,选项卡最后一行与输入框之间应有明显空隙(约 28px),最后一个选项**整行可点,中间也能点中**。
|
||||
6. **胶囊出现时仍能操作。** 胶囊可见(已向上滚过一段)时点它,应直接回到最新内容,且回到底之后立刻能点最后一个选项。胶囊本身仍是居中的胶囊、44px 高、有阴影——位置和出现条件本轮没有改,不应有变化。
|
||||
7. **回归面:普通对话不受影响。** 打开一段普通咨询对话(不是校正),向上滚出「跳到最新」,确认它的位置、外观和底部留白与改动前一致(本轮只改了校正面的留白)。
|
||||
|
||||
## 已知边界
|
||||
|
||||
- **320px 宽(iPhone SE 1 代 / iPhone 5s 一档)**:按逐字保守估宽,四项 + 间隙约 292px,可用约 288px,差约 4px。按系统实际字宽算是放得下的,但如果真机上「已答 N 题」尾部被裁一点,属已知边界而非回归——范围和代表分钟仍必须完整。看到就记进进度记录,不要当新 Bug 开单。
|
||||
- **「已对照 N 件」是否也该从桌面撤掉**:`frontend/DESIGN.md` §10 的禁列表说这一项归交付卡、不上条,而代码后来把它加到了条上。本轮只在手机上把它移出可见行,桌面保留原状,等产品裁决。见 `docs/BUG_HISTORY.md` BUG-918。
|
||||
+7
-3
@@ -226,7 +226,7 @@ Radii have two visual steps. Controls use 8px (`--radius-md`; `--radius-xs` and
|
||||
### Jump to latest
|
||||
|
||||
- **Structure:** one pill button (`JumpToLatestButton`) with a down arrow and the label “跳到最新”, shared by both chat surfaces.
|
||||
- **Placement:** hangs off the composer wrap's top edge (`.jump-to-latest`, absolute, `bottom: 100%`) so it never reflows the transcript or the composer.
|
||||
- **Placement:** hangs off the composer wrap's top edge (`.jump-to-latest`, absolute, `bottom: 100%`) so it never reflows the transcript or the composer. It occupies a 56px band above the composer — the 44px target plus the `space-3` the overlay holds under itself — and the transcript has to reserve that band **plus** one more `space-3` below its last item, or the last tappable row reads as covered and its centre is where the chip's only `pointer-events: auto` area sits (BUG-919). On the rectification surface that reservation is `.message-list { padding-bottom: calc(var(--rectification-jump-clearance) + var(--space-3)) }`, and `.conversation` carries the matching `scroll-padding-block-end`. Full non-overlap at every scroll position would cost `conversationAnchorThreshold` (96px) on top of the band, i.e. 152px of trailing void; that was rejected — the chip only appears once the reader has scrolled up past that threshold.
|
||||
- **Visibility:** shown when the reader is not anchored — more than 96px from the bottom after scrolling up — and hidden again once they return or press it. Sending a question re-anchors.
|
||||
- **Surface:** canvas fill, hairline border, `--shadow-elevated`; hover uses the warm card surface. No utility-class shadows.
|
||||
- **Accessibility:** a real button in document order with a visible label matching its accessible name, 44px target, and the focus ring; the icon is decorative.
|
||||
@@ -635,14 +635,18 @@ Admin 的 antd `<Spin>` 是独立设计系统,不在此表。
|
||||
|
||||
**二、条高固定,从首次渲染起就占位。** 这是正确性要求,不是视觉偏好:条在滚动容器外,高度一变就改 `clientHeight`,而没有任何观察者会因此重新跟随——贴底的读者会看着内容悄悄滑出视野底部且无法恢复。同理,Case 未就绪时渲染**等高的空骨架**(`data-state="pending"`),不得用 spinner、不得用「正在加载」文案、不得让条晚一步挂载。桌面 64px,767px 以下 56px;56px 等于既有的 `--rectification-jump-clearance`(44px 触控尺寸 + `space-3`),不新造常数。
|
||||
|
||||
读数和轴的水平内边距对齐助手列:桌面 `space-8 + --assistant-content-inset`,767px 以下 `space-4 + --assistant-content-inset`,与 `.message-list` 沟槽和头像让位相同。底边发丝线和背景仍拉满聊天列,不把条做成第二条滚动容器。
|
||||
读数和轴的水平内边距在桌面对齐助手列:`space-8 + --assistant-content-inset`,与 `.message-list` 沟槽和头像让位相同。**767px 以下只取 `space-4`,不加头像缩进**(BUG-918):条不在消息列里,手机上对齐头像列买不到任何可见收益,而那 44px 每侧要吃掉 375px 里的 88px,读数因此被裁。底边发丝线和背景仍拉满聊天列,不把条做成第二条滚动容器。
|
||||
|
||||
**三、条上只有两个元素**:轴本体(区间带 + 候选标记 + 刻度)与一行读数。读数四项、一行排完:
|
||||
**三、条上只有两个元素**:轴本体(区间带 + 候选标记 + 刻度)与一行读数。手机上读数四项、一行排完:
|
||||
|
||||
`05:07–05:09 · 3 分钟 · 代表分钟 05:08 · 已答 6 题`
|
||||
|
||||
**读数只增宽,不增高。** 条高仍是硬规定二的 64 / 56px,`.rectification-timeline__readout` 因此是 `flex-wrap: nowrap` + `white-space: nowrap`;真放不下时由条本身的 `overflow: hidden` 裁掉尾部,四项里「已答 N 题」排最后、最先被裁——它是背景信息,区间和代表分钟不是。
|
||||
|
||||
**一行是有宽度预算的,767px 以下必须按 375px 算,不是「应该放得下」。** BUG-918 的实证:13px 下四项 + 头像缩进已超 375px,第四项被自身 `text-overflow: ellipsis` 显示成「已…」。因此 compact 段(`@media (max-width: 767px)`,与 `.is-compact` 的 768px 断点同源)做三件事:`padding-inline` 去掉头像缩进、读数 `column-gap` 收到 `space-2`、`.rectification-timeline__dated` `display: none`。按逐字保守估宽,手机上四项 + 3×8px 间隙约 292px,375px 可用 343px、390px 可用 358px;320px 只余约 −4px,靠条自身 `overflow: hidden` 兜底。**再往条上加一项,先重算这笔预算,不是直接追加一个 span。** 组件永远无条件渲染全部 span,隐藏只在 CSS 里发生,轴的 `aria-label` 仍由全部标签拼成,读屏软件不因视口宽度丢内容。
|
||||
|
||||
**「已对照 N 件」在手机上不在可见行里,桌面上目前仍在——这是一处未裁决的口径冲突。** 下面的禁列表写明这一项归交付卡、不上条,而它后来(cfb41daf)被加到了条上且没有改本文件。BUG-918 只把它在手机上移出可见行;桌面是否也撤掉需要产品裁决,见 `docs/BUG_HISTORY.md` BUG-918。
|
||||
|
||||
**代表分钟上条是 D10(2026-09-16)的产品决定,推翻了「条上只有区间」的原设计。** 理由是窄屏:`.is-compact` 下右栏盘面整体变成默认关闭的 overlay,代表分钟此前只在 `.rectification-board__clock` 出现,于是用户最该一直看着的那个数字恰恰看不见。条上的取值必须与盘面标题行**同一次调用**——`workingRectificationTime(result)`,由 `rectification-agentic-chat.tsx` 算一次、两处共用,不允许第二条推导路径。已采用时写「已采用 HH:MM」,否则写「代表分钟 HH:MM」;**采用不是确认**,完整边界句仍只在交付 / 采用旁白里出现一次,条上不写、也不得暗示某一分钟已确认。
|
||||
|
||||
**条上不得出现收窄进度(「已从 30 分钟收到 7 分钟」)。** 开场窗口确实在线上,但没有任何投影把这个对比作为字段给出,而 VOICE.md 第 2 条要求进度数字来自服务端。前端自己减还会算错:轴是**当前**窗口且会放宽(BUG-572),相减会把一次放宽报成进度。这句话继续由服务端拼进旁白。缺的字段见 `BLOCKED.md`。
|
||||
|
||||
@@ -2538,9 +2538,17 @@ input:not([type="radio"]):not([type="checkbox"]):not([class^="ant-"]):not([class
|
||||
}
|
||||
/* One line, always. The bar's height is fixed on purpose (see above), so the
|
||||
readout may grow wider but never taller; wrapping would push the axis out of
|
||||
a box nothing is observing. Four short items clear 375px with room to spare,
|
||||
and the bar's own `overflow: hidden` clips rather than reflows if they ever
|
||||
do not. */
|
||||
a box nothing is observing. The bar's own `overflow: hidden` clips rather
|
||||
than reflows if the row ever does not fit.
|
||||
|
||||
This used to claim "four short items clear 375px with room to spare". They
|
||||
did not even at four, and a fifth was added later (BUG-918). At 13px, with
|
||||
the avatar-column inset, a 375px phone left 255px of content box: the four
|
||||
items needed about 304px, the five about 383px. So `已答 N 题` came through
|
||||
as `已…` and `已对照 N 件` never appeared at all. The compact block below is
|
||||
what makes the row fit — it drops the inset and the fifth item there, for
|
||||
about 292px against 343px. Anything added here has to be re-measured against
|
||||
that budget, not assumed to fit. */
|
||||
.rectification-timeline__readout {
|
||||
margin: 0;
|
||||
display: flex;
|
||||
@@ -2656,17 +2664,56 @@ input:not([type="radio"]):not([type="checkbox"]):not([class^="ant-"]):not([class
|
||||
.rectification-workspace__chat {
|
||||
--rectification-timeline-height: 56px;
|
||||
}
|
||||
/* No `--assistant-content-inset` here (BUG-918). The bar is not in the
|
||||
message list, so on a phone the 44px avatar gutter buys no alignment the
|
||||
reader can see; it cost 88px of the 375px the readout has to fit into and
|
||||
the row got clipped instead. The page margin is the honest edge on a
|
||||
narrow viewport, and the axis reads wider for it. */
|
||||
.rectification-timeline {
|
||||
padding-inline: calc(var(--space-4) + var(--assistant-content-inset));
|
||||
padding-inline: var(--space-4);
|
||||
}
|
||||
/* Tighter column rhythm buys another 12px across three gaps; the row is a
|
||||
list of short figures, not prose, so space-2 still separates them. */
|
||||
.rectification-timeline__readout {
|
||||
column-gap: var(--space-2);
|
||||
}
|
||||
/* `已对照 N 件` is the one item DESIGN.md §10 never wanted on the bar — it
|
||||
belongs to the delivery card, and it is a different quantity from
|
||||
`已答 N 题` (events matched, not questions answered). It is the fifth item
|
||||
in a row budgeted for four, so on a phone it leaves the visual row. It
|
||||
stays in the DOM and in the axis `aria-label`, so assistive tech still
|
||||
reads it, and the range plus the working minute — the two figures the
|
||||
product requires to be visible at all times — never shorten. */
|
||||
.rectification-timeline__dated {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
/* `--rectification-jump-clearance` is the band the floating 跳到最新 chip
|
||||
occupies above the composer: its 44px touch target plus the `space-3` the
|
||||
overlay holds under itself. The transcript reserves that band **plus another
|
||||
`space-3`** (BUG-919). Reserving exactly the band left the last tappable
|
||||
option flush against the chip, so a choice card's final row read as covered
|
||||
and its centre — the only part of the row the chip's `pointer-events: auto`
|
||||
button actually intercepts — was hard to hit.
|
||||
|
||||
With `.conversation`'s own `space-4` the tail now keeps 84px of clear space:
|
||||
56px of chip band and 28px of air. Eliminating overlap in *every* scroll
|
||||
position would need `conversationAnchorThreshold` (96px) on top of the band,
|
||||
i.e. 152px of trailing void under the last message at rest — a whole option
|
||||
row of dead space on a 667px phone, and the chip only shows once the reader
|
||||
has scrolled up past that threshold anyway. The chip's own placement and
|
||||
visibility rule are unchanged (BUG-478 owns them). */
|
||||
.rectification-workspace__chat .conversation {
|
||||
--rectification-jump-clearance: calc(44px + var(--space-3));
|
||||
padding-bottom: var(--space-4);
|
||||
/* Anything scrolled into view lands above the chip band too. This is inert
|
||||
for `use-conversation-scroll-anchor`, which assigns `scrollTop` directly
|
||||
rather than calling a scroll-into-view API. */
|
||||
scroll-padding-block-end: var(--rectification-jump-clearance);
|
||||
}
|
||||
.rectification-workspace__chat .message-list {
|
||||
width: 100%;
|
||||
padding-bottom: var(--rectification-jump-clearance);
|
||||
padding-bottom: calc(var(--rectification-jump-clearance) + var(--space-3));
|
||||
}
|
||||
.conversation.is-rectification .message-actions {
|
||||
margin-inline-start: var(--assistant-content-inset);
|
||||
|
||||
@@ -24,6 +24,13 @@
|
||||
* must never change — see `.rectification-timeline` in globals.css. The readout
|
||||
* stays one line for that reason: it grew wider, never taller. An unready case
|
||||
* renders the same-height skeleton rather than nothing.
|
||||
*
|
||||
* One line has a width budget, and on a 375px phone it is spent by the fourth
|
||||
* item (BUG-918). Every span below is always rendered; globals.css decides how
|
||||
* many of them a narrow viewport shows, and it hides `__dated` there. The axis
|
||||
* `aria-label` below is assembled from all five labels regardless, so nothing a
|
||||
* screen reader gets depends on the viewport. Adding a sixth item means
|
||||
* re-measuring that budget, not appending a span.
|
||||
*/
|
||||
|
||||
"use client";
|
||||
@@ -46,6 +53,8 @@ export function RectificationTimeline({ view }: { view: RectificationTimelineVie
|
||||
{view.answeredLabel ? (
|
||||
<span className="rectification-timeline__answered">{view.answeredLabel}</span>
|
||||
) : null}
|
||||
{/* Hidden below 768px by globals.css, never omitted here: the label is
|
||||
part of the accessible axis description either way. */}
|
||||
{view.datedEventLabel ? (
|
||||
<span className="rectification-timeline__dated">{view.datedEventLabel}</span>
|
||||
) : null}
|
||||
|
||||
@@ -0,0 +1,200 @@
|
||||
/**
|
||||
* 2026-09-17, iPhone on staging: the rectification timeline readout came through
|
||||
* as `04:48–05:07 20 分钟 代表分钟 04:53 已…` (BUG-918), and the floating
|
||||
* 跳到最新 chip sat on top of the last tappable option of a choice card
|
||||
* (BUG-919).
|
||||
*
|
||||
* What this file can and cannot check, stated plainly: there is no jsdom or
|
||||
* happy-dom in this suite, so `renderToStaticMarkup` is the only render path and
|
||||
* `scrollWidth` / `clientWidth` do not exist to compare. Every geometric claim
|
||||
* below is therefore **arithmetic over the rendered text**, using deliberately
|
||||
* generous per-glyph widths (see `readoutWidthPx`), plus CSS declaration
|
||||
* assertions of the kind `chart-page-view.test.tsx` uses. A real-device pass at
|
||||
* 375 and 390 CSS px is still required and lives in
|
||||
* `docs/testing/rectification-mobile-timeline-readout-20260917.md`.
|
||||
*/
|
||||
|
||||
import assert from "node:assert/strict";
|
||||
import { readFileSync } from "node:fs";
|
||||
import React from "react";
|
||||
import { renderToStaticMarkup } from "react-dom/server";
|
||||
import test from "node:test";
|
||||
|
||||
import { RectificationTimeline } from "../src/components/rectification-timeline.tsx";
|
||||
import { buildRectificationTimeline } from "../src/lib/rectification-timeline-scale.ts";
|
||||
|
||||
Object.assign(globalThis, { React });
|
||||
|
||||
const styles = readFileSync(new URL("../src/app/globals.css", import.meta.url), "utf8");
|
||||
/** Comments carry prose that would otherwise satisfy declaration assertions. */
|
||||
const declarations = styles.replace(/\/\*[\s\S]*?\*\//g, " ");
|
||||
|
||||
/** The `@media (max-width: 767px)` block that owns the timeline, and only it. */
|
||||
function compactTimelineBlock(source: string): string {
|
||||
const start = source.indexOf("@media (max-width: 767px) {\n .rectification-workspace__chat {\n --rectification-timeline-height: 56px;");
|
||||
assert.notEqual(start, -1, "compact timeline media block not found");
|
||||
let depth = 0;
|
||||
for (let at = start; at < source.length; at += 1) {
|
||||
if (source[at] === "{") depth += 1;
|
||||
else if (source[at] === "}") {
|
||||
depth -= 1;
|
||||
if (depth === 0) return source.slice(start, at + 1);
|
||||
}
|
||||
}
|
||||
assert.fail("compact timeline media block is unbalanced");
|
||||
}
|
||||
|
||||
function ruleBody(source: string, selector: string): string {
|
||||
const at = source.indexOf(`${selector} {`);
|
||||
assert.notEqual(at, -1, `missing rule: ${selector}`);
|
||||
const end = source.indexOf("}", at);
|
||||
assert.notEqual(end, -1, `unterminated rule: ${selector}`);
|
||||
return source.slice(at + selector.length + 2, end);
|
||||
}
|
||||
|
||||
/** The readout's five spans in document order, text only. */
|
||||
function readoutItems(markup: string): string[] {
|
||||
const readout = markup.match(/<p class="rectification-timeline__readout">([\s\S]*?)<\/p>/);
|
||||
assert.ok(readout, "readout paragraph not rendered");
|
||||
return [...readout[1].matchAll(/<span class="rectification-timeline__[a-z-]+">([^<]*)<\/span>/g)]
|
||||
.map((match) => match[1]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Upper bound on a readout item's rendered width at 13px, the size the bar is
|
||||
* pinned to. Every figure here is rounded **up** from the system sans stacks the
|
||||
* app ships (digits measure about 0.556em in -apple-system / Helvetica and the
|
||||
* readout asks for tabular-nums, so 0.6em is slack, not a guess). An assertion
|
||||
* that passes against these numbers passes against the real font.
|
||||
*/
|
||||
const GLYPH_EM: Readonly<Record<string, number>> = {
|
||||
digit: 0.6,
|
||||
colon: 0.33,
|
||||
space: 0.28,
|
||||
dash: 0.5,
|
||||
cjk: 1,
|
||||
other: 0.6,
|
||||
};
|
||||
|
||||
function readoutWidthPx(text: string, fontSizePx = 13): number {
|
||||
let em = 0;
|
||||
for (const character of text) {
|
||||
if (/\d/.test(character)) em += GLYPH_EM.digit;
|
||||
else if (character === ":") em += GLYPH_EM.colon;
|
||||
else if (character === " ") em += GLYPH_EM.space;
|
||||
else if (character === "–" || character === "-") em += GLYPH_EM.dash;
|
||||
else if (/[ -鿿豈-]/.test(character)) em += GLYPH_EM.cjk;
|
||||
else em += GLYPH_EM.other;
|
||||
}
|
||||
return em * fontSizePx;
|
||||
}
|
||||
|
||||
/** The worst realistic readout: every optional item present, widest labels. */
|
||||
const fullView = buildRectificationTimeline({
|
||||
searchWindow: ["04:30", "05:30"],
|
||||
credibleRange: ["04:48", "05:07"],
|
||||
candidateTimes: ["04:48", "04:53", "05:07"],
|
||||
stage: "minute",
|
||||
workingTime: "04:53",
|
||||
answeredProbeCount: 6,
|
||||
datedEventCount: 3,
|
||||
});
|
||||
|
||||
test("the timeline renders every readout figure the projection carries", () => {
|
||||
assert.ok(fullView, "the fixture window has to produce a view");
|
||||
const items = readoutItems(renderToStaticMarkup(<RectificationTimeline view={fullView} />));
|
||||
assert.deepEqual(items, ["04:48–05:07", "20 分钟", "代表分钟 04:53", "已答 6 题", "已对照 3 件"]);
|
||||
// BUG-918's fix is CSS, not omission: the compact viewport hides one item, it
|
||||
// never drops it from the markup, so the accessible axis description below
|
||||
// reads the same on every viewport.
|
||||
const markup = renderToStaticMarkup(<RectificationTimeline view={fullView} />);
|
||||
const axisLabel = markup.match(/aria-label="([^"]*)"/)?.[1] ?? "";
|
||||
for (const item of items) {
|
||||
assert.ok(axisLabel.includes(item), `axis label is missing ${item}`);
|
||||
}
|
||||
});
|
||||
|
||||
test("the four items a 375px phone shows fit the compact content box", () => {
|
||||
assert.ok(fullView);
|
||||
const items = readoutItems(renderToStaticMarkup(<RectificationTimeline view={fullView} />));
|
||||
// `.rectification-timeline__dated` is display:none below 768px, so the visible
|
||||
// row is the first four.
|
||||
const visible = items.slice(0, 4);
|
||||
const columnGapPx = 8; // --space-2, the compact column-gap
|
||||
const required = visible.reduce((total, item) => total + readoutWidthPx(item), 0)
|
||||
+ columnGapPx * (visible.length - 1);
|
||||
|
||||
// 375 CSS px (iPhone SE 2/3, 13 mini) minus the compact padding-inline,
|
||||
// var(--space-4) on each side. The 44px avatar inset that used to be added
|
||||
// here is what pushed the row over (BUG-918).
|
||||
const available375 = 375 - 2 * 16;
|
||||
assert.ok(
|
||||
required <= available375,
|
||||
`readout needs ${required.toFixed(0)}px, ${available375}px available at 375px`,
|
||||
);
|
||||
// 390 CSS px (iPhone 12–16 base) has to hold too.
|
||||
assert.ok(required <= 390 - 2 * 16);
|
||||
|
||||
// And the shape of the old bug: with the inset and all five items it did not
|
||||
// fit, which is why the fourth item arrived as 「已…」.
|
||||
const oldRequired = items.reduce((total, item) => total + readoutWidthPx(item), 0) + 12 * (items.length - 1);
|
||||
assert.ok(oldRequired > 375 - 2 * (16 + 44));
|
||||
});
|
||||
|
||||
test("the compact timeline drops the avatar inset and the fifth figure", () => {
|
||||
const compact = compactTimelineBlock(declarations);
|
||||
// Page margin only — no --assistant-content-inset on a phone.
|
||||
assert.match(ruleBody(compact, ".rectification-timeline"), /padding-inline: var\(--space-4\);/);
|
||||
assert.doesNotMatch(ruleBody(compact, ".rectification-timeline"), /--assistant-content-inset/);
|
||||
assert.match(ruleBody(compact, ".rectification-timeline__readout"), /column-gap: var\(--space-2\);/);
|
||||
assert.match(ruleBody(compact, ".rectification-timeline__dated"), /display: none;/);
|
||||
// The range and the working minute are the two figures the product requires
|
||||
// to stay visible at every width, so neither may be hidden or shrunk here.
|
||||
assert.doesNotMatch(compact, /\.rectification-timeline__(range|width|working)\b/);
|
||||
// Still one line, still a fixed-height bar: the compact block must not undo
|
||||
// either, or the bar would resize outside anything observing it.
|
||||
assert.doesNotMatch(compact, /flex-wrap: wrap|white-space: normal/);
|
||||
assert.match(compact, /--rectification-timeline-height: 56px;/);
|
||||
});
|
||||
|
||||
test("the desktop readout keeps all five figures and the assistant-column inset", () => {
|
||||
const desktop = ruleBody(declarations, ".rectification-timeline");
|
||||
assert.match(desktop, /padding: 0 calc\(var\(--space-8\) \+ var\(--assistant-content-inset\)\);/);
|
||||
assert.match(declarations, /\.rectification-timeline__readout \{[^}]*white-space: nowrap;/);
|
||||
assert.match(declarations, /\.rectification-timeline__readout \{[^}]*flex-wrap: nowrap;/);
|
||||
assert.match(declarations, /\.rectification-timeline__readout \{[^}]*font-size: 13px;/);
|
||||
});
|
||||
|
||||
test("the transcript reserves the jump-to-latest band plus air below it", () => {
|
||||
// BUG-919: reserving exactly the chip band left the last option flush under
|
||||
// the chip. The band is the chip's 44px target plus the space-3 it holds
|
||||
// under itself; the list now reserves that plus one more space-3.
|
||||
assert.match(
|
||||
declarations,
|
||||
/\.rectification-workspace__chat \.conversation \{[^}]*--rectification-jump-clearance: calc\(44px \+ var\(--space-3\)\);/,
|
||||
);
|
||||
assert.match(
|
||||
declarations,
|
||||
/\.rectification-workspace__chat \.message-list \{[^}]*padding-bottom: calc\(var\(--rectification-jump-clearance\) \+ var\(--space-3\)\);/,
|
||||
);
|
||||
// Anything scrolled into view lands above the band too.
|
||||
assert.match(
|
||||
declarations,
|
||||
/\.rectification-workspace__chat \.conversation \{[^}]*scroll-padding-block-end: var\(--rectification-jump-clearance\);/,
|
||||
);
|
||||
});
|
||||
|
||||
test("the jump-to-latest overlay itself is untouched by the clearance change", () => {
|
||||
// Its placement and visibility rule belong to BUG-478; this round only widened
|
||||
// the transcript's reservation.
|
||||
assert.match(declarations, /\.jump-to-latest \{[^}]*position: absolute;/);
|
||||
assert.match(declarations, /\.jump-to-latest \{[^}]*bottom: 100%;/);
|
||||
assert.match(declarations, /\.jump-to-latest \{[^}]*justify-content: center;/);
|
||||
assert.match(declarations, /\.jump-to-latest \{[^}]*padding-bottom: var\(--space-3\);/);
|
||||
assert.match(declarations, /\.jump-to-latest \{[^}]*pointer-events: none;/);
|
||||
assert.match(declarations, /\.jump-to-latest__button \{[^}]*min-height: 44px;/);
|
||||
// No second scroll-follow and no reflowing layout for the chip.
|
||||
const anchor = readFileSync(new URL("../src/hooks/use-conversation-scroll-anchor.ts", import.meta.url), "utf8");
|
||||
assert.doesNotMatch(anchor, /scroll-padding|rectification-jump-clearance/);
|
||||
assert.match(anchor, /export const conversationAnchorThreshold = 96;/);
|
||||
});
|
||||
@@ -418,7 +418,12 @@ test("the chat grid has three rows and the timeline row is a fixed height", () =
|
||||
test("the mobile bar stays within the 44px touch rhythm", () => {
|
||||
const mobile = styles.slice(styles.indexOf("@media (max-width: 767px) {\n .rectification-workspace__chat {"));
|
||||
assert.match(mobile, /--rectification-timeline-height: 56px;/);
|
||||
assert.match(mobile, /padding-inline: calc\(var\(--space-4\) \+ var\(--assistant-content-inset\)\);/);
|
||||
// 原值: padding-inline: calc(var(--space-4) + var(--assistant-content-inset));
|
||||
// 新值: padding-inline: var(--space-4);
|
||||
// 原因: BUG-918。真机 375px 下这 44px 头像沟槽让读数放不下,第四项被
|
||||
// text-overflow 裁成「已…」。条不在消息列里,手机上对齐头像列没有
|
||||
// 可见收益,页面边距才是诚实的边界。桌面那条规则不变(上一条测试)。
|
||||
assert.match(mobile, /padding-inline: var\(--space-4\);/);
|
||||
// 56px is the existing jump-clearance constant, not a new number.
|
||||
assert.match(styles, /--rectification-jump-clearance: calc\(44px \+ var\(--space-3\)\);/);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user