Commit Graph
706 Commits
Author SHA1 Message Date
Miguel Angel Simon Sierra 0fe38e8cc8 refactor(studio): single-source timeline selection id-resolution
The DOM-selection to timeline sync routes through the canonical resolveTimelineIdForSelection
(source-file, ancestor, active-comp fallback) instead of a narrow domId/id match that
mismatched sub-composition clips.

The preview-sync equality check compares selection as sets both ways and includes the
anchor, so duplicate resolutions no longer mask an unsynced member.
2026-07-09 16:54:34 -04:00
Miguel Angel Simon Sierra 04ddd411ec fix(studio): harden group timeline edits (capabilities, rollback, snapping, marquee)
Group move/resize rejects the gesture when any selected member forbids the op (e.g. a
locked clip), so a group never edits a clip that individually cannot move, and a
persist failure now propagates so the optimistic preview rolls back.

Snapping excludes every moving member, not just the grabbed clip. The marquee hit-test
uses the real pixels-per-second (was floored at 1, wrong below 1x zoom), and a
sub-threshold marquee click scrubs the playhead like a plain lane click.
2026-07-09 16:54:34 -04:00
Miguel Angel Simon Sierra 3c6c1d3f27 refactor(studio): single-source timeline id-resolution and resize-clamp math
Extract resolveTimelineIdForSelection so DOM-to-timeline id mapping lives in one place
with a single sourceFile / activeCompPath / index.html fallback, fixing a sub-composition
selection that previously diverged between callers.

Extract shared start-trim delta helpers used by both single-clip and group resize, and
remove the never-called refreshDomEditGroupSelectionsFromPreview.
2026-07-09 16:54:34 -04:00
Miguel Angel Simon Sierra 95ded6c026 fix(studio): draw the timeline marquee in the theme accent color
The marquee and range overlays used a hardcoded blue; they now use the timeline
theme accent for the border with a translucent accent fill.
2026-07-09 16:54:34 -04:00
Miguel Angel Simon Sierra fa3848a33d fix(studio): keep the timeline multi-selection through a group edit
setSelectedElementId no longer resets the selection set when re-selecting an element
that is already a member: DOM-to-selection sync echoes fire on every pointer move during
a group drag and were collapsing the set to the grabbed clip.

Also drops the duplicate clearSelectedElementIds action in favor of clearSelection,
which the marquee now uses to clear on an empty drag.
2026-07-09 16:54:34 -04:00
Miguel Angel Simon Sierra 36413da7f6 feat(studio): resize selected timeline clips together
Apply handle deltas through a group resize session that snapshots selected members.

Clamp one shared delta against zero start and min duration before preview and commit.

Playback-start changes are calculated per member with the existing trim formula.
2026-07-09 16:54:34 -04:00
Miguel Angel Simon Sierra 5d27af30b2 feat(studio): move selected timeline clips together
Fan selected body drags through a group move session that snapshots all selected members.

The grabbed clip snap result is applied first, then one shared delta is clamped.

Live preview patches every moved member and release persists once through batch timing.
2026-07-09 16:54:34 -04:00
Miguel Angel Simon Sierra 45d09047b7 feat(studio): add timeline marquee selection 2026-07-09 16:54:34 -04:00
Miguel Angel Simon Sierra 1cf601202d feat(studio): batch timeline timing commits
Persist group timing edits through one coalesced write per source file.

Keep batch timing queued behind any z-index commit for the same gesture.
2026-07-09 16:54:34 -04:00
Miguel Angel Simon Sierra 1d858f004d feat(studio): highlight timeline selection sets
Render selected styling from selectedElementIds in the timeline.

Sync the set into preview group selection boxes without collapsing the anchor.
2026-07-09 16:54:34 -04:00
Miguel Angel Simon Sierra cca31feffe feat(studio): activate selected element set state
Use selectedElementIds as the live selection set with selectedElementId as anchor.

Keep single-click selection as a one-element set and clear both fields together.
2026-07-09 16:54:34 -04:00
Miguel Angel Simon Sierra 6080f5ad3e fix(studio): propagate z-index reorder save failures and drop dead targetTrack
handleDomZIndexReorderCommit no longer swallows per-entry save failures: it settles every
patch, and on any rejection rolls back the eager DOM z-index/position and the optimistic
store zIndex before rejecting, so a failed save cannot leave the UI showing a stacking order
that never persisted or let an ordered-after timing write proceed.

