-
Notifications
You must be signed in to change notification settings - Fork 0
Timeline blocks land whole, with a crisp entrance — no typing reveal #251
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -458,6 +458,29 @@ export function MessageTimeline(props: { | |
| const timelineRowByKey = projection.rowByKey | ||
| const timelineRows = projection.rows | ||
|
|
||
| // Entrance bookkeeping (Kate 2026-08-24: blocks animate in whole, crisply). | ||
| // A row animates only when it JOINS the projection after the session's first | ||
| // paint, and only once — virtual rows unmount and remount on every | ||
| // scroll-back, so mount alone must never trigger the entrance. The first | ||
| // mounted row after a session switch seeds the set with the whole history | ||
| // (synchronously, so no render of stale rows can slip in between); a key | ||
| // never seen before animates and is recorded. Rows that join off-window | ||
| // animate on their first scroll into view — still exactly once. | ||
| let enteredFor: string | undefined | ||
| const enteredKeys = new Set<string>() | ||
| const shouldAnimateEnter = (rowKey: string) => { | ||
| const sid = sessionID() | ||
| if (enteredFor !== sid) { | ||
| enteredFor = sid | ||
| enteredKeys.clear() | ||
| for (const row of timelineRows()) enteredKeys.add(TimelineRow.key(row)) | ||
| return false | ||
| } | ||
| if (enteredKeys.has(rowKey)) return false | ||
| enteredKeys.add(rowKey) | ||
| return true | ||
|
Comment on lines
+469
to
+481
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win Initialize entrance state for empty sessions. If a session first renders with no timeline rows, the later first row enters the initialization branch and skips its entrance animation. Initialize or reset the per-session state from the session identity, including when the initial row set is empty, and add coverage for an initially empty session followed by its first row. 📍 Affects 1 file
🤖 Prompt for AI Agents |
||
| } | ||
|
|
||
| let prependAnchor: { key: string; offset: number } | undefined | ||
| let prependAnchorFrame: number | undefined | ||
| let prependLoading = false | ||
|
|
@@ -1583,6 +1606,8 @@ export function MessageTimeline(props: { | |
| let element: HTMLDivElement | ||
| const initialItem = virtualItemByKey().get(props.rowKey)! | ||
| const initialRow = timelineRowByKey().get(props.rowKey)! | ||
| // Decided once at creation — remounts of an already-entered row get false. | ||
| const animateEnter = shouldAnimateEnter(props.rowKey) | ||
| const item = createMemo(() => virtualItemByKey().get(props.rowKey) ?? initialItem) | ||
| const row = createMemo(() => timelineRowByKey().get(props.rowKey) ?? initialRow) | ||
| const tool = () => { | ||
|
|
@@ -1622,18 +1647,19 @@ export function MessageTimeline(props: { | |
| height: `${item().size}px`, | ||
| overflow: "clip", | ||
| // Rounded virtual measurements can otherwise clip a framed row's outer paint. | ||
| // 4px, not 0.5px: the live rail dot breathes by a 0→4px ring | ||
| // (index.css thought-rail-breathe) and the old margin clipped the | ||
| // whole animation away — found in PR #246's testing. Keep in step | ||
| // with the ring size there. | ||
| "overflow-clip-margin": row()._tag === "TurnGap" ? undefined : "4px", | ||
| // 8px, not 0.5px: the live rail dot breathes by a 0→4px ring | ||
| // (index.css thought-rail-breathe; found clipped in PR #246's | ||
| // testing), and an entering row rides a --motion-enter-rise | ||
| // translate on top of it — the margin must cover ring + rise. | ||
| "overflow-clip-margin": row()._tag === "TurnGap" ? undefined : "8px", | ||
| }} | ||
| > | ||
| <div | ||
| ref={(value) => { | ||
| element = value | ||
| }} | ||
| data-index={item().index} | ||
| data-timeline-enter={animateEnter ? "" : undefined} | ||
| style={{ "min-height": ready() ? undefined : `${initialItem.size}px` }} | ||
| > | ||
| <TimelineRowView | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Keep the fade at the configured duration in reduced-motion mode.
The later reduced-motion rule on Lines 204-208 forces every animation duration to
0.01ms !important. Therefore,[data-timeline-enter]does not retain its0.18sfade.Add a more-specific
!importantduration rule for[data-timeline-enter]inside this media query.Proposed fix
`@media` (prefers-reduced-motion: reduce) { :root { --motion-enter-rise: 0px; } + [data-timeline-enter] { + animation-duration: var(--motion-enter-duration) !important; + } }📝 Committable suggestion
🤖 Prompt for AI Agents