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
104 lines
4.5 KiB
TypeScript
104 lines
4.5 KiB
TypeScript
/**
|
||
* The always-present range bar above the rectification conversation.
|
||
*
|
||
* Read-only by design: it answers "how wide is the range now", never "which
|
||
* minute do I take". Adoption stays on the delivery card, so this component
|
||
* takes no callbacks and renders no controls.
|
||
*
|
||
* It carries the axis and a one-line readout: range, width, working minute and
|
||
* the answered-question count. The working minute is here because on a narrow
|
||
* viewport the board is an overlay and closed by default, which used to leave
|
||
* the one number the reader tracks off screen entirely (D10, 2026-09-16).
|
||
*
|
||
* Still cut, and staying cut: a legend, the stage name, and the range-change
|
||
* sentence — that sentence is already written into the persisted assistant
|
||
* message (BUG-588 requires it there, so repeating it would put one sentence on
|
||
* screen twice).
|
||
*
|
||
* Every figure is echoed from the server projection. The working minute comes
|
||
* from the same `workingRectificationTime(result)` call the board's title-row
|
||
* clock makes, so the two surfaces cannot disagree; the count is the server's
|
||
* `answered_probe_count`. Nothing is computed here.
|
||
*
|
||
* The bar lives outside the scroll container as its own grid row, so its height
|
||
* 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";
|
||
|
||
import type { RectificationTimelineView } from "@/lib/rectification-timeline-scale";
|
||
|
||
export function RectificationTimeline({ view }: { view: RectificationTimelineView | null }) {
|
||
if (!view) {
|
||
return <div className="rectification-timeline" data-state="pending" aria-hidden="true" />;
|
||
}
|
||
|
||
return (
|
||
<div className="rectification-timeline" data-state="ready">
|
||
<p className="rectification-timeline__readout">
|
||
<span className="rectification-timeline__range">{view.rangeLabel}</span>
|
||
<span className="rectification-timeline__width">{view.widthLabel}</span>
|
||
{view.workingLabel ? (
|
||
<span className="rectification-timeline__working">{view.workingLabel}</span>
|
||
) : null}
|
||
{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}
|
||
</p>
|
||
<div
|
||
className="rectification-timeline__axis"
|
||
role="img"
|
||
aria-label={[
|
||
`出生时间搜索范围 ${view.axisStartLabel} 到 ${view.axisEndLabel}`,
|
||
`当前区间 ${view.rangeLabel}`,
|
||
view.widthLabel,
|
||
view.workingLabel,
|
||
view.answeredLabel,
|
||
view.datedEventLabel,
|
||
].filter(Boolean).join(",")}
|
||
>
|
||
<span className="rectification-timeline__rule" />
|
||
{/* Placed and resized with transform only. Animating left/width would
|
||
run layout every frame, and globals.css bans transitions on width
|
||
outright (the sidebar shell animates none of its geometry). */}
|
||
<span
|
||
className="rectification-timeline__band"
|
||
style={{ transform: `translateX(${view.bandStartPercent}%) scaleX(${view.bandWidthPercent / 100})` }}
|
||
/>
|
||
{view.marks.map((mark) => (
|
||
<span
|
||
key={mark.key}
|
||
className="rectification-timeline__mark-at"
|
||
style={{ transform: `translateX(${mark.percent}%)` }}
|
||
>
|
||
<span className="rectification-timeline__mark" data-state={mark.state} />
|
||
</span>
|
||
))}
|
||
{view.ticks.map((tick) => (
|
||
<span
|
||
key={tick.key}
|
||
className="rectification-timeline__tick"
|
||
style={{ left: `${tick.percent}%` }}
|
||
>
|
||
{tick.label}
|
||
</span>
|
||
))}
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|