From 277c0c3b63f612ecb5afb87d151e76acccdeb2e9 Mon Sep 17 00:00:00 2001 From: Olive Vaughn Date: Wed, 30 Sep 2026 01:22:01 -0400 Subject: [PATCH] Keep the timeline's ruler and playhead dot in view while its rows scroll Co-Authored-By: Claude Opus 5.5 --- frontend/src/arthur/ui/timeline.cljs | 7 +++---- static/arthur/app.css | 30 +++++++++++++++++++++++----- 2 files changed, 28 insertions(+), 9 deletions(-) diff --git a/frontend/src/arthur/ui/timeline.cljs b/frontend/src/arthur/ui/timeline.cljs index 24eb3ea..35b9981 100644 --- a/frontend/src/arthur/ui/timeline.cljs +++ b/frontend/src/arthur/ui/timeline.cljs @@ -355,9 +355,7 @@ (reset! over nil) (when (< 1 (count from)) (rf/dispatch [::ui/move-node from []]))))} - [:div {:style {:height "var(--ruler)" - :border-bottom "1px solid var(--line)" - :background "var(--chrome)"}}] + [:div.tl-corner] (doall (for [row visible] ^{:key (str (:path row))} [label-cell row selection over]))] [:div.tl-tracks @@ -401,7 +399,8 @@ :on-pointer-cancel (fn [_] (reset! scrubbing false))} (doall (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) (doall (for [row visible] ^{:key (str (:path row))} [track-cell row frames sliding])) diff --git a/static/arthur/app.css b/static/arthur/app.css index fb46057..0808ea3 100644 --- a/static/arthur/app.css +++ b/static/arthur/app.css @@ -562,9 +562,16 @@ button.share-button:hover, button.share-button.on { filter: brightness(1.1); } flex: 1; min-height: 0; 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; } +.tl-labels, +.tl-tracks { min-height: 100%; } + .tl-labels { flex: 0 0 var(--label); /* 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; position: sticky; 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); 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: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 { - position: relative; + position: sticky; + top: 0; + z-index: 2; height: var(--ruler); border-bottom: 1px solid var(--line); background: var(--chrome); +} + +.tl-ruler { cursor: pointer; user-select: none; } @@ -718,11 +736,13 @@ button.share-button:hover, button.share-button.on { filter: brightness(1.1); } z-index: 3; } -.tl-playhead::before { - content: ""; +/* The playhead's head, in the ruler rather than on the line, so it stays at the + top with the ruler while the rows scroll. */ +.tl-knob { position: absolute; + pointer-events: none; + margin-left: -4px; top: 1px; - left: -4px; width: 9px; height: 9px; border-radius: 50%;