Keep the timeline's ruler and playhead dot in view while its rows scroll

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Olive Vaughn 2026-09-30 01:22:01 -04:00
parent c139e4d747
commit 277c0c3b63
2 changed files with 28 additions and 9 deletions

View file

@ -355,9 +355,7 @@
(reset! over nil) (reset! over nil)
(when (< 1 (count from)) (when (< 1 (count from))
(rf/dispatch [::ui/move-node from []]))))} (rf/dispatch [::ui/move-node from []]))))}
[:div {:style {:height "var(--ruler)" [:div.tl-corner]
:border-bottom "1px solid var(--line)"
:background "var(--chrome)"}}]
(doall (for [row visible] (doall (for [row visible]
^{:key (str (:path row))} [label-cell row selection over]))] ^{:key (str (:path row))} [label-cell row selection over]))]
[:div.tl-tracks [:div.tl-tracks
@ -401,7 +399,8 @@
:on-pointer-cancel (fn [_] (reset! scrubbing false))} :on-pointer-cancel (fn [_] (reset! scrubbing false))}
(doall (doall
(for [f (range 0 frames step)] (for [f (range 0 frames step)]
^{:key f} [:div.tick {:style {:left (edge% f frames)}} f]))] ^{:key f} [:div.tick {:style {:left (edge% f frames)}} f]))
[:div.tl-knob {:style {:left (at% frame frames)}}]]
(if (seq visible) (if (seq visible)
(doall (for [row visible] (doall (for [row visible]
^{:key (str (:path row))} [track-cell row frames sliding])) ^{:key (str (:path row))} [track-cell row frames sliding]))

View file

@ -562,9 +562,16 @@ button.share-button:hover, button.share-button.on { filter: brightness(1.1); }
flex: 1; flex: 1;
min-height: 0; min-height: 0;
display: flex; display: flex;
/* Not `stretch`: that sizes both columns to the body's height, the rows then
overflow them, and the sticky ruler goes out with its column. Each column is
as tall as its rows instead, and at least as tall as the body. */
align-items: flex-start;
overflow: auto; overflow: auto;
} }
.tl-labels,
.tl-tracks { min-height: 100%; }
.tl-labels { .tl-labels {
flex: 0 0 var(--label); flex: 0 0 var(--label);
/* A flex item's `min-width` defaults to `auto`, which floors its used size at /* A flex item's `min-width` defaults to `auto`, which floors its used size at
@ -574,7 +581,9 @@ button.share-button:hover, button.share-button.on { filter: brightness(1.1); }
min-width: 0; min-width: 0;
position: sticky; position: sticky;
left: 0; left: 0;
z-index: 1; /* Over the ruler and the playhead, which both pass under it when the tracks
scroll sideways. */
z-index: 4;
background: var(--pane); background: var(--pane);
border-right: 1px solid var(--line); border-right: 1px solid var(--line);
} }
@ -638,11 +647,20 @@ button.share-button:hover, button.share-button.on { filter: brightness(1.1); }
.tl-delete { margin-left: auto; padding: 0 4px; border: 0; background: none; color: var(--dim); } .tl-delete { margin-left: auto; padding: 0 4px; border: 0; background: none; color: var(--dim); }
.tl-delete:hover { color: var(--fg); } .tl-delete:hover { color: var(--fg); }
/* The ruler and the corner above the labels stay at the top of the body while
the rows scroll under them: the ruler is the scrubber, and scrolling down to
a row is no reason to lose it. */
.tl-corner,
.tl-ruler { .tl-ruler {
position: relative; position: sticky;
top: 0;
z-index: 2;
height: var(--ruler); height: var(--ruler);
border-bottom: 1px solid var(--line); border-bottom: 1px solid var(--line);
background: var(--chrome); background: var(--chrome);
}
.tl-ruler {
cursor: pointer; cursor: pointer;
user-select: none; user-select: none;
} }
@ -718,11 +736,13 @@ button.share-button:hover, button.share-button.on { filter: brightness(1.1); }
z-index: 3; z-index: 3;
} }
.tl-playhead::before { /* The playhead's head, in the ruler rather than on the line, so it stays at the
content: ""; top with the ruler while the rows scroll. */
.tl-knob {
position: absolute; position: absolute;
pointer-events: none;
margin-left: -4px;
top: 1px; top: 1px;
left: -4px;
width: 9px; width: 9px;
height: 9px; height: 9px;
border-radius: 50%; border-radius: 50%;