Transport row is now |◀ ▶/⏸ ▶|. step-frame! pauses, rounds the video
clock to the nearest frame and seeks ±1; the rAF clock reports the new
position back into :playhead, so the red line follows (same single
seek-only path as jump/scrub).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
seek-to-frame! and the rAF clock both reach the <video> through the
shared video-el atom, which was populated by an inline :ref closure —
recreated every render and callable with nil by React, so video-el ended
up nil. That made set! currentTime (jumps + scrubs) and the clock's
currentTime read both silent no-ops: the red line moved (separate
:playhead dispatch) but the picture never seeked or followed.
Capture the element with a stable ref that ignores nil, and collapse the
clock to a single source of truth: one always-on rAF copies currentTime
into :playhead, and jump/scrub only seek (no optimistic :playhead write),
so the line and the picture can't disagree. Replaces the old
push!/tick/stop + on-play/on-pause/on-seeked bookkeeping.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The annotation lane above the tracks now collapses non-overlapping
annotations onto a single row. Each annotation is assigned a lane by
greedy first-fit in definition order (:created): it takes the topmost
lane it doesn't time-overlap, so overlapping annotations stack with the
earlier-defined one on top and the newer beneath.
- annotations carry :created (new ones stamp Date.now; seed has explicit
values; old saved data defaults to 0 = oldest)
- pane order: by start, then longest-first so a section leads a tie
- seed data reworked to overlap (Act I / rally / baseline / glance / net,
plus an overlapping nested child) so the packing is visible; one-time
localStorage reset bumped so it lands
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Each annotation can be expanded into its own nested timeline. Reuses the
dormant :timeline-stack: annotations carry a :parent (:root = top level),
expanding pushes the annotation id, and only annotations whose :parent
matches the stack top are surfaced — so nested annotations stay invisible
on the main timeline.
- crop: the expanded annotation's own spans define a time window + the set
of involved tracks; non-involved tracks collapse to height 0 (CSS-
transitioned, so tracks animate away and survivors slide up), and the
view offsets/crops to the window
- breadcrumb bar + back button (shown only when nested) fully restore the
main timeline
- new annotations attach to the current context; the expand button shows a
child count and delete removes the whole subtree
- seed data (Opening rally + 2 children, Net-cam beat) for testing, plus a
one-time localStorage reset so it shows
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>