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 <noreply@anthropic.com>
This commit is contained in:
Your Name 2026-06-30 10:19:01 -04:00
parent 0c305e42ea
commit 602c710377
2 changed files with 39 additions and 18 deletions

View file

@ -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 */

View file

@ -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)))}]])
: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))}}]))