Skip to content

feat(ui): timeline polish — filmstrips + draggable playhead#39

Merged
yultyyev merged 3 commits into
feat/ui-timelinefrom
feat/timeline-filmstrip
Jun 19, 2026
Merged

feat(ui): timeline polish — filmstrips + draggable playhead#39
yultyyev merged 3 commits into
feat/ui-timelinefrom
feat/timeline-filmstrip

Conversation

@yultyyev

@yultyyev yultyyev commented Jun 19, 2026

Copy link
Copy Markdown
Collaborator

Timeline polish: filmstrips + a draggable playhead

Two timeline-UX improvements heading into v0.3.0. Media clips read like a real NLE, and the playhead is finally interactive.

Targets feat/ui-timeline (#36's branch), not main — folds into #36 so it ships inside v0.3.0 rather than a separate publish (same pattern as #37/#38).

1 — Frame-thumbnail filmstrips on clips

Media clips were flat gradient blocks. Now each shows a row of source-frame thumbnails across its trimmed window.

  • GET /api/media/thumb?mediaId=&t=&h= — one small still from a source media file (fast input-seek), cached per (mediaId, t, h) under the workspace, generated to a temp file then atomically renamed so concurrent filmstrip requests never serve a half-written frame. Unknown media → 404; a non-thumbnailable source → 422 (the clip falls back to its gradient).
  • DocTimeline lays out one thumb per ~72px slot (≤16), lazy-loaded; a scrim keeps the label/duration legible; text/color clips keep their solid block.

2 — Draggable playhead + ruler scrub rail

The playhead was a non-interactive line (pointer-events: none); the only scrub control was a disconnected 220px slider in the header — nothing to grab on the timeline.

  • The playhead is now a draggable handle (a thin line + a downward-triangle marker): pointer-down + drag scrubs to the cursor anywhere along the timeline, via setPointerCapture and a clientX → seconds map that accounts for the gutter and horizontal scroll.
  • A transparent native range over the ruler is the scrub surface: click/drag to position, arrow keys move one frame each — the accessible, keyboard-friendly control, replacing the removed header slider.
  • Also deletes a stale duplicate .doc-tl-playhead::before that was offsetting the line with a stray border wedge.

Verification

pnpm type-check ✓ · 548 tests ✓ (new: buildThumbnailArgs arg shape) · pnpm lint exit 0 · pnpm build ✓.

  • Filmstrip — live e2e 8/8 (distinct frames, cached, concurrent-safe, 404 on unknown); DOM renders the thumbnails + scrim.
  • Scrub — live browser check: dispatching the rail to 4.5s moved the playhead to exactly 533px (128 + 4.5×90) with the readout at 4.50s; a clientX at the 3s mark maps to 3.00s; the playhead computes pointer-events: auto / ew-resize; after the fix the line is centered (left: 8px of the 16px strip, no stray border) and the marker is a border-triangle, not a rotated square. An adversarial review pass independently caught the duplicate-::before defect; fixed.

🤖 Generated with Claude Code

yultyyev added 3 commits June 18, 2026 17:13
GET /api/media/thumb?mediaId=&t=&h= extracts one small still from a SOURCE
media file (not the composited timeline) via a fast input-seek frame grab
(buildThumbnailArgs). Each thumb is keyed by (mediaId, t, h) and cached under
the workspace, generated to a temp path then atomically renamed so concurrent
filmstrip requests never serve a half-written file. Unknown media → 404; a
non-thumbnailable source (e.g. audio-only) → 422, so a clip just falls back to
its gradient block.
Media clips now show a row of source-frame thumbnails across their trimmed
window — a filmstrip — instead of a flat gradient block. The clip samples one
/api/media/thumb still per ~72px slot (capped at 16), at the middle of each
slot; the gradient shows underneath until the frames load, a scrim keeps the
label/duration legible, and text/color clips keep their solid block. Thumbs are
lazy-loaded so long timelines don't fetch off-screen frames.
The playhead was a non-interactive line (pointer-events:none) and the only
scrub control was a disconnected 220px slider in the header — there was nothing
to grab on the timeline. Now:

- The playhead is a draggable handle: pointer-down + drag scrubs to the cursor
  anywhere along the timeline (setPointerCapture + a clientX→sec map that
  accounts for the gutter and horizontal scroll). It renders as the conventional
  thin line + a downward-triangle marker.
- A transparent native range over the ruler is the scrub surface: click or drag
  to position, arrow keys (one frame each) move the playhead — the accessible,
  keyboard-friendly control that replaces the removed header slider.

Also removes a stale duplicate .doc-tl-playhead::before that survived an earlier
refactor and was offsetting the line with a stray border wedge.
@yultyyev yultyyev changed the title feat(ui): frame-thumbnail filmstrips on timeline clips feat(ui): timeline polish — filmstrips + draggable playhead Jun 19, 2026
@yultyyev yultyyev merged commit d51382d into feat/ui-timeline Jun 19, 2026
1 check passed
@github-actions

Copy link
Copy Markdown

🎉 This PR is included in version 0.3.0 🎉

The release is available on:

Your semantic-release bot 📦🚀

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant