From 602c71037761dbcfd07679ff0cec008b7e588ee3 Mon Sep 17 00:00:00 2001 From: Your Name Date: Tue, 30 Jun 2026 10:19:01 -0400 Subject: [PATCH] feat: per-annotation scroll-to flag gating vertical scroll on jump Add :meta {:scroll-to bool} to annotations, toggled by a "Scroll to clips on jump" checkbox in the annotation form. goto! now separates follow? (horizontal recenter) from scroll-to? (vertical scroll to the clip's track); the vertical scroll only fires for annotations that opt in, resolved from the canonical scene group at each jump site (jump button/popover, script highlight, link chips). Persists for free via editable-group + the backend's opaque group merge; round-trips through json keywordize, so no migration needed. Co-Authored-By: Claude Opus 4.8 --- tl/resources/public/css/app.css | 3 ++ tl/src/tl/views.cljs | 54 ++++++++++++++++++++++----------- 2 files changed, 39 insertions(+), 18 deletions(-) diff --git a/tl/resources/public/css/app.css b/tl/resources/public/css/app.css index 6fadd50..722c835 100644 --- a/tl/resources/public/css/app.css +++ b/tl/resources/public/css/app.css @@ -239,6 +239,9 @@ body { overflow: hidden; background: var(--desktop); background-size: 2px 2px; .pt-x:hover { text-decoration: underline; } .form-hint { font-size: 11px; color: var(--mute); } +.form-check { display: flex; align-items: center; gap: 6px; font-family: var(--chicago); + font-size: 11px; color: var(--ink); cursor: pointer; user-select: none; } +.form-check input { cursor: pointer; } .form-actions { display: flex; gap: 8px; justify-content: flex-end; margin-top: 4px; } .save, .cancel { padding: 6px 14px; font-size: 13px; } /* default button: the heavy double Mac ring */ diff --git a/tl/src/tl/views.cljs b/tl/src/tl/views.cljs index 4254c0d..66a0b17 100644 --- a/tl/src/tl/views.cljs +++ b/tl/src/tl/views.cljs @@ -197,9 +197,12 @@ :behavior "smooth"})))))) (defn- goto! - "Move the playhead to local frame `local` and seek the video to match." - ([local] (goto! local false)) - ([local follow?] + "Move the playhead to local frame `local` and seek the video to match. + `follow?` recentres horizontally; `scroll-to?` also scrolls the tracks + vertically to the clip under the landing frame (opt-in per annotation)." + ([local] (goto! local false false)) + ([local follow?] (goto! local follow? false)) + ([local follow? scroll-to?] (let [ctx @(rf/subscribe [::subs/context]) segs @(rf/subscribe [::subs/segments]) fps @(rf/subscribe [::subs/fps]) @@ -209,7 +212,7 @@ (when @play (swap! play assoc :idx (seg-at segs local))) ; keep playback in sync (when follow? (r/after-render #(do (follow! fps zoom local true) ; smooth on jump/link - (scroll-to-seg-track! segs local))))))) + (when scroll-to? (scroll-to-seg-track! segs local)))))))) (defn video-monitor [] [:video {:src @(rf/subscribe [::subs/clip-url]) :controls true :preload "auto" @@ -391,9 +394,16 @@ (scene/link-local @(rf/subscribe [::subs/scene]) @(rf/subscribe [::subs/context]) {:ref (keyword ref) :at (js/parseInt at 10)})) -(defn- goto-link! [local] - (when local - (goto! local true))) +(defn- ann-scroll-to? + "Does group `a` opt into vertical scroll-to-clip when navigated to?" + [a] (boolean (get-in a [:meta :scroll-to]))) + +(defn- ref-scroll-to? [scene ref] + (ann-scroll-to? (get-in scene [:groups (keyword ref)]))) + +(defn- goto-link! + ([local] (goto-link! local false)) + ([local scroll-to?] (when local (goto! local true scroll-to?)))) (defn content-display "Read-only render of annotation `content`: markdown blocks (headings, lists, @@ -410,7 +420,7 @@ (let [lf (scene/link-local scene ctx {:ref ref :at at})] [:span.link-chip {:class (when-not lf "broken") :title (when-not lf "linked clip no longer in this timeline") - :on-click #(goto-link! lf)} + :on-click #(goto-link! lf (ref-scroll-to? scene ref))} (when-not lf "△ ") label (when lf [:span.link-f (str " " (js/Math.round lf) "f")])]))))) @@ -483,7 +493,8 @@ :on-input emit :on-key-up save! :on-mouse-up save! :on-blur save! :on-click (fn [e] (when-let [chip (.closest (.-target e) ".link-chip")] (when-let [lf (link-frame (.. chip -dataset -ref) (.. chip -dataset -at))] - (goto-link! lf))))}])) + (goto-link! lf (ref-scroll-to? @(rf/subscribe [::subs/scene]) + (.. chip -dataset -ref))))))}])) (defn- point-candidates [scene ctx q timelines?] (let [needle (str/lower-case q) @@ -649,9 +660,10 @@ than one bar) — a button that opens a popover of one target per piece, each labelled with the clip + frame it lands on (as the annotation editor shows it)." [open a scene segs] - (let [bars (:bars a)] + (let [bars (:bars a) + scroll? (ref-scroll-to? scene (:id a))] (if (< (count bars) 2) - [:button.jump-btn {:on-click #(goto! (:start a) true)} "↪ jump"] + [:button.jump-btn {:on-click #(goto! (:start a) true scroll?)} "↪ jump"] [:span {:style {:position "relative"}} [:button.jump-btn {:on-click #(swap! open (fn [x] (when (not= x (:id a)) (:id a))))} (str "↪ jump (" (count bars) ") ▾")] @@ -659,7 +671,7 @@ [:div.jump-pop (for [[i [lo _]] (map-indexed vector bars)] ^{:key i} - [:button {:on-click #(do (goto! lo true) (reset! open nil))} + [:button {:on-click #(do (goto! lo true scroll?) (reset! open nil))} (str "▸ " (clip-at scene segs lo))])])]))) (defn commentary [] @@ -746,11 +758,16 @@ [:form.form {:on-submit (fn [e] (.preventDefault e) (save))} [:div.form-head (cond root? "Edit description" new? "New annotation" :else "Edit annotation")] (when-not root? - [:div.form-row - [:input.form-name {:placeholder "Name" :value (:name d) - :on-change #(put (assoc d :name (.. % -target -value)))}] - [:input.form-color {:type "color" :value (:color d) - :on-change #(put (assoc d :color (.. % -target -value)))}]]) + [:<> + [:div.form-row + [:input.form-name {:placeholder "Name" :value (:name d) + :on-change #(put (assoc d :name (.. % -target -value)))}] + [:input.form-color {:type "color" :value (:color d) + :on-change #(put (assoc d :color (.. % -target -value)))}]] + [:label.form-check {:title "When you jump to this annotation, scroll the tracks to its clips"} + [:input {:type "checkbox" :checked (boolean (get-in d [:meta :scroll-to])) + :on-change #(put (assoc-in d [:meta :scroll-to] (.. % -target -checked)))}] + [:span "Scroll to clips on jump"]]]) [:div.form-marks-label "Content"] ^{:key gid} [content-editor gid (:content orig)] (if (and linking (= gid (:gid linking))) @@ -905,7 +922,8 @@ (let [[x0 y0 x1 y1] rect] [:div.script-hl {:title (:name a) :data-ann (name (:id a)) :on-mouse-down #(.stopPropagation %) - :on-click #(goto! (:start a) true) ; jump the timeline to this annotation + :on-click #(goto! (:start a) true ; jump the timeline to this annotation + (ref-scroll-to? @(rf/subscribe [::subs/scene]) (:id a))) :style {:left (* x0 w) :top (* y0 h) :width (* (- x1 x0) w) :height (* (- y1 y0) h) :background (str (:color a) "44") :border (str "1.5px solid " (:color a))}}]))