Files
Miguel Ángel a8d44b2596 fix(studio): keep clips on their row through group, ungroup, and composition recovery (#4068)
* fix(studio): keep a split clip on its original track row

Splitting an unauthored clip cloned the DOM node without stamping
data-track-index, so the runtime's positional-index fallback renumbered
it onto a different track on insertion. Both halves now pin to the
element's current resolved track.

Also adds A, matching CapCut, to return from the razor tool to select
while it is armed.

* fix(studio): stop the in-point seek from also firing when A exits the razor tool

Both A bindings register on window in capture phase; the razor's listener
mounts after the playback one, so relying on preventDefault ordering let
the seek fire before the tool switch. Playback now checks activeTool
directly instead.

* fix(studio): stamp the resolved track through group, ungroup, and composition recovery

Same defect class as the split fix: an unauthored clip's track falls back to
its position in document order, and any DOM insertion elsewhere can shift
that position. Group and ungroup move elements into and out of a wrapper;
composition recovery clones a missing element back in. All three now stamp
each element's already-resolved track explicitly, the same way split does,
so none of them can drop a clip onto a different row.

Also closes a gap in the earlier "A" hotkey fix: the razor-armed guard was
unconditionally swallowing Shift+A and Alt+A, which should still seek to the
in-point since only plain A exits the razor tool.

* refactor(studio): reuse the existing patch-target and selection-match types

Simplify pass on the track-stamping fix: resolveGroupChildTracks built its
patch target by hand instead of calling buildDomEditPatchTarget, already used
one function up for the same shape. resolveAuthoredTrack's selection param
was a hand-typed copy of ElementMatchSelection instead of importing it.
Collapses the three duplicated optional-integer track checks added to the
wrap/unwrap/split routes into one isOptionalInteger helper. No behavior
change.

* fix(studio): match an ungroup child with no DOM id by its stable id

findMatchingTimelineElementId had no branch for hfId, the stable content-hash
id every element gets regardless of whether it has an authored id. An
ungroup child identified only by that id fell through every branch and kept
its old track instead of getting the resolved one stamped, leaving it
exposed to the same row-drift this PR closes for every other element.

* refactor(studio): resolve review lint suppressions with real fixes

Three lint suppressions from this branch's earlier commits, resolved instead
of left as noise:

- unwrapElementsFromHtml's complexity: extracted the child-track map build
  into its own named function.
- handlePlaybackKeyDown's complexity: extracted the razor/A-key check into
  its own named function.
- The duplicated param block between UseDomEditSessionParams and
  UseDomSelectionParams: pulled the shared fields into one
  DomEditTimelineParams type both interfaces now extend.

No behavior change.

* fix(studio): fall back to the resolved track when nothing was authored

resolveAuthoredTrack only ever returned authoredTrack, so an implicit or
ungrouped child with no explicit authoredTrack resolved to undefined
instead of its actual resolved track, silently skipping the row-pin for
exactly the unauthored population this change exists to fix. Falls back
to the resolved track, matching the function's own existing comment.

* fix(studio): pin the razor split's resolved track for unauthored clips

buildCutTarget had the identical gap resolveAuthoredTrack did: it only
stamped an explicitly authored track, omitting the field entirely for
an implicit/unauthored clip, so a split of it still drifted onto a new
row. Falls back to the element's resolved track, same as the group and
ungroup paths.

* refactor(studio): share one track-fallback rule between split and group

resolveAuthoredTrack and buildCutTarget each re-implemented "authored
track, else the runtime's resolved track" with a different rounding
rule (one rounded only the authored branch, the other neither). Extract
resolveElementTrack so both flows share the same rule and can't drift
out of sync again.
2026-09-18 09:01:15 -04:00
..