@hyperframes/studio 0.8.7 → 0.8.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/assets/{hyperframes-player-bDcU464i.js → hyperframes-player-OP68oEBG.js} +1 -1
- package/dist/assets/{index-CUXnyoIa.js → index-57oMsXQN.js} +111 -111
- package/dist/assets/{index-MSOSP1-J.js → index-BIdEpMGS.js} +1 -1
- package/dist/assets/{index-FkAs3zRu.js → index-D1Sa69m2.js} +1 -1
- package/dist/index.d.ts +10 -1
- package/dist/index.html +1 -1
- package/dist/index.js +40 -35
- package/dist/index.js.map +1 -1
- package/package.json +7 -7
- package/src/components/editor/CanvasContextMenu.test.tsx +1 -1
- package/src/components/editor/CanvasContextMenu.tsx +1 -1
- package/src/components/editor/PropertyPanelFlat.tsx +1 -5
- package/src/components/editor/TimelineFxPopover.test.tsx +89 -0
- package/src/components/editor/TimelineFxPopover.tsx +49 -15
- package/src/hooks/timelineTrackVisibility.test.ts +31 -0
- package/src/hooks/timelineTrackVisibility.ts +20 -4
- package/src/player/components/AutomationSelectionMenu.tsx +1 -1
- package/src/player/components/ClipContextMenu.tsx +1 -1
- package/src/player/components/KeyframeDiamondContextMenu.tsx +1 -1
- package/src/player/components/Timeline.test.ts +1 -1
- package/src/player/components/TimelineFxButton.tsx +1 -1
- package/src/player/components/TimelineGroupHeader.test.tsx +76 -0
- package/src/player/components/TimelineGroupHeader.tsx +8 -4
- package/src/player/components/TimelineLanes.test.tsx +3 -1
- package/src/player/components/TimelineTrackHeader.test.tsx +2 -1
- package/src/player/components/TimelineTrackPlainHeader.test.tsx +161 -0
- package/src/player/components/TimelineTrackPlainHeader.tsx +17 -10
- package/src/player/components/TrackGapContextMenu.tsx +1 -1
- package/src/player/components/timelineCallbacks.ts +14 -1
- package/src/player/components/useTimelineTrackDerivations.test.ts +103 -0
- package/src/player/components/useTimelineTrackDerivations.ts +4 -11
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
// @vitest-environment happy-dom
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The visibility control's accessible contract.
|
|
5
|
+
*
|
|
6
|
+
* The audio wording ran behind the `audio-track-mute` canary at 0%, so it had
|
|
7
|
+
* never rendered in any suite: it returned "Muted" / "Mute", which named the
|
|
8
|
+
* CURRENT state rather than the action, and dropped the track suffix so every
|
|
9
|
+
* audio row shared one accessible name. Music plus VO is the ordinary case, so
|
|
10
|
+
* that is two identical buttons. Pinned here because the label and the icon are
|
|
11
|
+
* the whole identity of this control.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import React, { act } from "react";
|
|
15
|
+
import { createRoot } from "react-dom/client";
|
|
16
|
+
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
17
|
+
import { PlainTrackHeader, VisibilityButton } from "./TimelineTrackPlainHeader";
|
|
18
|
+
|
|
19
|
+
(globalThis as unknown as { IS_REACT_ACT_ENVIRONMENT: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
|
|
20
|
+
|
|
21
|
+
afterEach(() => {
|
|
22
|
+
document.body.innerHTML = "";
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
function renderButton(props: {
|
|
26
|
+
hidden: boolean;
|
|
27
|
+
isAudioTrack?: boolean;
|
|
28
|
+
trackDisplayNumber: number | null;
|
|
29
|
+
}): { host: HTMLElement; unmount: () => void; onToggle: ReturnType<typeof vi.fn> } {
|
|
30
|
+
const host = document.createElement("div");
|
|
31
|
+
document.body.append(host);
|
|
32
|
+
const root = createRoot(host);
|
|
33
|
+
const onToggle = vi.fn();
|
|
34
|
+
act(() =>
|
|
35
|
+
root.render(
|
|
36
|
+
React.createElement(VisibilityButton, {
|
|
37
|
+
hidden: props.hidden,
|
|
38
|
+
trackNumber: 7,
|
|
39
|
+
trackDisplayNumber: props.trackDisplayNumber,
|
|
40
|
+
visible: true,
|
|
41
|
+
isAudioTrack: props.isAudioTrack,
|
|
42
|
+
onToggle,
|
|
43
|
+
}),
|
|
44
|
+
),
|
|
45
|
+
);
|
|
46
|
+
return { host, unmount: () => act(() => root.unmount()), onToggle };
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const labelOf = (host: HTMLElement) => host.querySelector("button")?.getAttribute("aria-label");
|
|
50
|
+
|
|
51
|
+
describe("VisibilityButton", () => {
|
|
52
|
+
it("names the action, not the state, on an audible audio track", () => {
|
|
53
|
+
const view = renderButton({ hidden: false, isAudioTrack: true, trackDisplayNumber: 2 });
|
|
54
|
+
expect(labelOf(view.host)).toBe("Mute track 2");
|
|
55
|
+
view.unmount();
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
// The half that was wrong: a muted row read "Muted", so nothing told a
|
|
59
|
+
// screen-reader user that activating it would unmute.
|
|
60
|
+
it("promises the un-mute when the audio track is already muted", () => {
|
|
61
|
+
const view = renderButton({ hidden: true, isAudioTrack: true, trackDisplayNumber: 2 });
|
|
62
|
+
expect(labelOf(view.host)).toBe("Unmute track 2");
|
|
63
|
+
view.unmount();
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it("keeps each audio row's name unique, so two tracks are distinguishable", () => {
|
|
67
|
+
const first = renderButton({ hidden: false, isAudioTrack: true, trackDisplayNumber: 1 });
|
|
68
|
+
const second = renderButton({ hidden: false, isAudioTrack: true, trackDisplayNumber: 3 });
|
|
69
|
+
expect(labelOf(first.host)).toBe("Mute track 1");
|
|
70
|
+
expect(labelOf(second.host)).toBe("Mute track 3");
|
|
71
|
+
expect(labelOf(first.host)).not.toBe(labelOf(second.host));
|
|
72
|
+
first.unmount();
|
|
73
|
+
second.unmount();
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it("still says Hide/Show on a visual track", () => {
|
|
77
|
+
const shown = renderButton({ hidden: false, trackDisplayNumber: 2 });
|
|
78
|
+
expect(labelOf(shown.host)).toBe("Hide track 2");
|
|
79
|
+
shown.unmount();
|
|
80
|
+
const hiddenRow = renderButton({ hidden: true, trackDisplayNumber: 2 });
|
|
81
|
+
expect(labelOf(hiddenRow.host)).toBe("Show track 2");
|
|
82
|
+
hiddenRow.unmount();
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
// The callback acts on the REAL track key; the display row rides along so the
|
|
86
|
+
// undo-history label announces the same row this button just did, instead of
|
|
87
|
+
// re-deriving it from an ordering that has no group anchors in it.
|
|
88
|
+
it("toggles the real track number and passes the row it announced", () => {
|
|
89
|
+
const view = renderButton({ hidden: false, isAudioTrack: true, trackDisplayNumber: 2 });
|
|
90
|
+
view.host.querySelector("button")?.dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
|
91
|
+
expect(view.onToggle).toHaveBeenCalledWith(7, true, 2);
|
|
92
|
+
view.unmount();
|
|
93
|
+
});
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
describe("PlainTrackHeader", () => {
|
|
97
|
+
function renderHeader(overrides: Partial<Parameters<typeof PlainTrackHeader>[0]> = {}) {
|
|
98
|
+
const host = document.createElement("div");
|
|
99
|
+
document.body.append(host);
|
|
100
|
+
const root = createRoot(host);
|
|
101
|
+
const onToggleSolo = vi.fn();
|
|
102
|
+
act(() =>
|
|
103
|
+
root.render(
|
|
104
|
+
React.createElement(PlainTrackHeader, {
|
|
105
|
+
trackNumber: 0,
|
|
106
|
+
trackDisplayNumber: 1,
|
|
107
|
+
trackLabel: "Voiceover",
|
|
108
|
+
clipCount: 1,
|
|
109
|
+
isTrackHidden: false,
|
|
110
|
+
isAudioTrack: true,
|
|
111
|
+
onToggleTrackHidden: vi.fn(),
|
|
112
|
+
showTrackLabel: true,
|
|
113
|
+
isGroupMuted: false,
|
|
114
|
+
isSoloed: false,
|
|
115
|
+
onToggleSolo,
|
|
116
|
+
...overrides,
|
|
117
|
+
}),
|
|
118
|
+
),
|
|
119
|
+
);
|
|
120
|
+
return { host, unmount: () => act(() => root.unmount()), onToggleSolo };
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const soloButton = (host: HTMLElement) =>
|
|
124
|
+
host.querySelector('button[aria-label="Hear only this"]');
|
|
125
|
+
const labelSpan = (host: HTMLElement) => host.querySelector("span.min-w-0");
|
|
126
|
+
|
|
127
|
+
it("offers solo on an audio track and reports its pressed state", () => {
|
|
128
|
+
const view = renderHeader({ isSoloed: true });
|
|
129
|
+
expect(soloButton(view.host)?.getAttribute("aria-pressed")).toBe("true");
|
|
130
|
+
view.unmount();
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it("withholds solo from a visual track", () => {
|
|
134
|
+
const view = renderHeader({ isAudioTrack: false });
|
|
135
|
+
expect(soloButton(view.host)).toBeNull();
|
|
136
|
+
view.unmount();
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
// A group-muted member is silent without being hidden itself, so the strike
|
|
140
|
+
// is the only thing that says so — and the title has to explain why, since a
|
|
141
|
+
// user who never touched THIS row's mute is looking for the reason.
|
|
142
|
+
it("strikes the label through when the row's own mute is on", () => {
|
|
143
|
+
const view = renderHeader({ isTrackHidden: true });
|
|
144
|
+
expect(labelSpan(view.host)?.className).toContain("line-through");
|
|
145
|
+
view.unmount();
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
it("strikes it through for a group mute too, and says which", () => {
|
|
149
|
+
const view = renderHeader({ isGroupMuted: true });
|
|
150
|
+
expect(labelSpan(view.host)?.className).toContain("line-through");
|
|
151
|
+
expect(labelSpan(view.host)?.getAttribute("title")).toBe("Voiceover (group muted)");
|
|
152
|
+
view.unmount();
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
it("leaves an audible row unstruck", () => {
|
|
156
|
+
const view = renderHeader();
|
|
157
|
+
expect(labelSpan(view.host)?.className).not.toContain("line-through");
|
|
158
|
+
expect(labelSpan(view.host)?.getAttribute("title")).toBe("Voiceover");
|
|
159
|
+
view.unmount();
|
|
160
|
+
});
|
|
161
|
+
});
|
|
@@ -1,15 +1,24 @@
|
|
|
1
1
|
import { Eye, EyeSlash, SpeakerHigh, SpeakerSlash } from "@phosphor-icons/react";
|
|
2
|
-
import { isCanaryEnabled } from "../../telemetry/canary";
|
|
3
2
|
import { Music } from "../../icons/SystemIcons";
|
|
4
3
|
import { TimelineSoloButton } from "./TimelineSoloButton";
|
|
5
4
|
import type { TimelineEditCallbacks } from "./timelineCallbacks";
|
|
6
5
|
import { TrackClipCount } from "./TrackClipCount";
|
|
7
6
|
import { trackDisplaySuffix } from "./timelineTrackDisplay";
|
|
8
7
|
|
|
9
|
-
|
|
10
|
-
|
|
8
|
+
/**
|
|
9
|
+
* Audio tracks say "Mute", not "Hide" — the eye IS mute for sound-only rows.
|
|
10
|
+
*
|
|
11
|
+
* Action-phrased and per-track, like the visual branch: `hidden` here is the
|
|
12
|
+
* CURRENT state, so the name has to promise the opposite. It read "Muted" /
|
|
13
|
+
* "Mute" until the rollout, which named the state instead of the action (a
|
|
14
|
+
* screen-reader user could not tell that activating an already-muted row would
|
|
15
|
+
* unmute it) and dropped `suffix`, so every audio row shared one accessible
|
|
16
|
+
* name — music plus VO being the ordinary case. The wording matches the undo
|
|
17
|
+
* entry `timelineTrackVisibility` writes for the same click, where `hidden` is
|
|
18
|
+
* the INCOMING state and the two therefore read inverted.
|
|
19
|
+
*/
|
|
11
20
|
function visibilityButtonLabel(showAsMute: boolean, hidden: boolean, suffix: string): string {
|
|
12
|
-
if (showAsMute) return hidden ?
|
|
21
|
+
if (showAsMute) return hidden ? `Unmute track${suffix}` : `Mute track${suffix}`;
|
|
13
22
|
return hidden ? `Show track${suffix}` : `Hide track${suffix}`;
|
|
14
23
|
}
|
|
15
24
|
|
|
@@ -37,7 +46,7 @@ export function VisibilityButton({
|
|
|
37
46
|
// Display number in the text, real key in the callback. The two must not be
|
|
38
47
|
// conflated in either direction.
|
|
39
48
|
const suffix = trackDisplaySuffix(trackDisplayNumber);
|
|
40
|
-
const showAsMute = Boolean(isAudioTrack)
|
|
49
|
+
const showAsMute = Boolean(isAudioTrack);
|
|
41
50
|
const label = visibilityButtonLabel(showAsMute, hidden, suffix);
|
|
42
51
|
return (
|
|
43
52
|
<button
|
|
@@ -50,7 +59,7 @@ export function VisibilityButton({
|
|
|
50
59
|
onPointerDown={(event) => event.stopPropagation()}
|
|
51
60
|
onClick={(event) => {
|
|
52
61
|
event.stopPropagation();
|
|
53
|
-
void onToggle?.(trackNumber, !hidden);
|
|
62
|
+
void onToggle?.(trackNumber, !hidden, trackDisplayNumber);
|
|
54
63
|
}}
|
|
55
64
|
>
|
|
56
65
|
{visibilityButtonIcon(showAsMute, hidden)}
|
|
@@ -93,9 +102,7 @@ export function PlainTrackHeader({
|
|
|
93
102
|
{showTrackLabel && (
|
|
94
103
|
<span
|
|
95
104
|
className={`min-w-0 flex-1 truncate text-[11px] ${
|
|
96
|
-
isAudioTrack && (isTrackHidden || isGroupMuted)
|
|
97
|
-
? "line-through"
|
|
98
|
-
: ""
|
|
105
|
+
isAudioTrack && (isTrackHidden || isGroupMuted) ? "line-through" : ""
|
|
99
106
|
}`}
|
|
100
107
|
title={isGroupMuted && !isTrackHidden ? `${trackLabel} (group muted)` : trackLabel}
|
|
101
108
|
>
|
|
@@ -111,7 +118,7 @@ export function PlainTrackHeader({
|
|
|
111
118
|
isAudioTrack={isAudioTrack}
|
|
112
119
|
onToggle={onToggleTrackHidden}
|
|
113
120
|
/>
|
|
114
|
-
{isAudioTrack &&
|
|
121
|
+
{isAudioTrack && onToggleSolo && (
|
|
115
122
|
<TimelineSoloButton isSoloed={isSoloed} onToggle={onToggleSolo} />
|
|
116
123
|
)}
|
|
117
124
|
</>
|
|
@@ -75,7 +75,7 @@ export const TrackGapContextMenu = memo(function TrackGapContextMenu({
|
|
|
75
75
|
return createPortal(
|
|
76
76
|
<div
|
|
77
77
|
ref={menuRef}
|
|
78
|
-
className="fixed z-
|
|
78
|
+
className="fixed z-[200] bg-neutral-900 border border-neutral-700 rounded-md shadow-lg py-1 min-w-[180px]"
|
|
79
79
|
style={{ left: adjustedX, top: adjustedY }}
|
|
80
80
|
onPointerLeave={() => onHoverAction(null)}
|
|
81
81
|
>
|
|
@@ -67,7 +67,20 @@ export interface TimelineEditCallbacks {
|
|
|
67
67
|
}>,
|
|
68
68
|
options?: { coalesceKey?: string },
|
|
69
69
|
) => Promise<void> | void;
|
|
70
|
-
|
|
70
|
+
/**
|
|
71
|
+
* `displayNumber` is the row the CLICKED control announced. It travels with
|
|
72
|
+
* the click because the header and the undo-history label derive the row from
|
|
73
|
+
* two different orderings: the header's comes from the group-aware row list
|
|
74
|
+
* (synthetic anchor rows, members pulled contiguous), the history's from a
|
|
75
|
+
* plain ascending sort of element-bearing keys. Once a group exists those
|
|
76
|
+
* disagree, so the same click said "Mute track 2" and recorded "Mute track 1".
|
|
77
|
+
* Passing the rendered number keeps one answer instead of two derivations.
|
|
78
|
+
*/
|
|
79
|
+
onToggleTrackHidden?: (
|
|
80
|
+
track: number,
|
|
81
|
+
hidden: boolean,
|
|
82
|
+
displayNumber?: number | null,
|
|
83
|
+
) => Promise<void> | void;
|
|
71
84
|
/** B7's bus strip: live-write the group's own attribute while dragging. */
|
|
72
85
|
onSetAudioGroupAttributeLive?: (groupId: string, attr: string, value: string | null) => void;
|
|
73
86
|
/** ...and persist one undo entry on release. */
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
// @vitest-environment happy-dom
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Row derivation for audio groups.
|
|
5
|
+
*
|
|
6
|
+
* This ran behind the `audio-groups` canary at 0% until the rollout, so it had
|
|
7
|
+
* never executed in the enabled state in any suite — the off-cohort branch
|
|
8
|
+
* returned raw tracks and stopped. Now it reorders rows and emits synthetic
|
|
9
|
+
* anchor rows for every user, which is exactly the pair of invariants pinned
|
|
10
|
+
* here: an ungrouped project is untouched, and a group's members become
|
|
11
|
+
* contiguous under an anchor at `memberTracks[0] - 0.5`.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import React, { act } from "react";
|
|
15
|
+
import { createRoot } from "react-dom/client";
|
|
16
|
+
import { afterEach, describe, expect, it } from "vitest";
|
|
17
|
+
import { usePlayerStore, type TimelineElement } from "../store/playerStore";
|
|
18
|
+
import { useTimelineTrackDerivations } from "./useTimelineTrackDerivations";
|
|
19
|
+
|
|
20
|
+
globalThis.IS_REACT_ACT_ENVIRONMENT = true;
|
|
21
|
+
|
|
22
|
+
afterEach(() => {
|
|
23
|
+
usePlayerStore.getState().reset();
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
function clip(id: string, track: number, extra: Partial<TimelineElement> = {}): TimelineElement {
|
|
27
|
+
return { id, label: id, tag: "audio", start: 0, duration: 1, track, ...extra };
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function derive(elements: TimelineElement[]): ReturnType<typeof useTimelineTrackDerivations> {
|
|
31
|
+
let result: ReturnType<typeof useTimelineTrackDerivations> | undefined;
|
|
32
|
+
function Probe() {
|
|
33
|
+
result = useTimelineTrackDerivations(elements);
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
const root = createRoot(document.createElement("div"));
|
|
37
|
+
act(() => root.render(React.createElement(Probe)));
|
|
38
|
+
act(() => root.unmount());
|
|
39
|
+
if (!result) throw new Error("derivations did not render");
|
|
40
|
+
return result;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
describe("useTimelineTrackDerivations", () => {
|
|
44
|
+
it("leaves an ungrouped project in raw ascending order", () => {
|
|
45
|
+
const { tracks, groups, trackGroupOf } = derive([clip("b", 2), clip("a", 0), clip("c", 1)]);
|
|
46
|
+
|
|
47
|
+
expect(tracks.map(([track]) => track)).toEqual([0, 1, 2]);
|
|
48
|
+
expect(tracks.map(([, els]) => els.map((el) => el.id))).toEqual([["a"], ["c"], ["b"]]);
|
|
49
|
+
expect(groups).toEqual([]);
|
|
50
|
+
expect(trackGroupOf.size).toBe(0);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
// The reordering case: track 1 sits between the group's two members in raw
|
|
54
|
+
// order and must not be dragged into the group.
|
|
55
|
+
it("pulls interleaved members contiguous under a synthetic anchor row", () => {
|
|
56
|
+
const { tracks, groups, trackGroupOf } = derive([
|
|
57
|
+
clip("vo-1", 0, { audioGroup: "voiceover", audioGroupLabel: "Voiceover" }),
|
|
58
|
+
clip("music", 1),
|
|
59
|
+
clip("vo-2", 2, { audioGroup: "voiceover" }),
|
|
60
|
+
]);
|
|
61
|
+
|
|
62
|
+
// Anchor immediately above the lowest member, then both members, then the
|
|
63
|
+
// ungrouped track keeps its own position after them.
|
|
64
|
+
expect(tracks.map(([track]) => track)).toEqual([-0.5, 0, 2, 1]);
|
|
65
|
+
// The anchor row owns no clips of its own.
|
|
66
|
+
expect(tracks[0]?.[1]).toEqual([]);
|
|
67
|
+
|
|
68
|
+
expect(groups).toHaveLength(1);
|
|
69
|
+
expect(groups[0]).toMatchObject({
|
|
70
|
+
id: "voiceover",
|
|
71
|
+
label: "Voiceover",
|
|
72
|
+
anchorKey: -0.5,
|
|
73
|
+
memberTracks: [0, 2],
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
expect(trackGroupOf.get(0)?.id).toBe("voiceover");
|
|
77
|
+
expect(trackGroupOf.get(2)?.id).toBe("voiceover");
|
|
78
|
+
expect(trackGroupOf.has(1)).toBe(false);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it("carries the group element's own label, volume and mute onto the row", () => {
|
|
82
|
+
const { groups } = derive([
|
|
83
|
+
clip("vo-1", 3, {
|
|
84
|
+
audioGroup: "vo",
|
|
85
|
+
audioGroupLabel: "VO bus",
|
|
86
|
+
audioGroupVolume: 0.5,
|
|
87
|
+
audioGroupHidden: true,
|
|
88
|
+
}),
|
|
89
|
+
]);
|
|
90
|
+
|
|
91
|
+
expect(groups[0]).toMatchObject({
|
|
92
|
+
label: "VO bus",
|
|
93
|
+
anchorKey: 2.5,
|
|
94
|
+
volume: 0.5,
|
|
95
|
+
hidden: true,
|
|
96
|
+
});
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it("falls back to the group id and unity volume when the bus carries neither", () => {
|
|
100
|
+
const { groups } = derive([clip("sfx-1", 0, { audioGroup: "sfx" })]);
|
|
101
|
+
expect(groups[0]).toMatchObject({ id: "sfx", label: "sfx", volume: 1, hidden: false });
|
|
102
|
+
});
|
|
103
|
+
});
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { useMemo } from "react";
|
|
2
2
|
import type { TimelineElement } from "../store/playerStore";
|
|
3
|
-
import { isCanaryEnabled } from "../../telemetry/canary";
|
|
4
3
|
import { getTrackStyle, type TrackVisualStyle } from "./timelineIcons";
|
|
5
4
|
|
|
6
5
|
/** One resolved audio group, positioned in the row order. */
|
|
@@ -157,16 +156,10 @@ export function useTimelineTrackDerivations(expandedElements: TimelineElement[])
|
|
|
157
156
|
return Array.from(map.entries()).sort(([a], [b]) => a - b);
|
|
158
157
|
}, [expandedElements]);
|
|
159
158
|
|
|
160
|
-
const { tracks, groups, trackGroupOf } = useMemo(
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
groups: [],
|
|
165
|
-
trackGroupOf: new Map<number, TimelineTrackGroupInfo>(),
|
|
166
|
-
};
|
|
167
|
-
}
|
|
168
|
-
return groupTimelineTracks(rawTracks);
|
|
169
|
-
}, [rawTracks]);
|
|
159
|
+
const { tracks, groups, trackGroupOf } = useMemo(
|
|
160
|
+
() => groupTimelineTracks(rawTracks),
|
|
161
|
+
[rawTracks],
|
|
162
|
+
);
|
|
170
163
|
|
|
171
164
|
const trackStyles = useMemo(() => {
|
|
172
165
|
const map = new Map<number, TrackVisualStyle>();
|