Also removes the dead targetTrack parameter threaded through the timeline edit helpers;
vertical placement is owned by the z-index intent.
2026-07-09 16:53:26 -04:00
Miguel Angel Simon Sierra 51dd8a0753 fix(studio): order the timing write after the z-index commit on a diagonal drag
A drag that both moved a clip in time and restacked it fired two writers on
the same file via separate queues (a targeted z-index patch and a full-file
timing overwrite), so the overwrite could clobber the just-persisted z-index
and the restack silently vanished after reload. The move now awaits the
z-index commit before persisting timing, giving the file one ordered writer
per gesture. Adds a regression test that gates the commit and asserts the
timing write waits.
2026-07-09 00:04:35 -04:00
Miguel Angel Simon Sierra 6f0bf75b69 feat(studio): fill the ruler with ticks across the full timeline width
The ruler now generates ticks across the visible width instead of stopping
at the composition end, so a zoomed-out ruler stays labelled to the right
edge. The tick interval stays pps-driven, so spacing is unchanged. Logic
lives in a new generateVisibleTicks helper to keep Timeline.tsx under the
600-line cap.
2026-07-08 23:46:30 -04:00
Miguel Angel Simon Sierra eb31e86d4f feat(studio): lanes and ruler span the full timeline width at any zoom
When the composition is narrower than the panel (zoomed out) the track
lanes, ruler bar, and context header now fill the whole timeline instead
of stopping at the content width and leaving a flat dark void. Content
stays in composition coordinates, so ticks, clips, and the playhead are
unaffected.
2026-07-08 23:46:29 -04:00
Miguel Angel Simon Sierra ffd513130b fix(studio): keep a remounted clip marked active when it stays at the playhead
The structural refresh now force-syncs data-active on freshly queried clip
nodes instead of diffing. A clip that changes lanes on a reorder remounts as
a new DOM node; if it stayed under the playhead the diff skipped it and it
rendered as inactive.
2026-07-08 23:46:29 -04:00
Miguel Angel Simon Sierra eb6a311d82 fix(studio): make a timeline lane a z-band so a vertical restack moves the row
Clips share a lane only when they carry the same z-index and don't overlap
in time. Previously a non-overlapping clip packed into the first
time-compatible row regardless of z, so restacking changed the z-index but
the clip's row never followed.
2026-07-08 23:46:29 -04:00
Miguel Angel Simon Sierra db6ccb8082 fix(studio): stop horizontal drag from cancelling a vertical restack
Lane/overlap resolution now uses the clip's authored time span instead
of the live dragged start. A diagonal drag that drifts the clip out of
overlap no longer flips the placement from restack to lane-join, so the
two axes never fight.
2026-07-08 23:46:29 -04:00
Miguel Angel Simon Sierra ce0ddbccf8 fix(studio): keep Timeline under the 600-line cap and un-export postRootDurationToPreview
CI file-size check and fallow audit caught two issues the --no-verify
commits skipped: Timeline.tsx sat at 602 (max 600) and
postRootDurationToPreview was an unused export (only called in-file).
Trim a comment to hit 600; drop the export.
2026-07-08 23:46:28 -04:00
Miguel Angel Simon Sierra 9f6c20e482 perf(studio): grow composition duration live on extend, no preview remount
Extending a clip past the video end used to force the server-fallback
path that fully remounts the preview iframe (the SDK fast path can't
express the root composition's data-duration, and the runtime bakes+drops
data-duration at load so it can't be patched live). On a large comp that
remount is a visible hitch.

Add a runtime control-bridge action set-root-duration -> clock.setDuration,
so the studio can grow the transport length in place. On an extend the
studio now posts it (and patches the clip's own timing live) instead of
reloading; it only reloads when a GSAP source rewrite actually happened
(the gsap-mutation endpoints now report a mutated flag). Non-animated
extends — the common case — commit as fast as a normal edit.

Verified: bridge dispatch + studio no-reload/post-message paths unit-
tested; core/studio/studio-server typecheck + suites green; the built
runtime artifact carries the handler; E2E confirms the extend no longer
remounts the preview and still persists.
2026-07-08 23:46:28 -04:00
Miguel Angel Simon Sierra 580676bbf4 fix(studio): freeze timeline zoom during extend-drag so clips don't jump
Dragging a clip past the video end fed back on itself: the growing preview
length shrank the fit-to-width zoom, which remapped the pointer, moved the
clip, and grew the length again (visible jumping). Split the committed
basis duration (drives zoom) from the displayed effective duration (adds
the live preview for ruler + width). Zoom holds fixed through the gesture
and the extra length scrolls; it re-fits once on drop. Duration math
extracted to pure tested helpers in timelineLayout.
2026-07-08 23:46:28 -04:00
Miguel Angel Simon Sierra d7e7fca8ec feat(studio): magnetic snapping to clip edges, playhead, and bounds
Timeline moves and resizes previously snapped only to the beat grid; they
now also snap the dragged edge to other clips' start/end, the playhead,
and the composition bounds, whichever is nearest within the existing
threshold. Beats fold in as one target kind, so beat snapping is
unchanged. Snap logic lives in a new pure timelineSnapTargets module with
unit tests; a snapped edge shows a guide line (beat highlight, else a thin
accent line) for both drag and resize.
2026-07-08 23:46:27 -04:00
Miguel Angel Simon Sierra 6b4870246c fix(studio): dropping an overlapping clip on a lane restacks it, not a no-op
A vertical drag onto a lane whose clips overlap the dragged clip in time
used to resolve to the "nearest valid lane", which included the clip's own
lane — so a small drag snapped straight back and felt like the editor
refused the move. Overlapping clips can't share a row (a row is 1-D in
time), but the drag should still restack, never reject.

Now a conflicting onto-drop converts to an edge insertion adjacent to the
target by drag direction (up -> above, down -> below): the clip lands on
its own lane just in front of / behind the target, with its time
unchanged. Non-overlapping drops still join the lane. Deletes the
nearest-valid-placement search (~90 lines) this replaces.
2026-07-08 23:46:27 -04:00
Miguel Angel Simon Sierra 5e3ca6ae63 feat(studio): drag a clip past the video end to extend its duration
Dragging a timeline clip (or its right resize edge) past the end of the
video now extends the composition duration on drop, instead of clamping
the clip at the current end. Extend-only and undoable.

- Relax the move/resize horizontal clamps that pinned a clip's end at the
  current duration; effectiveDuration now folds in the active drag/resize
  preview so the ruler and track width grow live as you drag past the end.
- On drop, extend the root composition data-duration (and the store) when
  the clip's new end exceeds it, via a shared extendRootDurationInSource
  helper extracted from the block installer (now the single owner of that
  logic). An extending edit routes through the server persist path since
  the SDK setTiming op can't express the root composition's own duration.
2026-07-08 23:46:27 -04:00
Miguel Angel Simon Sierra ed8bf475d3 feat(studio): lane-model timeline (non-overlapping clips share a row)
Rework the timeline row model from one-row-per-stacking-layer to NLE
"lanes": a row holds a time-sequence of non-overlapping clips, ordered
top = front. Stacking only matters between clips that overlap in time.

- Lane packing: per stacking context, sort clips by effective z-index
  desc (DOM order tiebreak) and greedily pack each onto the first lane
  whose members don't overlap it in time; else open a new lane. Lane's
  representative z is the max member z. Audio stays one clip per lane.
- Lane-aware drag: dropping onto a lane joins it (clip takes the lane's
  z) when there's no time-conflict; a conflicting drop is rejected and
  the preview snaps to the nearest valid lane or a new-lane insertion.
  Between/above/below still create a new lane at that stacking level.
- Overlap checks use the drag-preview start/duration, so conflict is
  judged by where the clip lands, not where it started.

Only explicit drags write z-index; authored z is preserved and
data-track-index is never rewritten.
2026-07-08 23:46:26 -04:00
Miguel Angel Simon Sierra bc5fc410ee feat(studio): timeline layer UI — context headers, audio lanes, drop affordances
L4 polish for the stacking-layer timeline:
- Sub-composition layers group under a slim "Inside: <comp>" header with a green
  accent, so it reads that restacking there is scoped to that context.
- Audio clips render in a distinct bottom lane: separator border, muted row bg,
  music glyph in the gutter, beat strip — clearly not part of the z-order stack.
- During a vertical drag, a drop affordance shows join-vs-insert: an "onto"
  target highlights the row (join that layer / same z); "between"/"above"/
  "below" shows an insertion line (new layer here).

New pure helper timelineDropIndicator.ts (+ test) maps placement -> indicator;
row-group rendering extracted into TimelineLayerGroupHeader / TimelineLayerGutter
/ TimelineDropInsertionLine / TimelineDragGhost to keep files under the 600 cap.
2026-07-08 23:46:26 -04:00
Miguel Angel Simon Sierra 82fe779bd8 fix(studio): sub-composition child clips restack via self-contained intent
applyTimelineStackingReorder resolved each z-index change by looking the clip up
in the top-level timelineElements list, but sub-composition children live only
in the expanded list, so the lookup missed them and the reorder silently bailed.
Carry the element's locator (domId/selector/sourceFile) on each z-index change
so the commit resolves the live element directly from the preview DOM. Verified
E2E: dragging a clip inside a sub-composition restacks it and patches the
sub-comp source, while the parent composition is untouched.

Adds timelineEditingHelpers.test.ts (locator-based commit + audio no-op).
2026-07-08 23:46:26 -04:00
Miguel Angel Simon Sierra eef500c890 fix(studio): realm-safe isHTMLElement so timeline z-index commits land
applyTimelineStackingReorder resolves the live clip from the preview IFRAME,
then gated it with `element instanceof HTMLElement` against the MAIN window's
constructor. Cross-realm instanceof is always false, so every timeline z-index
commit silently bailed ("element not live in iframe") — the drag resolved the
right z but never wrote it. Use the element's own-realm HTMLElement constructor
(matching timelineDOM.ts). Verified end-to-end: dragging a card down now lowers
its z-index and reorders the row, leaving sibling z-indexes untouched.

Unit tests missed this because the happy-dom test iframe shares a realm; caught
via a real-browser Puppeteer E2E drag.
2026-07-08 23:46:25 -04:00
Miguel Angel Simon Sierra e5ed512529 feat(studio): timeline track = stacking layer (NLE-style layering)
Re-architect the timeline row model from data-track-index rows to stacking
layers. Rows represent stacking layers per context: explicit-z clips merge onto
one track when they share a z and don't overlap in time; auto-z clips stay one
row each (DOM order); audio is pulled into its own bottom lanes. Rows keyed by a
stable layer id, not data-track-index.

Vertical drag always writes z-index, never track: drop onto a layer joins it
(same z), between layers interpolates a new z, past the ends creates a new
front/back layer. data-track-index is never rewritten; #958 holds. Adds
hasExplicitZIndex capture (computed z != auto).

L1: hasExplicitZIndex on the element model
L2: buildStackingTimelineLayers (layer-based rows)
L3: layer-aware vertical drag (join / interpolate / new-extreme)
2026-07-08 23:46:25 -04:00
Miguel Angel Simon Sierra 03f8089e52 fix(studio): capture effective (computed) z-index for timeline ordering
createTimelineElementFromManifestClip used `clip.zIndex ?? computed`, but the
runtime reports inline-only z-index (0 for CSS-rule authored z-index), and
`0 ?? x` keeps the 0 — so every CSS-styled clip collapsed to a z=0 tie. The
timeline then ordered rows by DOM position instead of true stacking, and the
first vertical drag renumbered the whole tie group, clobbering the author's
z-index. Prefer the effective computed read from the live element (the same
read the reorder commit uses); fall back to the runtime value only when the
element isn't live.
2026-07-08 23:46:25 -04:00
Miguel Angel Simon Sierra 5ce362299c refactor(studio): single-source the timeline stacking key + guard audio reorder
The element stacking key (element.key ?? id) was recomputed in four places
(reorder-intent generation, row ordering, the commit-time sibling lookup via a
threaded keyOf param, and resolveTimelineMove). Any drift would silently break
the sibling lookup and no-op the reorder. Route all of them through the existing
getTimelineElementIdentity owner, share one toStackingOrderItem mapper between
row ordering and reorder intent, and drop the keyOf parameter.

Also enforce the audio side-effect invariant in the single mutation owner
(applyTimelineStackingReorder): dragging an audio clip has no visual layer to
restack, so it never writes z-index. Covered by a new hook test.
2026-07-08 23:46:24 -04:00
Miguel Angel Simon Sierra dd980697a2 feat(studio): unify timeline vertical reorder with z-index stacking
Timeline rows now order by scoped stacking (z-index per stacking context)
instead of data-track-index, and dragging a clip up/down commits a targeted
z-index change through the same shared path the layers panel uses. Both panels
stay consistent and moving a clip actually changes front/back. data-track-index
is demoted to time-overlap layout only; no bulk z-index injection (#958 intact).

Also restores beat-snapping on keyframe retiming (re-wires snapKeyframePctToBeat,
orphaned when keyframe dragging was removed) which surfaced while unifying the
model. Extracts pure track-ordering logic to timelineTrackOrder.ts, the stacking
reorder commit + deleteSelectedKeyframes to timelineEditingHelpers.ts, to keep
StudioApp / the timeline hook / Timeline under the studio 600-LOC cap.

U3: scoped stacking row order in the timeline
U4: vertical drag commits z-index via the shared reorder commit
U8: restore keyframe beat-snap on retime
2026-07-08 23:46:24 -04:00
Miguel Angel Simon Sierra 070ee91694 feat(studio): expose resolved z-index and stacking context on timeline clips 2026-07-08 23:46:24 -04:00
Miguel Angel Simon Sierra dae26e72b2 refactor(studio): extract shared layerOrdering module from LayersPanel 2026-07-08 23:46:24 -04:00
Miguel Ángel 0ac000181e feat(studio): storyboard empty state with agent copy-prompt and skeleton preview (#2091)
The storyboard view's empty state was a single "no storyboard yet" line. Add a
copy-to-clipboard prompt box (a ready-to-paste handoff prompt carrying the
canonical STORYBOARD.md frontmatter + per-frame format) so users can hand it to
their coding agent, plus a faded skeleton of the contact-sheet grid so landing
on an empty board previews what a filled one looks like instead of a dead end.
2026-07-08 22:32:40 -04:00
Miguel Ángel 623f91d6a1 feat(studio): always-on crop with reposition handle, drop crop mode (#2090)
Crop is now part of the element selection instead of a separate mode. Selecting
a croppable element shows edge handles just outside each side and, once cropped,
the full content with the cropped-away area dimmed plus a center reposition
handle to pan the crop window. Dragging the body moves the element, edge handles
crop, the center handle pans; corners stay free for the resize handle. Removes
the crop-mode toggle (toolbar + property-panel buttons), the cropMode/
cropAvailable player-store state, and the double-click-to-crop gesture. The
clip-path inset model is unchanged.
2026-07-08 22:24:26 -04:00
Vance Ingalls 1e8dd29815 chore: release v0.7.44 2026-07-08 16:21:05 -07:00
Vance Ingalls f8f945d42e chore: release v0.7.43 2026-07-08 15:51:39 -07:00
Vance Ingalls 8854bad8f9 fix(engine,cli): resolve drawElement to a Chrome build that actually has it
canvas.drawElementImage is an unlaunched Dev/Canary-only Blink feature
(~151+). The CLI's pinned CHROME_VERSION fallback was still 131.0.6778.85 —
a puppeteer 24→25.2.1 bump that pinned it to Chrome Dev 151.0.7912.0 was
written on 2026-06-29 but never merged (orphaned local commit, no PR). Any
render on that pin, or on the shared puppeteer-cache binary, or on system
Chrome (Stable, no drawElementImage at all) got a canvas.getContext("2d")
missing the method and crashed mid-capture with "ctx.drawElementImage is
not a function" instead of falling back (HF#2060).

Three changes:
- Bump puppeteer/puppeteer-core to ^25.2.1 across every package that
  depends on it, and CHROME_VERSION to 152.0.7928.2 (today's Dev channel;
  confirmed via direct probe to implement drawElementImage, unlike 131).
- `ensureBrowser({ preferManagedChrome: true })`, always used by `render`:
  resolve straight to our pinned/cached build, skipping both the shared
  puppeteer-cache preference and system Chrome. Rendering shouldn't depend
  on whatever arbitrary Chrome a machine happens to have — that's exactly
  how this regressed (any Mac with Chrome.app installed bypassed the CLI's
  pin entirely).
- A runtime capability probe in the engine, right before any other
  drawElement work: if `drawElementImage` isn't a function on the injected
  canvas, route to the existing screenshot-fallback gate instead of
  crashing. This is the real backstop — it protects every resolution path
  (env override, stale cache entry, a future Chrome regression), not just
  the ones `preferManagedChrome` reaches.

Verified end-to-end: rendering against chrome-headless-shell 131 (confirmed
to lack drawElementImage) now falls back cleanly and produces a valid MP4
instead of crashing; rendering against a capable build still engages
drawElement normally. 922 engine tests + 1373 CLI tests pass.

Fixes #2060.
2026-07-08 14:14:28 -07:00
Vance IngallsandClaude Fable 5 f6cd711bf0 chore: release v0.7.42
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-07 16:04:26 -07:00
Miguel Ángel 60463d0bd5 chore: release v0.7.41 2026-07-07 20:30:24 +00:00
Miguel Ángel 2bd9bb6f69 chore: release v0.7.40 (#2024) 2026-07-07 12:30:13 -04:00
Miguel Ángel 574da2b215 fix(studio): off-canvas indicators track live layout instead of going stale (#2018)
The dashed indicators for elements outside the canvas stayed pinned at an element's
old position after a move or a seek: they were recomputed by an effect keyed on
compRect/activeCompositionPath, neither of which changes on an in-place soft-reload
edit or a playhead seek.

They now recompute via a MutationObserver on the preview document (coalesced to one
recompute per frame), so they follow the element live. Crop-hugging is preserved.
Adds a regression test that mutates an element in place and asserts the indicator moves.
2026-07-07 04:39:46 -04:00
Miguel Ángel 037266e72b feat(studio): timeline revamp with active-clip highlighting and hide controls (#2017)
Timeline UI
- Highlight clips visible at the playhead in the primary color; others share one neutral color
- Minimalist rounded clips, single-color track rows, no gutter icons or superscript labels
- Per-track eye toggle and a per-element hide button in the design panel
- Ruler zoom fixes: sub-second tick intervals and correct label formatting at high zoom
- Sticky gutter so track controls stay visible while scrolling

WYSIWYG visibility (data-hidden)
- Runtime honors data-hidden (display:none), so hiding affects the render, not just the preview
- HTML stays the source of truth; hide state persists and round-trips on reload

Split several studio files to stay under the 600-line cap; pure relocations, no behavior change.
2026-07-07 04:26:56 -04:00
Miguel Ángel 5d59835446 fix(studio): static-position drag no longer freezes an element beside an animated rotation (#2016)
## What

Brief description of the change.

## Why

Why is this change needed?

## How

How was this implemented? Any notable design decisions?

## Test plan

How was this tested?

- [ ] Unit tests added/updated
- [ ] Manual testing performed
- [ ] Documentation updated (if applicable)
2026-07-07 03:45:17 -04:00
Vance Ingalls 229e88eac5 chore: release v0.7.39 2026-07-06 22:48:42 -07:00
ukimsanov 8f022d06ec fix(studio): remove selection overlay fill 2026-07-06 21:02:36 -07:00
Miguel Angel Simon Sierra 676f461f6a feat(studio): non-destructive crop + cross-project asset view 2026-07-06 23:40:32 -04:00
Vance Ingalls d8d3a93b0d chore: release v0.7.38 2026-07-06 17:47:34 -07:00
Vance Ingalls 89e36da13d fix(studio): shell UX — data-loss guards, error surfacing, dialog contracts, toasts (#1964) 2026-07-06 16:56:06 -07:00