From 96639554e4aa5c578b58e184699ae59bd0935d0d Mon Sep 17 00:00:00 2001 From: Your Name Date: Tue, 30 Jun 2026 02:42:31 -0400 Subject: [PATCH] translucent stuff etc --- tl/resources/public/css/app.css | 67 +++++++++++++++--- tl/src/tl/views.cljs | 119 ++++++++++++++++++-------------- 2 files changed, 128 insertions(+), 58 deletions(-) diff --git a/tl/resources/public/css/app.css b/tl/resources/public/css/app.css index 6d53811..a05ab2b 100644 --- a/tl/resources/public/css/app.css +++ b/tl/resources/public/css/app.css @@ -54,20 +54,20 @@ body { overflow: hidden; background: var(--desktop); background-size: 2px 2px; press. The default action (.save) gets the heavy Mac "default button" ring. */ .jump-btn, .add-btn, .edit-btn, .del-btn, .expand-btn, .back-btn, .crumb, .play-btn, .step-btn, .row-x, .add-mark, .save, .cancel, .hl-ok, .hl-cancel, -.pane-tabs button { +.pane-tabs button, .track-toggle { font-family: var(--chicago); background: var(--paper); color: var(--ink); border: 1px solid var(--ink); border-radius: 8px; cursor: pointer; line-height: 1.3; } .jump-btn:hover, .add-btn:hover, .edit-btn:hover, .del-btn:hover, .expand-btn:hover, .back-btn:hover, .crumb:hover, .play-btn:hover, .step-btn:hover, .row-x:hover, .add-mark:hover, .save:hover, .cancel:hover, .hl-ok:hover, .hl-cancel:hover, -.pane-tabs button:hover { +.pane-tabs button:hover, .track-toggle:hover { background: var(--hover); } .jump-btn:active, .add-btn:active, .edit-btn:active, .del-btn:active, .expand-btn:active, .back-btn:active, .crumb:active, .play-btn:active, .step-btn:active, .row-x:active, .add-mark:active, .save:active, .cancel:active, .hl-ok:active, .hl-cancel:active, -.pane-tabs button:active { +.pane-tabs button:active, .track-toggle:active { background: var(--ink); color: var(--paper); } @@ -289,10 +289,35 @@ body { overflow: hidden; background: var(--desktop); background-size: 2px 2px; .add-annotation:hover { background: var(--paper); color: var(--ink); } .add-annotation:active { box-shadow: 0 0 0 var(--ink); transform: translate(2px, 2px); } -.timeline-scroll { flex: 1; overflow-y: auto; overflow-x: hidden; min-height: 0; } -.timeline { display: flex; align-items: flex-start; background: var(--paper); } +.timeline-scroll { flex: 1; overflow: hidden; min-height: 0; display: flex; } +.timeline { + flex: 1; min-width: 0; min-height: 0; + position: relative; display: flex; flex-direction: column; background: var(--paper); +} +.timeline-head { + position: absolute; top: 0; left: 0; right: 0; z-index: 12; + display: flex; align-items: flex-start; + background: rgba(255, 255, 255, .58); + backdrop-filter: blur(4px); + -webkit-backdrop-filter: blur(4px); +} +html.dark .timeline-head { + background: rgba(0, 0, 0, .58); +} +.timeline-body-scroll { + flex: 1; min-height: 0; display: flex; align-items: flex-start; + overflow-y: auto; overflow-x: hidden; + box-sizing: border-box; +} -.gutter { flex-shrink: 0; width: 150px; user-select: none; } +.gutter-lanes, .gutter-tracks { + flex-shrink: 0; width: 150px; user-select: none; transition: width .2s ease; +} +.gutter-lanes { + position: relative; + box-sizing: border-box; background: transparent; border-right: 1px solid var(--ink); +} +.track-toggle { display: none; } .gutter-label { box-sizing: border-box; border-bottom: 1px solid var(--ink); border-right: 1px solid var(--ink); @@ -300,8 +325,18 @@ body { overflow: hidden; background: var(--desktop); background-size: 2px 2px; background: var(--paper); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } -.hscroll { overflow-x: auto; flex: 1; } -.content { position: relative; cursor: ew-resize; overflow: hidden; } +.hscroll { + overflow-x: auto; overflow-y: hidden; + flex: 1; min-width: 0; + overscroll-behavior-x: contain; +} +.lanes-hscroll { overflow: hidden; flex: 1; min-width: 0; } +.content { position: relative; cursor: ew-resize; } +.track-lane { + position: absolute; left: 0; box-sizing: border-box; + border-bottom: 1px dotted color-mix(in srgb, var(--ink) 22%, transparent); + pointer-events: none; +} .track-row { position: relative; background: var(--paper); border-bottom: 1px solid var(--ink); overflow: hidden; transition: height .3s ease, opacity .3s ease; } @@ -336,10 +371,15 @@ body { overflow: hidden; background: var(--desktop); background-size: 2px 2px; /* --- annotation lane ---------------------------------------------------- */ .ann-lane-row { position: relative; height: 18px; } +.ann-lanes { + position: relative; background: transparent; + box-shadow: 0 1px 0 var(--ink); +} .ann-bar { position: absolute; top: 2px; height: 14px; border-radius: 0; } .ann-bar-label { position: absolute; top: 1px; font-family: var(--chicago); font-size: 9px; line-height: 16px; color: var(--ink); white-space: nowrap; pointer-events: none; } +.lane-playhead { bottom: 0; } /* --- mobile: stack the top region vertically (video over annotation) ---- */ @media (max-width: 720px) { @@ -349,6 +389,17 @@ body { overflow: hidden; background: var(--desktop); background-size: 2px 2px; .annot-pane { width: 100%; min-width: 0; flex: 1 1 0; min-height: 110px; } .divider-h { height: 14px; } .slider input { width: 120px; } + .track-toggle { + display: block; position: absolute; top: 3px; left: 4px; z-index: 10; + max-width: calc(100% - 8px); padding: 2px 6px; font-size: 10px; + white-space: nowrap; overflow: hidden; text-overflow: ellipsis; + } + .timeline.tracks-collapsed .gutter-lanes, + .timeline.tracks-collapsed .gutter-tracks { width: 34px; } + .timeline.tracks-collapsed .gutter-label { + opacity: 0; padding: 0; border-right: 1px solid var(--ink); + pointer-events: none; + } } /* --- landing pages (project list / create) ----------------------------- */ diff --git a/tl/src/tl/views.cljs b/tl/src/tl/views.cljs index 889eb1a..9a0abe6 100644 --- a/tl/src/tl/views.cljs +++ b/tl/src/tl/views.cljs @@ -244,7 +244,9 @@ (set! (.-scrollLeft el) (max 0 (- x (/ (.-clientWidth el) 2))))))) (defn timeline [] - (let [content (atom nil)] + (let [content (atom nil) + lanes-scroll (atom nil) + tracks-open? (r/atom true)] (fn [] (let [scene @(rf/subscribe [::subs/scene]) fps @(rf/subscribe [::subs/fps]) @@ -260,58 +262,75 @@ linking @(rf/subscribe [::subs/linking]) len @(rf/subscribe [::subs/length]) width (px len fps zoom) - lane-h (* 18 (count anns)) + lane-h (max 18 (* 18 (count anns))) + tracks-h (* row-h (count tracks)) track-y (into {} (map-indexed (fn [i t] [(:id t) i]) tracks))] (when playing? (r/after-render #(follow! fps zoom playhead))) - [:div.timeline - [:div.gutter - [:div {:style {:height lane-h}}] - (for [t tracks] - ^{:key (:id t)} - [:div.gutter-label {:style {:height row-h :line-height (str (dec row-h) "px")}} - (:name t)])] - [:div.hscroll {:ref (fn [n] (reset! scroll-el n))} - [:div.content {:ref (fn [n] (reset! content n)) - :on-mouse-down #(scrub! @content fps zoom %) - :style {:width width :height (+ lane-h (* row-h (count tracks)))}} - [:div.playhead {:style {:left (px playhead fps zoom)}} [:div.playhead-handle]] - ;; annotation bars — solid for saved, dashed for the in-progress draft - (for [[i a] (map-indexed vector anns) - [j [lo hi]] (map-indexed vector (:bars a))] - ^{:key (str (:id a) "-" j)} - [:div.ann-bar {:title (:name a) - :on-mouse-down (when-not (:draft a) - (fn [e] (.stopPropagation e) + [:div.timeline {:class (when-not @tracks-open? "tracks-collapsed")} + [:div.timeline-head + [:div.gutter-lanes {:style {:height lane-h}} + [:button.track-toggle {:title (if @tracks-open? "Collapse track names" "Show track names") + :on-click #(swap! tracks-open? not)} + (if @tracks-open? "<- HIDE" "->")]] + [:div.lanes-hscroll {:ref (fn [n] (reset! lanes-scroll n))} + [:div.ann-lanes {:style {:height lane-h :width width} + :on-mouse-down #(scrub! @content fps zoom %)} + [:div.playhead.lane-playhead {:style {:left (px playhead fps zoom)}} + [:div.playhead-handle]] + ;; annotation bars — solid for saved, dashed for the in-progress draft + (for [[i a] (map-indexed vector anns) + [j [lo hi]] (map-indexed vector (:bars a))] + ^{:key (str (:id a) "-" j)} + [:div.ann-bar {:title (:name a) + :on-mouse-down (when-not (:draft a) + (fn [e] (.stopPropagation e) + (if linking + (do (.preventDefault e) ; pick: link to this timeline + (commit-link! {:kind :timeline :ref (:id a)} (:name a))) + (rf/dispatch [::events/expand (:id a)])))) + :style {:top (+ 2 (* i 18)) :height 14 + :left (px lo fps zoom) :width (max 4 (px (- hi lo) fps zoom)) + :background (str (:color a) (if (:draft a) "44" "cc")) + :cursor (if (:draft a) "default" "pointer") + :border-radius 2 + :border (str (if (:draft a) "1px dashed " "1px solid ") (:color a))}}]) + (for [[i a] (map-indexed vector anns) :let [[lo _] (first (:bars a))] :when lo] + ^{:key (str "lbl-" (:id a))} + [:div.ann-bar-label {:style {:top (* i 18) + :left (+ 4 (px lo fps zoom))}} (:name a)])]]] + [:div.timeline-body-scroll {:style {:padding-top lane-h}} + [:div.gutter-tracks + (for [t tracks] + ^{:key (:id t)} + [:div.gutter-label {:style {:height row-h :line-height (str (dec row-h) "px")}} + (:name t)])] + [:div.hscroll {:ref (fn [n] (reset! scroll-el n)) + :on-scroll #(when @lanes-scroll + (set! (.-scrollLeft @lanes-scroll) + (.. % -target -scrollLeft)))} + [:div.content.track-content {:ref (fn [n] (reset! content n)) + :on-mouse-down #(scrub! @content fps zoom %) + :style {:width width :height tracks-h}} + [:div.playhead {:style {:left (px playhead fps zoom)}}] + (for [[i t] (map-indexed vector tracks)] + ^{:key (str "lane-" (:id t))} + [:div.track-lane {:style {:top (* i row-h) :width width :height row-h}}]) + ;; clips (while authoring, click to fill the focused endpoint) + (for [{:keys [mark track src local] :as seg} segs] + (let [[c d] local] + ^{:key (str mark "-" (first src))} + [:div.clip {:class (when authoring? "clickable") + :on-mouse-down (when authoring? + (fn [e] (.stopPropagation e) (.preventDefault e) (if linking - (do (.preventDefault e) ; pick: link to this timeline - (commit-link! {:kind :timeline :ref (:id a)} (:name a))) - (rf/dispatch [::events/expand (:id a)])))) - :style {:position "absolute" :top (+ 2 (* i 18)) :height 14 - :left (px lo fps zoom) :width (max 4 (px (- hi lo) fps zoom)) - :background (str (:color a) (if (:draft a) "44" "cc")) - :cursor (if (:draft a) "default" "pointer") - :border-radius 2 - :border (str (if (:draft a) "1px dashed " "1px solid ") (:color a))}}]) - (for [[i a] (map-indexed vector anns) :let [[lo _] (first (:bars a))] :when lo] - ^{:key (str "lbl-" (:id a))} - [:div.ann-bar-label {:style {:position "absolute" :top (* i 18) - :left (+ 4 (px lo fps zoom))}} (:name a)]) - ;; clips (while authoring, click to fill the focused endpoint) - (for [{:keys [mark track src local] :as seg} segs] - (let [[c d] local] - ^{:key (str mark "-" (first src))} - [:div.clip {:class (when authoring? "clickable") - :on-mouse-down (when authoring? - (fn [e] (.stopPropagation e) (.preventDefault e) - (if linking - (commit-link! (scene/seg-point scene seg 0) - (clip-label scene segs (:mark seg))) - (rf/dispatch [::events/draft-click-seg (:mark seg)])))) - :style {:left (px c fps zoom) :width (max 1 (px (- d c) fps zoom)) - :top (+ lane-h (* (get track-y track 0) row-h)) :height (- row-h 2) - :line-height (str (- row-h 2) "px") :position "absolute"}} - [clip-thumbnail-layer thumbs fps zoom row-h seg] - [:div.clip-name (clip-label scene segs mark)]]))]]])))) + (commit-link! (scene/seg-point scene seg 0) + (clip-label scene segs (:mark seg))) + (rf/dispatch [::events/draft-click-seg (:mark seg)])))) + :style {:left (px c fps zoom) :width (max 1 (px (- d c) fps zoom)) + :top (* (get track-y track 0) row-h) :height (- row-h 2) + :line-height (str (- row-h 2) "px") :position "absolute"}} + [clip-thumbnail-layer thumbs fps zoom row-h seg] + [:div.clip-name (clip-label scene segs mark)]]))]]]])))) ;; --- links: inline markdown chips that seek the timeline ------------------ ;; A link is a ref-point named in the content as `[label](mark:ref@at)` (see