Skip to content

Commit dddeedd

Browse files
vanceingallsclaude
andcommitted
fix(studio): alt-click selects one linked member even inside the pair selection
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 parent 72fb959 commit dddeedd

4 files changed

Lines changed: 54 additions & 11 deletions

File tree

‎packages/studio/src/player/components/audioClipLink.test.ts‎

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,3 +79,36 @@ describe("linkedGestureKeys", () => {
7979
);
8080
});
8181
});
82+
83+
describe("selectClipWithLinks", () => {
84+
const linkedPair = [
85+
{ id: "talk", tag: "video", start: 0, duration: 4, track: 0, link: "lk-1" },
86+
{ id: "talk-audio", tag: "audio", start: 0, duration: 4, track: 1, link: "lk-1" },
87+
{ id: "title", tag: "div", start: 0, duration: 4, track: 2 },
88+
];
89+
90+
async function store() {
91+
const { usePlayerStore } = await import("../store/playerStore");
92+
const { selectClipWithLinks } = await import("./timelineLinkSelection");
93+
usePlayerStore.getState().setElements(linkedPair);
94+
const click = (key: string, alt = false) =>
95+
selectClipWithLinks(key, alt, usePlayerStore.getState().setSelectedElementId);
96+
return { usePlayerStore, click };
97+
}
98+
99+
it("a click selects the whole pair; Alt-click then narrows to one member", async () => {
100+
const { usePlayerStore, click } = await store();
101+
click("talk");
102+
expect(usePlayerStore.getState().selectedElementIds).toEqual(new Set(["talk", "talk-audio"]));
103+
click("talk-audio", true);
104+
expect(usePlayerStore.getState().selectedElementIds).toEqual(new Set(["talk-audio"]));
105+
});
106+
107+
it("a plain click collapses a larger selection to the clicked clip's link group", async () => {
108+
const { usePlayerStore, click } = await store();
109+
usePlayerStore.getState().setSelection(["talk", "talk-audio", "title"], "title");
110+
click("talk");
111+
expect(usePlayerStore.getState().selectedElementIds).toEqual(new Set(["talk", "talk-audio"]));
112+
expect(usePlayerStore.getState().selectedElementId).toBe("talk");
113+
});
114+
});

‎packages/studio/src/player/components/timelineClipGestureHandlers.ts‎

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import {
88
import type { TimelineEditCapabilities } from "./timelineEditCapabilities";
99
import type { TimelineEditCallbacks } from "./timelineCallbacks";
1010
import { CLIP_HANDLE_W } from "./timelineLayout";
11-
import { selectLinkPartners } from "./timelineLinkSelection";
11+
import { selectClipWithLinks } from "./timelineLinkSelection";
1212
import { SPLIT_BOUNDARY_EPSILON_S } from "../../utils/timelineElementSplit";
1313

1414
export interface ClipGestureDeps {
@@ -220,8 +220,7 @@ export function createClipGestureHandlers(
220220
return;
221221
}
222222
// Clip selection is idempotent; empty timeline space owns deselection.
223-
setSelectedElementId(elementKey);
224-
if (!e.altKey) selectLinkPartners(elementKey);
223+
selectClipWithLinks(elementKey, e.altKey, setSelectedElementId);
225224
onSelectElement?.(el);
226225
};
227226

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,22 @@
11
import { usePlayerStore } from "../store/playerStore";
22
import { expandToLinkedMembers } from "./audioClipLink";
33

4-
/** After selecting `key`, widen the selection to its link partners (primary stays `key`). */
5-
export function selectLinkPartners(key: string): void {
4+
/**
5+
* Select `key` the way a clip click does, then widen to link partners; Alt
6+
* selects `key` alone even when it sat inside a larger selection.
7+
*/
8+
export function selectClipWithLinks(
9+
key: string,
10+
altKey: boolean,
11+
setSelectedElementId: (id: string) => void,
12+
): void {
613
const state = usePlayerStore.getState();
7-
const expanded = expandToLinkedMembers([key], state.elements);
8-
if (expanded.size <= 1) return;
9-
state.setSelection(expanded, key);
14+
if (altKey) {
15+
state.setSelection([key], key);
16+
return;
17+
}
18+
setSelectedElementId(key);
19+
const { selectedElementIds, elements } = usePlayerStore.getState();
20+
const expanded = expandToLinkedMembers(selectedElementIds, elements);
21+
if (expanded.size > selectedElementIds.size) state.setSelection(expanded, key);
1022
}

‎packages/studio/src/player/components/useTimelineClipContextMenu.ts‎

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { useCallback } from "react";
22
import { usePlayerStore, type TimelineElement } from "../store/playerStore";
33
import type { ClipContextMenuState } from "./TimelineProvider";
4-
import { selectLinkPartners } from "./timelineLinkSelection";
4+
import { selectClipWithLinks } from "./timelineLinkSelection";
55

66
/** A clicked member of an active multi-selection keeps the whole group
77
* selected, so the context menu it opens acts on all of it. */
@@ -17,8 +17,7 @@ export function useClipContextMenu(
1717
e.preventDefault();
1818
const id = el.key ?? el.id;
1919
if (!(selectedElementIds.size > 1 && selectedElementIds.has(id))) {
20-
setSelectedElementId(id);
21-
if (!e.altKey) selectLinkPartners(id);
20+
selectClipWithLinks(id, e.altKey, setSelectedElementId);
2221
onSelectElement?.(el);
2322
}
2423
dismissGapMenu();

0 commit comments

Comments
 (0)