From c20c5366da34c1a3444d3ff9282e01d92bf274d8 Mon Sep 17 00:00:00 2001 From: Miguel Angel Simon Sierra Date: Mon, 27 Jul 2026 23:45:19 +0200 Subject: [PATCH] fix(studio): commit lane edits through the edited element's own selection An explicit null selection override now aborts the write instead of falling back to domEditSelection: a caller that resolved a selection for its own element and found none was committing onto whichever element happened to be selected. Ease changes and the playhead keyframe toggle resolve the edited element's animations and selection instead of the current selection's, and lane header rows follow the real label-column width so a narrowed column no longer hangs its value readout over the canvas. --- .../components/nle/useTimelineEditCallbacks.ts | 2 +- .../src/hooks/useGsapSelectionHandlers.test.tsx | 17 +++++++++++++++++ .../src/hooks/useGsapSelectionHandlers.ts | 4 ++-- .../player/components/TimelineTrackHeader.tsx | 10 ++++++++-- 4 files changed, 28 insertions(+), 5 deletions(-) diff --git a/packages/studio/src/components/nle/useTimelineEditCallbacks.ts b/packages/studio/src/components/nle/useTimelineEditCallbacks.ts index 7ca47e0bf..635f31675 100644 --- a/packages/studio/src/components/nle/useTimelineEditCallbacks.ts +++ b/packages/studio/src/components/nle/useTimelineEditCallbacks.ts @@ -313,7 +313,7 @@ export function useTimelineEditCallbacks({ : 0; // Same frame for read and write: the toggled element's animations decide // add-vs-remove, and its selection is what the mutation commits through. - const animations = resolveElementAnimations(el.key ?? el.id); + const animations = resolveElementAnimations(getTimelineElementIdentity(el)); void buildDomSelectionForTimelineElement(el).then((selection) => { if (!selection) return; const anim = animations.find((a) => a.keyframes); diff --git a/packages/studio/src/hooks/useGsapSelectionHandlers.test.tsx b/packages/studio/src/hooks/useGsapSelectionHandlers.test.tsx index e2bd9628a..6dc247b36 100644 --- a/packages/studio/src/hooks/useGsapSelectionHandlers.test.tsx +++ b/packages/studio/src/hooks/useGsapSelectionHandlers.test.tsx @@ -163,3 +163,20 @@ describe("useGsapSelectionHandlers retime settlement", () => { withSelection.unmount(); }); }); + +describe("useGsapSelectionHandlers selection override", () => { + it("aborts on an explicit null override instead of writing to the current selection", () => { + const removeKeyframe = vi.fn(); + const rendered = renderHandlers(makeParams({ removeKeyframe })); + + // Explicit null: the caller resolved a selection for its own element and + // found none, so the write must not land on the selected element. + rendered.handlers().handleGsapRemoveKeyframe("anim-1", 50, undefined, null); + expect(removeKeyframe).not.toHaveBeenCalled(); + + // Omitted override: falls back to the current selection as before. + rendered.handlers().handleGsapRemoveKeyframe("anim-1", 50); + expect(removeKeyframe).toHaveBeenCalledOnce(); + rendered.unmount(); + }); +}); diff --git a/packages/studio/src/hooks/useGsapSelectionHandlers.ts b/packages/studio/src/hooks/useGsapSelectionHandlers.ts index 857cfdaac..7247433d4 100644 --- a/packages/studio/src/hooks/useGsapSelectionHandlers.ts +++ b/packages/studio/src/hooks/useGsapSelectionHandlers.ts @@ -418,7 +418,7 @@ export function useGsapSelectionHandlers({ const handleGsapRemoveAllKeyframes = useCallback( (animId: string, selectionOverride?: DomEditSelection | null) => { - const selection = selectionOverride ?? domEditSelection ?? lastSelectionRef.current; + const selection = resolveWriteSelection(selectionOverride); if (!selection) return; observeGsapMutation( removeAllKeyframes(selection, animId), @@ -427,7 +427,7 @@ export function useGsapSelectionHandlers({ "Remove all keyframes", ); }, - [domEditSelection, observeGsapMutation, removeAllKeyframes], + [resolveWriteSelection, observeGsapMutation, removeAllKeyframes], ); const handleResetSelectedElementKeyframes = useCallback((): boolean => { diff --git a/packages/studio/src/player/components/TimelineTrackHeader.tsx b/packages/studio/src/player/components/TimelineTrackHeader.tsx index 205203901..6b2c0fb37 100644 --- a/packages/studio/src/player/components/TimelineTrackHeader.tsx +++ b/packages/studio/src/player/components/TimelineTrackHeader.tsx @@ -178,6 +178,7 @@ function PropertyGroupHeaderRow({ currentTime, clipPercentage, gutterBackground, + columnWidth, onTogglePropertyGroupKeyframe, onSeek, }: { @@ -188,6 +189,7 @@ function PropertyGroupHeaderRow({ currentTime: number; clipPercentage: number; gutterBackground: string; + columnWidth: number; onTogglePropertyGroupKeyframe?: TimelineEditCallbacks["onTogglePropertyGroupKeyframe"]; onSeek?: (time: number) => void; }) { @@ -201,10 +203,13 @@ function PropertyGroupHeaderRow({