diff --git a/tl/resources/public/css/app.css b/tl/resources/public/css/app.css
index 8443a74..c9334ee 100644
--- a/tl/resources/public/css/app.css
+++ b/tl/resources/public/css/app.css
@@ -55,20 +55,20 @@ body { overflow: hidden; background: var(--desktop); background-size: 4px 4px;
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, .track-toggle {
+.pane-tabs button, .track-toggle, .mark-script, .mark-script-new {
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, .track-toggle:hover {
+.pane-tabs button:hover, .track-toggle:hover, .mark-script:hover, .mark-script-new: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, .track-toggle:active {
+.pane-tabs button:active, .track-toggle:active, .mark-script:active, .mark-script-new:active {
background: var(--ink); color: var(--paper);
}
@@ -101,10 +101,13 @@ body { overflow: hidden; background: var(--desktop); background-size: 4px 4px;
background: none; cursor: pointer; }
.draw-tools .draw-wid { width: 56px; }
.draw-done { font-weight: bold; }
-.mark-draw { background: none; border: 1px solid transparent; border-radius: 0;
- font-size: 12px; cursor: pointer; padding: 0 3px; line-height: 1; }
-.mark-draw:hover { border-color: var(--ink); }
-.mark-draw.has { border-color: var(--ink); background: var(--paper); }
+.mark-draw, .mark-script {
+ width: 28px; height: 26px; flex: none;
+ display: inline-flex; align-items: center; justify-content: center;
+ border-radius: 0; font-size: 13px; cursor: pointer; padding: 0; line-height: 1;
+}
+.mark-draw { background: var(--paper); color: var(--ink); border: 1px solid var(--ink); }
+.mark-draw.has, .mark-script.has { background: var(--desktop); background-size: 4px 4px; }
.frame-readout {
position: absolute; bottom: 6px; right: 8px; z-index: 4;
@@ -209,10 +212,13 @@ body { overflow: hidden; background: var(--desktop); background-size: 4px 4px;
.form {
flex: 1; min-width: 0; overflow-y: auto;
background: var(--paper); border: 1px solid var(--ink); box-sizing: border-box;
- padding: 10px 12px;
- display: flex; flex-direction: column; gap: 10px;
+ padding: 12px;
+ display: flex; flex-direction: column; gap: 12px;
+}
+.form-head {
+ font-family: var(--chicago); font-size: 14px; color: var(--ink);
+ padding-bottom: 6px; border-bottom: 2px solid var(--ink);
}
-.form-head { font-family: var(--chicago); font-size: 14px; color: var(--ink); }
.form-row { display: flex; gap: 8px; align-items: center; }
.form-name { flex: 1; }
.form input[type=text], .form-name, .form-content {
@@ -224,7 +230,7 @@ body { overflow: hidden; background: var(--desktop); background-size: 4px 4px;
.form-content { width: 100%; min-height: 70px; resize: vertical; box-sizing: border-box;
font-family: inherit; }
.form-marks-label { font-family: var(--chicago); font-size: 11px; letter-spacing: .5px;
- color: var(--ink); margin-top: 4px; }
+ color: var(--ink); margin-top: 2px; padding-top: 2px; }
/* contenteditable content surface + inline link chips */
.content-editor { white-space: pre-wrap; word-break: break-word; outline: none; cursor: text;
@@ -239,8 +245,11 @@ body { overflow: hidden; background: var(--desktop); background-size: 4px 4px;
.link-chip .link-f { font-size: 11px; opacity: .7; }
.link-chip:hover .link-f { opacity: 1; }
-.mark-row { display: flex; flex-wrap: nowrap; align-items: center; gap: 6px; min-width: 0; }
-.mark-arrow { color: var(--ink); }
+.mark-row { display: flex; flex-wrap: wrap; align-items: center; gap: 6px; min-width: 0; }
+.mark-arrow {
+ color: var(--ink); flex: none; font-family: var(--chicago);
+ min-width: 16px; text-align: center;
+}
.row-x, .add-mark { padding: 3px 8px; font-size: 12px; }
.add-mark { align-self: flex-start; }
@@ -251,7 +260,7 @@ body { overflow: hidden; background: var(--desktop); background-size: 4px 4px;
/* point editor + autocomplete */
.pt-input { position: relative; flex: 1 1 130px; display: flex; align-items: center; min-width: 0; }
-.mark-row .pt-input { flex: 0 1 160px; max-width: 180px; }
+.mark-row .pt-input { flex: 1 1 128px; max-width: 190px; }
.pt-text {
flex: 1; min-width: 0; box-sizing: border-box;
background: var(--paper); color: var(--ink); border: 1px solid var(--ink);
@@ -277,11 +286,18 @@ body { overflow: hidden; background: var(--desktop); background-size: 4px 4px;
.link-insert { display: flex; align-items: flex-start; gap: 6px; align-self: stretch; }
.link-insert .pt-input { flex: 1 1 auto; max-width: none; }
+.link-insert-block .pt-input { flex: 1 1 220px; max-width: none; }
.hl-ok, .hl-cancel { padding: 2px 9px; font-size: 12px; }
-.pt-chip { flex: 1 1 0; min-width: 0; display: flex; align-items: center; gap: 4px;
+.pt-chip { flex: 1 1 128px; min-width: 0; display: flex; align-items: center; gap: 4px;
background: var(--paper); border: 1px solid var(--ink); border-radius: 0;
- padding: 2px 4px; }
+ padding: 3px 5px; min-height: 20px; box-shadow: inset -1px -1px 0 var(--shade); }
+.pt-chip.empty {
+ border-style: dashed; color: var(--mute); background: var(--shade);
+ box-shadow: none;
+}
+.pt-chip.pending { background: var(--paper); }
+.pt-chip.pending .pt-frame { color: var(--ink); opacity: 1; }
.pt-chip-name { font-size: 12px; color: var(--ink); white-space: nowrap;
overflow: hidden; text-overflow: ellipsis; flex: 1; }
.pt-frame { width: 56px; background: var(--paper); color: var(--ink); border: 1px solid var(--ink);
@@ -658,6 +674,17 @@ html.dark .timeline-head {
.zoom-read { font-size: 11px; color: var(--ink); min-width: 42px; text-align: center; }
.script-empty { color: var(--mute); font-size: 12px; padding: 20px; }
.ann.selected { box-shadow: inset 3px 0 0 var(--ink); }
+.script-return {
+ display: flex; align-items: center; gap: 8px; flex-wrap: wrap;
+ padding: 6px 10px; border-bottom: 2px solid var(--ink);
+ background: var(--desktop); background-size: 4px 4px;
+ font-family: var(--chicago); font-size: 11px;
+}
+.script-return .hl-cancel { border-radius: 0; padding: 2px 8px; }
+.script-return-text {
+ display: inline-block; background: var(--paper); border: 1px solid var(--ink);
+ padding: 2px 6px; box-shadow: 1px 1px 0 var(--ink);
+}
/* note rail (list of project script-notes) */
.note-rail { display: flex; align-items: center; gap: 8px; flex-wrap: wrap;
@@ -695,23 +722,42 @@ html.dark .timeline-head {
font-size: 11px; border: 1px solid var(--mute); border-radius: 0; padding: 3px; background: var(--paper); color: var(--ink); }
/* binding notes to annotations/marks (annotation form + cards) */
-.mark-block { margin-bottom: 4px; }
+.mark-block {
+ margin-bottom: 6px; padding: 5px 6px;
+ border: 1px solid var(--ink); background: var(--paper);
+ box-shadow: 2px 2px 0 var(--ink);
+ cursor: pointer;
+}
+.mark-block:hover { background: var(--shade); }
+.mark-block.pending { cursor: default; }
+.mark-block.pending:hover { background: var(--paper); }
/* the currently-selected/last-touched mark while authoring — so you know which
one a drawing binds to, and which you're about to delete */
-.mark-block.active-mark { box-shadow: inset 3px 0 0 var(--ink); background: var(--shade, rgba(128,128,128,.14));
- border-radius: 2px; padding: 2px 0 2px 3px; margin-left: -3px; }
+.mark-block.active-mark {
+ background: var(--desktop); background-size: 4px 4px;
+ box-shadow: inset 4px 0 0 var(--ink), 2px 2px 0 var(--ink);
+}
.mark-block.broken { opacity: 0.6; }
.mark-block.broken .pt-chip-name { text-decoration: line-through; }
-.note-drop { display: flex; align-items: center; flex-wrap: wrap; gap: 4px; min-height: 20px;
- margin: 2px 0 2px 14px; padding: 2px 4px; border: 1px dashed var(--mute); border-radius: 0; }
-.note-drop-label { font-size: 10px; color: var(--mute); }
-.note-drop-hint { font-size: 10px; color: var(--mute); font-style: italic; }
-.note-source { display: flex; flex-wrap: wrap; gap: 4px; margin: 4px 0; }
-.note-src { display: inline-flex; align-items: center; gap: 4px; font-size: 11px; cursor: grab;
- background: var(--paper); color: var(--ink); border: 1px solid var(--ink); border-radius: 0; padding: 1px 6px; }
-.note-src:active { cursor: grabbing; }
+.mark-note-list {
+ display: flex; flex-wrap: wrap; gap: 4px;
+ margin: 5px 0 0 24px;
+}
+.mark-script-wrap { position: relative; display: inline-flex; flex: none; }
+.mark-script-pop {
+ position: absolute; right: 0; top: calc(100% + 4px); z-index: 21;
+ width: 210px; padding: 6px;
+ background: var(--paper); border: 1px solid var(--ink); box-shadow: 2px 2px 0 var(--ink);
+}
+.mark-script-pop .pt-input { display: block; max-width: none; }
+.mark-script-pop .pt-dropdown { position: static; margin-top: 4px; box-shadow: none; }
+.mark-script-new {
+ width: 100%; margin-top: 6px; padding: 5px 8px;
+ border-radius: 0; text-align: left; font-size: 11px;
+}
.bound-note { display: inline-flex; align-items: center; gap: 3px; font-size: 11px;
- background: var(--paper); color: var(--ink); border: 1px solid var(--ink); border-radius: 0; padding: 0 4px; }
+ background: var(--paper); color: var(--ink); border: 1px solid var(--ink); border-radius: 0; padding: 1px 4px;
+ cursor: pointer; }
.bound-note.live { background: var(--ink); color: var(--paper); }
.bound-note-name { max-width: 120px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.note-live { font-weight: bold; }
@@ -783,6 +829,7 @@ html.dark .timeline-head {
padding: 0 2px; font-size: 13px; line-height: 1; flex: none; align-self: center;
}
.mark-grip:active { cursor: grabbing; }
+.mark-grip.disabled { cursor: default; opacity: .35; }
.mark-block.dragging { opacity: 0.4; }
.mark-block.drop-before { position: relative; }
.mark-block.drop-before::before {
diff --git a/tl/src/tl/events.cljs b/tl/src/tl/events.cljs
index d0f6700..7a0c9ac 100644
--- a/tl/src/tl/events.cljs
+++ b/tl/src/tl/events.cljs
@@ -378,6 +378,9 @@
(merge {:id rid :kind :text :content ""} region))]
(merge {:db db} (persist-note-fx db gid)))))
+(defn- add-in [coll x] (vec (distinct (conj (vec coll) x))))
+(defn- rm-in [coll x] (vec (remove #(= x %) coll)))
+
;; select-then-highlight with no active note: spin up a note (named after the
;; selected text) with the region already in it, and make it active.
(rf/reg-event-fx ::highlight-into-new-note
@@ -421,9 +424,6 @@
;; The pointer lives on the referrer (annotation or mark), so a shared note stays
;; pure and freely reusable. These mutate the draft in-place (db only) and ride
;; the annotation's Save, exactly like ::set-content and the marks editor.
-(defn- add-in [coll x] (vec (distinct (conj (vec coll) x))))
-(defn- rm-in [coll x] (vec (remove #(= x %) coll)))
-
(rf/reg-event-db ::bind-note-annotation
(fn [db [_ gid note-gid]]
(update-in db [:scene :groups gid :notes] #(add-in % note-gid))))
@@ -443,8 +443,66 @@
(update-mark db gid mark-id #(update % :notes rm-in note-gid))))
;; click a mark row to make it the active one (drawings/edits target it)
-(rf/reg-event-db ::set-active-mark
- (fn [db [_ mark-id]] (assoc-in db [:view :active-mark] mark-id)))
+(defn- drawing-state-for [db ann mark-id]
+ (let [existing (first (some (fn [m] (when (= mark-id (:id m)) (:drawings m)))
+ (get-in db [:scene :groups ann :marks])))]
+ {:ann ann :mark-id mark-id
+ :gid (or existing (keyword (str "draw-" (random-uuid))))
+ :new? (nil? existing)}))
+
+(defn- start-drawing-db [db ann mark-id]
+ (-> db
+ (assoc-in [:view :active-mark] mark-id)
+ (assoc-in [:view :draw] (drawing-state-for db ann mark-id))))
+
+(defn- draft-in-ctx [db ctx]
+ (some (fn [[gid g]]
+ (when (and (:draft g) (= ctx (:parent g))) [gid g]))
+ (get-in db [:scene :groups])))
+
+(defn- draft-mark-at [db ctx lf]
+ (when-let [[gid g] (draft-in-ctx db ctx)]
+ (let [scene (:scene db)
+ segs (scene/content-segments scene ctx)]
+ (some (fn [m]
+ (when (some (fn [[lo hi]] (and (<= lo lf) (< lf hi)))
+ (scene/mark-bars scene gid (:id m) segs))
+ {:ann gid :mark-id (:id m)}))
+ (:marks g)))))
+
+(defn- sync-draft-mark-for-playhead [db ctx lf]
+ (if-let [{:keys [ann mark-id]} (draft-mark-at db ctx lf)]
+ (if (= mark-id (get-in db [:view :active-mark]))
+ db
+ (start-drawing-db db ann mark-id))
+ (if (draft-in-ctx db ctx)
+ (-> db
+ (assoc-in [:view :active-mark] nil)
+ (assoc-in [:view :draw] nil))
+ db)))
+
+(defn- mark-start-local [db ann mark-id]
+ (let [scene (:scene db)
+ ctx (:parent (get-in scene [:groups ann]))
+ segs (scene/content-segments scene ctx)]
+ (ffirst (scene/mark-bars scene ann mark-id segs))))
+
+;; click/select a mark row: make it the drawing target and move the playhead to it.
+(rf/reg-event-fx ::set-active-mark
+ (fn [{:keys [db]} [_ mark-id]]
+ (let [[ann _] (or (draft-in-ctx db (peek (get-in db [:view :stack])))
+ (some (fn [[gid g]] (when (:draft g) [gid g]))
+ (get-in db [:scene :groups])))
+ ctx (:parent (get-in db [:scene :groups ann]))
+ local (when ann (mark-start-local db ann mark-id))
+ db (cond-> db
+ ann (start-drawing-db ann mark-id)
+ (and ctx local) (assoc-in [:view :playheads ctx] local))
+ sf (when (and ctx local)
+ (scene/local->source
+ (scene/content-segments (:scene db) ctx) local))]
+ (cond-> (sync-route {:db db} db)
+ sf (assoc :player/seek (/ sf (:fps db)))))))
;; "add a new script note for this mark": remember the target mark + hop to the
;; Script pane. When a note is created there (highlight-into-new-note) it binds to
@@ -452,6 +510,7 @@
(rf/reg-event-db ::new-note-for-mark
(fn [db [_ gid mark-id]]
(-> db (assoc-in [:view :note-target] {:gid gid :mark-id mark-id})
+ (assoc-in [:view :active-note] nil)
(assoc-in [:view :pane] :script))))
(rf/reg-event-db ::cancel-note-target
(fn [db _] (-> db (assoc-in [:view :note-target] nil)
@@ -468,14 +527,7 @@
;; gid. The entity isn't written until ::save-drawing, so cancel is a clean no-op.
(rf/reg-event-db ::start-drawing
(fn [db [_ ann mark-id]]
- (let [existing (first (some (fn [m] (when (= mark-id (:id m)) (:drawings m)))
- (get-in db [:scene :groups ann :marks])))]
- (-> db
- (assoc-in [:view :active-mark] mark-id) ; drawing a mark makes it the active one
- (assoc-in [:view :draw]
- {:ann ann :mark-id mark-id
- :gid (or existing (keyword (str "draw-" (random-uuid))))
- :new? (nil? existing)})))))
+ (start-drawing-db db ann mark-id)))
;; leave draw mode. Strokes autocommit as you draw, so this is just "done" —
;; nothing to save or discard here (the form's Save/Cancel is the rollback net).
@@ -553,7 +605,9 @@
(rf/reg-event-fx ::set-playhead
(fn [{:keys [db]} [_ ctx lf]]
- (let [next-db (assoc-in db [:view :playheads ctx] lf)]
+ (let [next-db (-> db
+ (assoc-in [:view :playheads ctx] lf)
+ (sync-draft-mark-for-playhead ctx lf))]
(sync-route {:db next-db} next-db))))
(rf/reg-event-db ::set-playing (fn [db [_ p]] (assoc-in db [:view :playing?] p)))
;; reveal an annotation's immediate children into the current timeline lane
diff --git a/tl/src/tl/md.cljs b/tl/src/tl/md.cljs
index 169b9e4..e9b4d6f 100644
--- a/tl/src/tl/md.cljs
+++ b/tl/src/tl/md.cljs
@@ -18,20 +18,22 @@
;; Two link kinds share the [label](scheme…) form:
;; frame [label](mark:ref@at) jump the playhead to a spot
;; timeline [label](timeline:gid) push that timeline onto the stack
+;; note [label](note:gid) jump to a script note
(def ^:private link-re
- "\\[([^\\]]*)\\]\\((?:mark:([^@)]+)@(-?\\d+)|timeline:([^)]+))\\)")
+ "\\[([^\\]]*)\\]\\((?:mark:([^@)]+)@(-?\\d+)|timeline:([^)]+)|note:([^)]+))\\)")
(defn link-token
"The token for a link map: {:kind :frame :label :ref :at} (the default) or
- {:kind :timeline :label :ref}."
+ {:kind :timeline :label :ref} or {:kind :script-note :label :ref}."
[{:keys [kind label ref at]}]
- (if (= kind :timeline)
- (str "[" label "](timeline:" (name ref) ")")
+ (case kind
+ :timeline (str "[" label "](timeline:" (name ref) ")")
+ :script-note (str "[" label "](note:" (name ref) ")")
(str "[" label "](mark:" (name ref) "@" at ")")))
(defn parse-content
"Split content `s` into [:text str] / [:link {…}] segments. Each link carries
- :kind — :frame (with :ref/:at) or :timeline (with :ref)."
+ :kind — :frame (with :ref/:at), :timeline (with :ref), or :script-note."
[s]
(if (empty? s)
[]
@@ -39,10 +41,11 @@
(loop [out [] last 0]
(if-let [m (.exec re s)]
(let [idx (.-index m) pre (subs s last idx)
- link (if (aget m 4)
- {:kind :timeline :label (aget m 1) :ref (keyword (aget m 4))}
- {:kind :frame :label (aget m 1) :ref (keyword (aget m 2))
- :at (js/parseInt (aget m 3) 10)})]
+ link (cond
+ (aget m 4) {:kind :timeline :label (aget m 1) :ref (keyword (aget m 4))}
+ (aget m 5) {:kind :script-note :label (aget m 1) :ref (keyword (aget m 5))}
+ :else {:kind :frame :label (aget m 1) :ref (keyword (aget m 2))
+ :at (js/parseInt (aget m 3) 10)})]
(recur (cond-> out
(seq pre) (conj [:text pre])
:always (conj [:link link]))
@@ -57,8 +60,9 @@
(= 3 (.-nodeType n)) (.-textContent n)
(= "BR" (.-tagName n)) "\n"
(and (.-classList n) (.contains (.-classList n) "link-chip"))
- (link-token (if (= "timeline" (.. n -dataset -kind))
- {:kind :timeline :label (.. n -dataset -label) :ref (keyword (.. n -dataset -ref))}
+ (link-token (case (.. n -dataset -kind)
+ "timeline" {:kind :timeline :label (.. n -dataset -label) :ref (keyword (.. n -dataset -ref))}
+ "script-note" {:kind :script-note :label (.. n -dataset -label) :ref (keyword (.. n -dataset -ref))}
{:kind :frame :label (.. n -dataset -label) :ref (keyword (.. n -dataset -ref))
:at (js/parseInt (.. n -dataset -at) 10)}))
;; A wrapper element (e.g. the
a browser inserts on Enter): recurse so
diff --git a/tl/src/tl/subs.cljs b/tl/src/tl/subs.cljs
index 5153578..4e798e2 100644
--- a/tl/src/tl/subs.cljs
+++ b/tl/src/tl/subs.cljs
@@ -22,6 +22,7 @@
;; :choosing (fresh draft — marks + title picker only) | :creating (full form).
(rf/reg-sub ::draft-stage (fn [db] (get-in db [:view :draft-stage])))
(rf/reg-sub ::script-jump (fn [db] (get-in db [:view :script-jump])))
+(rf/reg-sub ::note-target (fn [db] (get-in db [:view :note-target])))
(rf/reg-sub ::hidden-notes (fn [db] (get-in db [:view :hidden-notes] #{})))
(rf/reg-sub ::hidden-tags (fn [db] (get-in db [:view :hidden-tags] #{})))
(rf/reg-sub ::region-focus (fn [db] (get-in db [:view :region-focus])))
diff --git a/tl/src/tl/views.cljs b/tl/src/tl/views.cljs
index 9000ff9..2cad17e 100644
--- a/tl/src/tl/views.cljs
+++ b/tl/src/tl/views.cljs
@@ -790,10 +790,10 @@
:border "1px solid rgba(78,143,194,0.85)"
:pointer-events "none" :z-index 5}}])]]]]))))
-;; --- links: inline markdown chips that seek the timeline ------------------
-;; A link is a ref-point named in the content as `[label](mark:ref@at)` (see
-;; tl.scene). The editor is a contenteditable surface where links live as atomic
-;; chips (delete with backspace, click to seek); everything else is plain text.
+;; --- links: inline markdown chips -----------------------------------------
+;; Content links are named tokens like `[label](mark:ref@at)`,
+;; `[label](timeline:gid)`, or `[label](note:gid)` (see tl.md). The editor is a
+;; contenteditable surface where links live as atomic chips.
(defn- link-frame
"ctx-local frame for a chip's {:ref :at}, resolved against the live scene."
@@ -804,18 +804,30 @@
(defn- goto-link! [local]
(when local (goto! local true)))
+(defn- note-link-ok? [scene ref]
+ (= :script-note (get-in scene [:groups ref :type])))
+
(defn content-display
"Read-only render of annotation `content`: markdown blocks (headings, lists,
paragraphs) with inline formatting plus clickable link chips."
[scene ctx content]
(md/render content
(fn [{:keys [kind label ref at]}]
- (if (= :timeline kind)
+ (case kind
+ :timeline
(let [path (scene/path-to scene ref)]
[:span.link-chip {:class (when-not path "broken")
:title (when-not path "timeline no longer here")
:on-click #(when path (rf/dispatch [::events/open-stack path]))}
"⤢ " label])
+
+ :script-note
+ (let [ok? (note-link-ok? scene ref)]
+ [:span.link-chip {:class (when-not ok? "broken")
+ :title (when-not ok? "script note no longer here")
+ :on-click #(when ok? (rf/dispatch [::events/jump-to-note ref]))}
+ (if ok? "📄 " "△ ") label])
+
(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")
@@ -827,18 +839,29 @@
(let [span (js/document.createElement "span")
scene @(rf/subscribe [::subs/scene])
timeline? (= :timeline kind)
- lf (when-not timeline?
+ note? (= :script-note kind)
+ lf (when-not (or timeline? note?)
(scene/link-local scene @(rf/subscribe [::subs/context]) {:ref ref :at at}))
- ok (if timeline? (some? (scene/path-to scene ref)) lf)]
+ ok (cond
+ timeline? (some? (scene/path-to scene ref))
+ note? (note-link-ok? scene ref)
+ :else lf)]
(set! (.-className span) (if ok "link-chip" "link-chip broken"))
(set! (.-contentEditable span) "false")
- (when-not ok (set! (.-title span) (if timeline? "timeline no longer here"
- "linked clip no longer in this timeline")))
- (set! (.-textContent span) (str (cond timeline? "⤢ " (not ok) "△ ") label))
+ (when-not ok (set! (.-title span) (cond
+ timeline? "timeline no longer here"
+ note? "script note no longer here"
+ :else "linked clip no longer in this timeline")))
+ (set! (.-textContent span) (str (cond
+ timeline? "⤢ "
+ note? "📄 "
+ (not ok) "△ "
+ :else "")
+ label))
(aset (.-dataset span) "label" label)
(aset (.-dataset span) "ref" (name ref))
(when kind (aset (.-dataset span) "kind" (name kind)))
- (when-not timeline? (aset (.-dataset span) "at" (str at)))
+ (when-not (or timeline? note?) (aset (.-dataset span) "at" (str at)))
(when lf
(let [f (js/document.createElement "span")]
(set! (.-className f) "link-f")
@@ -891,10 +914,16 @@
(.removeAllRanges s) (.addRange s r))))
: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))))}]))
+ (case (.. chip -dataset -kind)
+ "timeline" (when-let [path (scene/path-to @(rf/subscribe [::subs/scene])
+ (keyword (.. chip -dataset -ref)))]
+ (rf/dispatch [::events/open-stack path]))
+ "script-note" (rf/dispatch [::events/jump-to-note
+ (keyword (.. chip -dataset -ref))])
+ (when-let [lf (link-frame (.. chip -dataset -ref) (.. chip -dataset -at))]
+ (goto-link! lf)))))}]))
-(defn- point-candidates [scene ctx q timelines?]
+(defn- point-candidates [scene ctx notes q timelines?]
(let [needle (str/lower-case q)
n (when (re-matches #"-?\d+" q) (js/parseInt q 10))]
(vec
@@ -915,7 +944,13 @@
(keep (fn [{:keys [gid name in path]}]
(when (str/includes? (str/lower-case (str "timeline " name " " in)) needle)
{:label name :kind :timeline :group (when in (str "in " in))
- :point {:kind :timeline :ref gid} :path path})))))))))
+ :point {:kind :timeline :ref gid} :path path})))))
+ (when timelines?
+ (->> notes
+ (keep (fn [{:keys [id name]}]
+ (when (str/includes? (str/lower-case (str "note script " name)) needle)
+ {:label name :kind :script-note :group "script note"
+ :point {:kind :script-note :ref id}})))))))))
(defn- point-label [cand off local]
(str (:label cand) " @" (js/Math.round local) "f"
@@ -923,7 +958,7 @@
(str " +" off))))
(defn- pick-point [cand off]
- (if (= :timeline (:kind cand))
+ (if (contains? #{:timeline :script-note} (:kind cand))
(assoc (:point cand) :label (:label cand) :local (:local cand) :candidate cand :offset 0)
(let [o (if (:abs cand) 0 (min (max 0 off) (max 0 (dec (or (:len cand) 1)))))
local (+ (:local cand) o)]
@@ -945,7 +980,8 @@
;; start CLOSED; only opens on focus (an auto-focused picker opens
;; itself via :on-focus). Was defaulting open even when unfocused.
open? (r/atom (boolean auto-focus?))]
- (let [cands (point-candidates scene ctx @text timelines?)
+ (let [notes @(rf/subscribe [::subs/notes])
+ cands (point-candidates scene ctx notes @text timelines?)
i (min @idx (max 0 (dec (count cands))))
cur (or (:candidate @picked) (get cands i))
maxo (max 0 (dec (or (:len cur) 1)))
@@ -956,6 +992,7 @@
(cond
(not timelines?) (goto! (:local (pick-point cand o2)))
(= :timeline (:kind cand)) (rf/dispatch [::events/open-stack (:path cand)])
+ (= :script-note (:kind cand)) nil
:else (rf/dispatch [::events/preview-frame
(:local (pick-point cand o2))]))))
emit! (fn [cand o2]
@@ -989,7 +1026,7 @@
(reset! idx 0)
(reset! off 0)
(reset! open? true)
- (preview! (first (point-candidates scene ctx (.. % -target -value) timelines?)) 0))
+ (preview! (first (point-candidates scene ctx notes (.. % -target -value) timelines?)) 0))
:on-key-down
(fn [e]
(case (.-key e)
@@ -1022,10 +1059,13 @@
:on-mouse-down #(.preventDefault %)
:on-mouse-enter #(go! j)
:on-click #(emit! c 0)}
- (when timelines? [:span.cand-kind (if (= :timeline (:kind c)) "⤢ " "↪ ")])
+ (when timelines? [:span.cand-kind (case (:kind c)
+ :timeline "⤢ "
+ :script-note "📄 "
+ "↪ ")])
[:span.cand-label (:label c)]
(when (:group c) [:span.cand-group (str " " (:group c))])
- (when-not (= :timeline (:kind c))
+ (when-not (contains? #{:timeline :script-note} (:kind c))
[:span.link-f (str " " (js/Math.round (:local c)) "f")])])))])))
(defn- local->draft-point [segs local]
@@ -1093,14 +1133,17 @@
(defn- link-picker [{:keys [scene ctx on-commit on-cancel]}]
(r/with-let [picked (r/atom nil)]
- [:div.link-insert
- [point-picker {:scene scene :ctx ctx :value @picked :auto-focus? true :timelines? true
- :on-pick #(reset! picked %)
- :on-cancel on-cancel}]
- [:button.hl-ok {:type "button" :title "Insert link" :disabled (nil? @picked)
- :on-click #(when @picked (on-commit @picked))}
- "✓"]
- [:button.hl-cancel {:type "button" :title "Cancel" :on-click on-cancel} "✕"]]))
+ [:div.mark-block.pending.link-insert-block
+ [:div.mark-row
+ [:span.mark-grip.disabled {:title "Insert link"} "↪"]
+ [point-picker {:scene scene :ctx ctx :value @picked :auto-focus? true :timelines? true
+ :placeholder "clip / annotation / timeline / note..."
+ :on-pick #(reset! picked %)
+ :on-cancel on-cancel}]
+ [:button.hl-ok {:type "button" :title "Insert link" :disabled (nil? @picked)
+ :on-click #(when @picked (on-commit @picked))}
+ "✓"]
+ [:button.hl-cancel {:type "button" :title "Cancel" :on-click on-cancel} "✕"]]]))
(defn- commit-link!
"Insert a link chip for ref-point `pt` (label `label`) at the editor caret."
@@ -1284,7 +1327,9 @@
:on-change #(put (assoc-in d [:marks i k :at] (to-frame (.. % -target -value) len)))}]
[:span.pt-dur (str "/" len)]
[:button.pt-chip-x {:type "button" :title "Re-pick this end"
- :on-click #(rf/dispatch [::events/unset-endpoint (:gid d) i k])} "✕"]]))
+ :on-click (fn [e]
+ (.stopPropagation e)
+ (rf/dispatch [::events/unset-endpoint (:gid d) i k]))} "✕"]]))
(defn- proxy-frame-chip
"Editable endpoint for a proxy (synthetic-clip) mark: clip name + frame input
@@ -1301,41 +1346,89 @@
(to-frame (.. % -target -value) len)])}]
[:span.pt-dur (str "/" len)]
[:button.pt-chip-x {:type "button" :title "Re-pick this end"
- :on-click #(rf/dispatch [::events/unset-proxy-endpoint gid mark-id pid which])} "✕"]]))
+ :on-click (fn [e]
+ (.stopPropagation e)
+ (rf/dispatch [::events/unset-proxy-endpoint gid mark-id pid which]))} "✕"]]))
+
+(defn- pending-frame-chip [scene segs {:keys [seg f]}]
+ (let [len (scene/seg-length segs seg)]
+ [:div.pt-chip.pending
+ [:span.pt-chip-name (clip-label scene segs seg)]
+ [:input.pt-frame {:type "number" :value (js/Math.round f) :disabled true}]
+ [:span.pt-dur (str "/" len)]]))
+
+(defn- empty-frame-chip [label]
+ [:div.pt-chip.empty
+ [:span.pt-chip-name label]
+ [:span.pt-dur "empty"]])
+
+(defn- pending-mark-block [{:keys [scene ctx segs pt choosing?]}]
+ (let [picked? (map? pt)]
+ [:div.mark-block.pending
+ [:div.mark-row
+ [:span.mark-grip.disabled {:title "New mark"} "⠿"]
+ (if picked?
+ [pending-frame-chip scene segs pt]
+ [point-picker {:scene scene :ctx ctx :class "active" :auto-focus? true
+ :placeholder "click start clip..."
+ :on-pick #(when-let [p (local->draft-point segs (:local %))]
+ (rf/dispatch [::events/draft-click-seg (:seg p) (:f p)]))}])
+ [:span.mark-arrow "→"]
+ (if picked?
+ [point-picker {:scene scene :ctx ctx :class "active" :auto-focus? true
+ :placeholder "click end clip..."
+ :on-pick #(let [end-local (if (and (zero? (:offset %))
+ (pos? (or (get-in % [:candidate :len]) 0)))
+ (+ (:local %) (get-in % [:candidate :len]))
+ (:local %))]
+ (when-let [p (local->draft-end-point segs end-local)]
+ (rf/dispatch [::events/draft-click-seg (:seg p) (:f p)])))}]
+ [empty-frame-chip "End point"])
+ [:button.mark-draw {:type "button" :title "Finish the mark before drawing" :disabled true} "🖼+"]
+ (when-not choosing?
+ [:button.mark-script {:type "button" :title "Finish the mark before adding a script note" :disabled true} "📄+"])
+ [:button.row-x {:type "button" :title "Finish or cancel from the timeline" :disabled true} "✕"]]]))
;; --- script-note bindings (annotation form) ------------------------------
-;; A note is bound by dragging its chip from the source list onto a drop target
-;; (a mark row, or the annotation-level target). The pointer lives on the referrer
-;; and rides the annotation's Save.
+;; The pointer lives on the mark and rides the annotation's Save. Existing notes
+;; bind from a compact autocomplete; Add new jumps to the script pane and binds
+;; the highlighted note when it is created.
-(defn- note-src-chip
- "Draggable source chip for note `n`; click also toggles an annotation-level bind."
- [gid n]
- [:span.note-src {:draggable true
- :on-drag-start (fn [e] (.. e -dataTransfer (setData "text/note" (name (:id n)))))
- :on-click #(rf/dispatch [::events/bind-note-annotation gid (:id n)])
- :title "Drag onto a mark, or click to bind to the whole annotation"}
+(defn- mark-note-chip [ng n live on-unbind]
+ [:span.bound-note {:class (when (contains? live ng) "live")
+ :title "Jump to this passage in the script"
+ :on-click #(rf/dispatch [::events/jump-to-note ng])}
+ (when (contains? live ng) [:span.note-live "»»» "])
[:span.ann-swatch {:style {:background (:color n)}}]
- (:name n)])
+ [:span.bound-note-name (:name n)]
+ [:button.row-x {:type "button" :title "Unbind"
+ :on-click #(on-unbind ng)} "✕"]])
-(defn- note-drop
- "Drop zone rendering `bound` note-gids as removable chips; a dropped note calls
- (on-bind note-gid), a chip's ✕ calls (on-unbind note-gid)."
- [label bound nmap live on-bind on-unbind]
- [:div.note-drop {:on-drag-over #(.preventDefault %)
- :on-drop (fn [e] (.preventDefault e)
- (let [g (.. e -dataTransfer (getData "text/note"))]
- (when (seq g) (on-bind (keyword g)))))}
- (when label [:span.note-drop-label label])
- (if (seq bound)
- (for [ng bound :let [n (nmap ng)] :when n]
- ^{:key (name ng)}
- [:span.bound-note {:class (when (contains? live ng) "live")}
- (when (contains? live ng) [:span.note-live "»»» "])
- [:span.ann-swatch {:style {:background (:color n)}}]
- [:span.bound-note-name (:name n)]
- [:button.row-x {:type "button" :title "Unbind" :on-click #(on-unbind ng)} "✕"]])
- [:span.note-drop-hint "drop a note"])])
+(defn- mark-script-picker [gid mark-id notes bound]
+ (r/with-let [open? (r/atom false)]
+ (let [bound? (set bound)
+ choices (remove #(contains? bound? (:id %)) notes)
+ by-name (into {} (map (juxt :name :id)) choices)]
+ [:span.mark-script-wrap
+ [:button.mark-script {:type "button"
+ :class (when (seq bound) "has")
+ :title "Attach script note"
+ :on-click #(swap! open? not)}
+ (if (seq bound) "📄" "📄+")]
+ (when @open?
+ [:div.mark-script-pop {:on-click #(.stopPropagation %)}
+ [autocomplete {:items (mapv :name choices)
+ :placeholder "script note…"
+ :auto-focus? true
+ :on-choose (fn [choice]
+ (when-let [note-gid (by-name choice)]
+ (rf/dispatch [::events/bind-note-mark gid mark-id note-gid])
+ (reset! open? false)))}]
+ [:button.mark-script-new {:type "button"
+ :on-click (fn []
+ (reset! open? false)
+ (rf/dispatch [::events/new-note-for-mark gid mark-id]))}
+ "+ Add new"]])])))
;; drag-to-reorder marks: one live drag at a time, so a single module atom holds
;; {:src i :over j}. Deref'd in the form so the drop line follows the cursor.
@@ -1363,7 +1456,8 @@
:on-drag-end (fn [_] (reset! mark-drag nil))})
(defn annotation-form []
- (r/with-let [orig (dissoc @(rf/subscribe [::subs/draft-group]) :draft :gid)]
+ (r/with-let [orig (dissoc @(rf/subscribe [::subs/draft-group]) :draft :gid)
+ last-scrolled (atom nil)]
(let [d @(rf/subscribe [::subs/draft-group])
scene @(rf/subscribe [::subs/scene])
;; anchor the form to the draft's home context, not the live stack top:
@@ -1393,6 +1487,12 @@
(rf/dispatch [::events/save-group gid (dissoc d :draft :gid)
(when-not new? orig)])
(rf/dispatch [::events/finish-edit]))]
+ (when (and active (not= active @last-scrolled))
+ (reset! last-scrolled active)
+ (r/after-render
+ #(when-let [node (js/document.querySelector
+ (str "[data-mark-row='" active "']"))]
+ (.scrollIntoView node #js {:block "nearest" :behavior "smooth"}))))
[:form.form {:on-submit (fn [e] (.preventDefault e) (save))}
[:div.form-head (cond root? "Edit description" new? "New annotation" :else "Edit annotation")]
(when (and (not root?) (not choosing?))
@@ -1401,36 +1501,7 @@
[: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 "During playback, scroll each of this annotation's clips into view as the playhead reaches it"}
- [:input {:type "checkbox" :checked (boolean (get-in d [:meta :scroll-to]))
- :on-change #(put (assoc-in d [:meta :scroll-to] (.. % -target -checked)))}]
- [:span "Follow clips during playback"]]
- [:label.form-check {:title "Hide this annotation from the timeline lane (still usable in links)"}
- [:input {:type "checkbox" :checked (boolean (get-in d [:meta :hidden]))
- :on-change #(put (assoc-in d [:meta :hidden] (.. % -target -checked)))}]
- [:span "Hide from timeline"]]
- [:div.form-marks-label "Tags"]
- (let [tags (vec (get-in d [:meta :tags]))]
- [:div.tag-editor
- (into [:div.tag-list]
- (for [t tags]
- ^{:key t} [tag-chip t #(put (assoc-in d [:meta :tags] (vec (remove #{%} tags))))]))
- [autocomplete {:items (filterv (complement (set tags)) @(rf/subscribe [::subs/project-tags]))
- :placeholder "add tag…" :allow-new? true
- :on-choose #(put (assoc-in d [:meta :tags] (conj tags %)))}]])])
- (when-not choosing?
- [:<>
- [:div.form-marks-label "Content"]
- ^{:key gid} [content-editor gid (:content orig)]
- (if (and linking (= gid (:gid linking)))
- [link-picker {:scene scene :ctx ctx
- :on-commit #(commit-link! (select-keys % [:ref :at :kind]) (:label %))
- :on-cancel #(rf/dispatch [::events/cancel-linking])}]
- [:button.add-mark {:type "button" :on-click #(rf/dispatch [::events/start-linking gid])}
- "Insert link"])
- (when (and linking (= gid (:gid linking)))
- [:div.form-hint "Click a clip or the frame-readout to link it, or pick above."])])
+ :on-change #(put (assoc d :color (.. % -target -value)))}]]])
(when-not root?
[:<>
;; the title IS the create/associate control: type a new title to make a
@@ -1463,7 +1534,9 @@
(when (= mark-id active) "active-mark ")
(when (contains? broken mark-id) "broken ")
(when (and (:src drag) (= i (:over drag))
- (not= i (:src drag))) "drop-before"))})
+ (not= i (:src drag))) "drop-before"))
+ :data-mark-row mark-id
+ :on-click #(rf/dispatch [::events/set-active-mark mark-id])})
[:div.mark-row
[:span.mark-grip {:draggable true :title "Drag to reorder"
:on-drag-start (fn [e]
@@ -1508,43 +1581,52 @@
(goto! s true))
(rf/dispatch [::events/start-drawing gid mark-id]))}
(if (seq (:drawings mark)) "🖼" "🖼+")]
+ (when-not choosing?
+ [mark-script-picker gid mark-id notes (:notes mark)])
[:button.row-x {:type "button" :title "Remove"
:on-click #(rf/dispatch [::events/remove-mark gid i])} "✕"]]
(when-not choosing?
- [note-drop nil (:notes mark) nmap live
- #(rf/dispatch [::events/bind-note-mark gid mark-id %])
- #(rf/dispatch [::events/unbind-note-mark gid mark-id %])])]))
- (when (and (map? pt) (not (:proxy pt)))
- [:div.mark-row
- [:div.pt-chip [:span.pt-chip-name (str (clip-label scene segs (:seg pt))
- " @" (js/Math.round (:f pt)) "f")]]
- [:span.mark-arrow "→"]
- [point-picker {:scene scene :ctx ctx :class "active" :auto-focus? true
- :placeholder "click end clip..."
- :on-pick #(let [end-local (if (and (zero? (:offset %))
- (pos? (or (get-in % [:candidate :len]) 0)))
- (+ (:local %) (get-in % [:candidate :len]))
- (:local %))]
- (when-let [p (local->draft-end-point segs end-local)]
- (rf/dispatch [::events/draft-click-seg (:seg p) (:f p)])))}]])
- (when (= pt :new)
- [:div.mark-row
- [point-picker {:scene scene :ctx ctx :class "active"
- :placeholder "click a clip..."
- :on-pick #(when-let [p (local->draft-point segs (:local %))]
- (rf/dispatch [::events/draft-click-seg (:seg p) (:f p)]))}]])
- [:div.form-hint "Drag across the timeline to select a range (click to move the playhead)."]
- (when-not choosing?
+ (when (seq (:notes mark))
+ (into [:div.mark-note-list]
+ (for [ng (:notes mark) :let [n (nmap ng)] :when n]
+ ^{:key (name ng)}
+ [mark-note-chip ng n live #(rf/dispatch [::events/unbind-note-mark gid mark-id %])]))))]))
+ (when (or (= pt :new) (and (map? pt) (not (:proxy pt))))
+ [pending-mark-block {:scene scene :ctx ctx :segs segs :pt pt :choosing? choosing?}])
+ [:div.form-hint "Drag across the timeline to select a range (click to move the playhead)."]])
+ (when-not choosing?
+ [:<>
+ (when-not root?
[:<>
- [:div.form-marks-label "Script notes"]
- [note-drop "Whole annotation" (:notes d) nmap live
- #(rf/dispatch [::events/bind-note-annotation gid %])
- #(rf/dispatch [::events/unbind-note-annotation gid %])]
- (if (seq notes)
- [:div.note-source
- (for [n notes] ^{:key (name (:id n))} [note-src-chip gid n])]
- [:div.form-hint "No script notes yet — create them in the Script tab."])
- [:div.form-hint "Drag a note onto a mark (or the whole-annotation target). It shows »»» while the playhead is in range."]])])
+ [:div.form-marks-label "Tags"]
+ (let [tags (vec (get-in d [:meta :tags]))]
+ [:div.tag-editor
+ (into [:div.tag-list]
+ (for [t tags]
+ ^{:key t} [tag-chip t #(put (assoc-in d [:meta :tags] (vec (remove #{%} tags))))]))
+ [autocomplete {:items (filterv (complement (set tags)) @(rf/subscribe [::subs/project-tags]))
+ :placeholder "add tag…" :allow-new? true
+ :on-choose #(put (assoc-in d [:meta :tags] (conj tags %)))}]])])
+ [:div.form-marks-label "Content"]
+ ^{:key gid} [content-editor gid (:content orig)]
+ (if (and linking (= gid (:gid linking)))
+ [link-picker {:scene scene :ctx ctx
+ :on-commit #(commit-link! (select-keys % [:ref :at :kind]) (:label %))
+ :on-cancel #(rf/dispatch [::events/cancel-linking])}]
+ [:button.add-mark {:type "button" :on-click #(rf/dispatch [::events/start-linking gid])}
+ "Insert link"])
+ (when (and linking (= gid (:gid linking)))
+ [:div.form-hint "Click a clip or the frame-readout to link it, or pick above."])
+ (when-not root?
+ [:<>
+ [:label.form-check {:title "During playback, scroll each of this annotation's clips into view as the playhead reaches it"}
+ [:input {:type "checkbox" :checked (boolean (get-in d [:meta :scroll-to]))
+ :on-change #(put (assoc-in d [:meta :scroll-to] (.. % -target -checked)))}]
+ [:span "Follow clips during playback"]]
+ [:label.form-check {:title "Hide this annotation from the timeline lane (still usable in links)"}
+ [:input {:type "checkbox" :checked (boolean (get-in d [:meta :hidden]))
+ :on-change #(put (assoc-in d [:meta :hidden] (.. % -target -checked)))}]
+ [:span "Hide from timeline"]]])])
[:div.form-actions
(when-not choosing?
[:button.save {:type "submit" :disabled (not valid?)} "Save"])
@@ -1875,6 +1957,7 @@
live @(rf/subscribe [::subs/active-note-set])
hidden @(rf/subscribe [::subs/hidden-notes])
authed? @(rf/subscribe [::subs/authed?])
+ note-target @(rf/subscribe [::subs/note-target])
script-err @(rf/subscribe [::subs/script-error])
scale (or @zoom 1)
;; notes whose highlights are drawn on the page (eye toggle off)
@@ -1916,6 +1999,13 @@
(reset! scrolled live-note)
(scroll-to-region! rid))))))
[:div.script-pane
+ (when note-target
+ [:div.script-return
+ [:button.hl-cancel {:type "button"
+ :title "Back to annotation"
+ :on-click #(rf/dispatch [::events/cancel-note-target])}
+ "‹ Back"]
+ [:span.script-return-text "Highlight script text to make a note for this mark."]])
(if url
[note-rail notes active hidden authed?]
[:div.script-hint
@@ -1947,12 +2037,15 @@
[:button.sel-add {:style {:left (:bx @sel) :top (:by @sel)}
:on-mouse-down #(.preventDefault %) ; keep the selection alive
:on-click (fn []
- (if active
+ (if (and active (not note-target))
(rf/dispatch [::events/add-region-text active region])
(rf/dispatch [::events/highlight-into-new-note region]))
(.removeAllRanges (.getSelection js/window))
(reset! sel nil))}
- (if active (str "+ Highlight → " (:name note)) "+ New note from selection")]))]
+ (cond
+ note-target "+ Create note for mark"
+ active (str "+ Highlight → " (:name note))
+ :else "+ New note from selection")]))]
(when url [:div.script-empty "Loading script…"]))]
(when (and url active note)
[note-editor note active scroll-to-region!])]
@@ -1967,12 +2060,15 @@
(let [pane @(rf/subscribe [::subs/pane])
draft @(rf/subscribe [::subs/draft-group])
draft? (some? draft)
+ note-target @(rf/subscribe [::subs/note-target])
save-err @(rf/subscribe [::subs/save-error])]
[:div.pane-wrap
(when save-err [:div.err {:style {:padding "4px 8px"}} "△ " save-err])
[:div.pane-tabs
[:button {:class (when (= pane :annotations) "active")
- :on-click #(rf/dispatch [::events/set-pane :annotations])} "Annotations"]
+ :on-click #(rf/dispatch [(if note-target
+ ::events/cancel-note-target
+ ::events/set-pane) :annotations])} "Annotations"]
[:button {:class (when (= pane :script) "active")
:on-click #(rf/dispatch [::events/set-pane :script])} "Script"]]
[:div.pane-body