feat(rectification): show the credible range as a persistent timeline

The range is the only quantity that expresses convergence, and it existed
only as a sentence in the transcript. The bar makes it permanent.

It is the chat grid's first row, outside the scroll container, so
scrollHeight is untouched and use-conversation-scroll-anchor needs no
change. Its height is fixed because nothing observes it: a growing bar
would alter clientHeight and let an anchored reader lose the tail.

The axis is the current search window, not the opening one, since
widening overruns the opening window. Marks are binary — in range or
excluded — because relative support between candidate minutes carries no
demonstrated minute-level discrimination (BUG-560, blocked).

Band and marks move by transform alone; globals.css bans width
transitions, and transform keeps the motion off the layout path.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016P5RoqzmUQEbeC2qjAkeGr
This commit is contained in:
Jesse_Chen
2026-09-09 04:23:05 +00:00
co-authored by Claude Fable 5
parent 109b1d7f0c
commit ce91a0d23c
7 changed files with 760 additions and 1 deletions
+121 -1
View File
@@ -2673,7 +2673,127 @@ input:not([type="radio"]):not([type="checkbox"]):not([class^="ant-"]):not([class
min-width: 0;
min-height: 0;
display: grid;
grid-template-rows: minmax(0, 1fr) auto;
/* Timeline row, scroll row, composer row. The timeline is deliberately its
own row rather than a sticky child of .conversation: outside the scroll
container it leaves scrollHeight untouched, so the stick-to-bottom distance
in use-conversation-scroll-anchor stays exact and its ResizeObserver (which
watches only the container's children) never sees it. */
grid-template-rows: var(--rectification-timeline-height) minmax(0, 1fr) auto;
}
/* Fixed height is correctness, not taste: the bar sits outside the scroll
container, so a height change alters clientHeight with nothing observing it,
and an anchored reader would have content slide out of view unrecovered.
The pending state keeps the same box for the same reason. */
.rectification-workspace__chat {
--rectification-timeline-height: 64px;
}
.rectification-timeline {
height: var(--rectification-timeline-height);
box-sizing: border-box;
display: flex;
flex-direction: column;
justify-content: center;
gap: var(--space-1);
padding: 0 var(--space-5);
background: var(--color-canvas);
border-bottom: 1px solid var(--color-border);
overflow: hidden;
}
.rectification-timeline__readout {
margin: 0;
display: flex;
align-items: baseline;
gap: var(--space-2);
font-size: 13px;
line-height: 1.2;
}
.rectification-timeline__range {
font-variant-numeric: tabular-nums;
font-weight: 500;
color: var(--color-ink);
}
.rectification-timeline__width {
color: var(--color-ink-secondary);
}
.rectification-timeline__axis {
position: relative;
height: 26px;
flex: none;
}
.rectification-timeline__rule {
position: absolute;
inset-inline: 0;
top: 7px;
height: 1px;
background: var(--color-border-strong);
}
/* Positioned and resized by transform alone: transitioning left/width would run
layout on every frame, and this stylesheet bans width transitions outright
(the sidebar shell animates none of its geometry either). The band is full
width and scaled down from its left edge, so one transform carries both the
start and the span. No side borders — scaleX would distort them, and the
fill edge already reads as the boundary. */
.rectification-timeline__band {
position: absolute;
top: 2px;
left: 0;
width: 100%;
height: 11px;
border-radius: 2px;
background: var(--color-action-soft);
transform-origin: left center;
will-change: transform;
transition: transform 420ms cubic-bezier(.32, .72, .24, 1);
}
.rectification-timeline__mark-at {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
pointer-events: none;
transition: transform 420ms cubic-bezier(.32, .72, .24, 1);
}
/* Every mark is the same size. Relative support between candidate minutes is
79 out of 100 with no demonstrated minute-level discrimination (BUG-560,
blocked), so grading them by size or weight would show a difference the
engine cannot back. In-range versus excluded is the only honest encoding. */
.rectification-timeline__mark {
position: absolute;
top: 3px;
left: -4.5px;
width: 9px;
height: 9px;
border-radius: 50%;
border: 1px solid var(--color-action);
background: var(--color-action);
transition: background 260ms ease, border-color 260ms ease, opacity 260ms ease;
}
.rectification-timeline__mark[data-state="out"] {
background: var(--color-canvas);
border-color: var(--color-border-strong);
opacity: .55;
}
.rectification-timeline__tick {
position: absolute;
top: 13px;
transform: translateX(-50%);
font-size: 10px;
line-height: 1.3;
color: var(--color-ink-tertiary);
font-variant-numeric: tabular-nums;
white-space: nowrap;
}
/* 56px is the existing --rectification-jump-clearance (44px touch target plus
space-3), reused rather than inventing a second constant. Past ~64px the bar
costs a whole message bubble on a 667px viewport. */
@media (max-width: 767px) {
.rectification-workspace__chat {
--rectification-timeline-height: 56px;
}
.rectification-timeline {
padding-inline: var(--space-4);
}
}
.rectification-workspace__chat .conversation {
--rectification-jump-clearance: calc(44px + var(--space-3));