@hyperframes/studio 0.8.10 → 0.8.12
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-DoTrWV8q.js → hyperframes-player-B243szNa.js} +1 -1
- package/dist/assets/{index-DV45YLqR.js → index-BTP86sD-.js} +1 -1
- package/dist/assets/index-BZfUJ2He.js +431 -0
- package/dist/assets/{index-BQs1fahB.js → index-DQ_lkuqY.js} +1 -1
- package/dist/assets/index-yGhfxxoL.css +1 -0
- package/dist/{chunk-ZALQ3CW7.js → chunk-6BT6DTB4.js} +2 -1
- package/dist/{domEditingLayers-XRVXRTNX.js → domEditingLayers-URA7BLWE.js} +2 -2
- package/dist/index.d.ts +84 -43
- package/dist/index.html +2 -2
- package/dist/index.js +6440 -5514
- package/dist/index.js.map +1 -1
- package/package.json +8 -8
- package/src/App.tsx +0 -2
- package/src/components/StudioRightPanel.tsx +15 -57
- package/src/components/editor/PropertyPanel.test.tsx +101 -3
- package/src/components/editor/PropertyPanel.tsx +15 -1
- package/src/components/editor/PropertyPanelEmptyState.test.tsx +74 -0
- package/src/components/editor/PropertyPanelEmptyState.tsx +35 -20
- package/src/components/editor/PropertyPanelFlat.tsx +82 -21
- package/src/components/editor/PropertyPanelFlatHeader.test.tsx +21 -0
- package/src/components/editor/TimelineFxPopover.tsx +24 -4
- package/src/components/editor/audioFxRevealTarget.test.ts +59 -0
- package/src/components/editor/audioFxRevealTarget.ts +113 -0
- package/src/components/editor/audioFxSignalPath.test.ts +50 -0
- package/src/components/editor/audioFxSignalPath.ts +71 -0
- package/src/components/editor/audioFxSummary.test.ts +8 -0
- package/src/components/editor/audioFxSummary.ts +28 -16
- package/src/components/editor/propertyPanelAudioFxGroup.test.tsx +101 -0
- package/src/components/editor/propertyPanelAudioFxGroup.tsx +92 -2
- package/src/components/editor/propertyPanelFlatClosedGroup.tsx +46 -0
- package/src/components/editor/propertyPanelFlatMotionSection.tsx +31 -0
- package/src/components/editor/propertyPanelFxCarveModule.tsx +203 -125
- package/src/components/editor/propertyPanelFxControls.test.tsx +50 -0
- package/src/components/editor/propertyPanelFxControls.tsx +10 -2
- package/src/components/editor/propertyPanelFxEqModule.tsx +3 -0
- package/src/components/editor/propertyPanelFxNodeRow.tsx +11 -1
- package/src/components/editor/propertyPanelFxPresetMenu.tsx +12 -2
- package/src/components/editor/propertyPanelFxRackChain.tsx +12 -5
- package/src/components/editor/propertyPanelFxSection.test.tsx +42 -0
- package/src/components/editor/propertyPanelFxSection.tsx +63 -4
- package/src/components/editor/propertyPanelFxSectionTypes.ts +19 -0
- package/src/components/editor/useAudioFxRevealSection.ts +64 -0
- package/src/components/editor/useAuditionTransport.test.ts +36 -0
- package/src/components/editor/useAuditionTransport.ts +78 -0
- package/src/components/editor/useFxAudition.ts +12 -1
- package/src/components/editor/useFxCarve.ts +0 -0
- package/src/components/editor/useFxCarveGrouping.test.ts +118 -0
- package/src/components/editor/useFxCarveGrouping.ts +139 -4
- package/src/components/editor/useFxCarveNodes.ts +153 -0
- package/src/components/editor/useFxLevelling.ts +5 -34
- package/src/components/nle/NLEContext.tsx +3 -10
- package/src/components/nle/PreviewPane.tsx +0 -1
- package/src/components/nle/useTimelineEditCallbacks.ts +5 -1
- package/src/contexts/DesignPanelInputContext.tsx +6 -5
- package/src/contexts/DomEditContext.tsx +10 -3
- package/src/contexts/FileManagerContext.tsx +3 -2
- package/src/contexts/PanelLayoutContext.tsx +3 -2
- package/src/contexts/StudioContext.tsx +7 -3
- package/src/contexts/TimelineEditContext.tsx +6 -2
- package/src/contexts/VariablePromoteContext.tsx +6 -2
- package/src/contexts/ViewModeContext.tsx +2 -2
- package/src/hooks/domEditDeleteMembers.ts +34 -0
- package/src/hooks/timelineAudioGroupCreate.ts +345 -0
- package/src/hooks/timelineAudioGroupVolume.test.ts +204 -0
- package/src/hooks/timelineAudioGroupVolume.ts +135 -3
- package/src/hooks/timelineEditingHelpers.test.ts +86 -0
- package/src/hooks/timelineEditingHelpers.ts +19 -7
- package/src/hooks/timelineElementFxAttribute.ts +0 -3
- package/src/hooks/timelineTrackVisibility.test.ts +95 -13
- package/src/hooks/timelineTrackVisibility.ts +6 -182
- package/src/hooks/useAudioGroupCarveAssignment.test.tsx +139 -0
- package/src/hooks/useBlockedTimelineEditToast.ts +27 -0
- package/src/hooks/useDomEditAttributeCommits.ts +13 -0
- package/src/hooks/useDomEditSession.test.tsx +57 -2
- package/src/hooks/useDomEditSession.ts +23 -24
- package/src/hooks/useEffectiveTimelineDuration.ts +9 -7
- package/src/hooks/useLivePlayheadTime.ts +3 -1
- package/src/hooks/useRemoveBackground.ts +70 -0
- package/src/hooks/useTimelineEditing.ts +5 -19
- package/src/hooks/useTimelineEditingTypes.ts +7 -0
- package/src/player/components/AutomationSelectionMenu.tsx +4 -0
- package/src/player/components/LayerDisclosureRow.tsx +59 -25
- package/src/player/components/PlayerControls.tsx +0 -38
- package/src/player/components/Timeline.test.ts +40 -1
- package/src/player/components/Timeline.tsx +5 -2
- package/src/player/components/TimelineAutomationLane.test.tsx +67 -0
- package/src/player/components/TimelineAutomationLane.tsx +89 -12
- package/src/player/components/TimelineAutomationLaneSlot.tsx +22 -3
- package/src/player/components/TimelineFxButton.test.tsx +184 -7
- package/src/player/components/TimelineFxButton.tsx +171 -23
- package/src/player/components/TimelineGroupHeader.test.tsx +0 -5
- package/src/player/components/TimelineGroupHeader.tsx +100 -102
- package/src/player/components/TimelineGroupLaneLabels.tsx +88 -0
- package/src/player/components/TimelineGroupRow.test.tsx +146 -0
- package/src/player/components/TimelineGroupRow.tsx +123 -64
- package/src/player/components/TimelineLanes.test.tsx +3 -3
- package/src/player/components/TimelineLanes.tsx +25 -23
- package/src/player/components/TimelineTrackHeader.test.tsx +298 -13
- package/src/player/components/TimelineTrackHeader.tsx +301 -381
- package/src/player/components/TimelineTrackPlainHeader.test.tsx +47 -69
- package/src/player/components/TimelineTrackPlainHeader.tsx +64 -54
- package/src/player/components/automationLaneData.test.ts +94 -0
- package/src/player/components/automationLaneData.ts +23 -0
- package/src/player/components/groupAutomationElement.test.ts +51 -0
- package/src/player/components/groupAutomationElement.ts +37 -0
- package/src/player/components/timelineCallbacks.ts +6 -2
- package/src/player/components/timelineKeyboardNavigation.test.ts +2 -2
- package/src/player/components/timelineKeyboardNavigation.ts +25 -8
- package/src/player/components/timelineLayout.ts +0 -1
- package/src/player/components/timelineViewModel.ts +30 -1
- package/src/player/components/trackHeaderLabelRows.tsx +328 -0
- package/src/player/components/useTimelineClipDisclosure.ts +41 -0
- package/src/player/components/useTimelineLaneRowIndexes.ts +1 -1
- package/src/player/components/useTimelineLogicalFocus.ts +2 -2
- package/src/player/components/useTimelineLogicalRows.test.tsx +2 -2
- package/src/player/components/useTimelineLogicalRows.ts +3 -3
- package/src/player/components/useTimelineTrackDerivations.ts +47 -8
- package/src/player/components/useTimelineTrackLayout.test.ts +119 -0
- package/src/player/components/useTimelineTrackLayout.ts +12 -4
- package/src/player/hooks/previewMessageRouter.ts +4 -11
- package/src/player/hooks/timelineSyncHydration.ts +395 -0
- package/src/player/hooks/useExpandedTimelineElements.test.ts +83 -0
- package/src/player/hooks/useExpandedTimelineElements.ts +40 -1
- package/src/player/hooks/useTimelinePlayer.ts +2 -7
- package/src/player/hooks/useTimelineSyncCallbacks.ts +43 -219
- package/src/player/lib/automationStoreSync.test.ts +26 -0
- package/src/player/lib/automationStoreSync.ts +28 -4
- package/src/player/lib/runtimeAudioId.test.ts +58 -0
- package/src/player/lib/timelineDOM.test.ts +115 -0
- package/src/player/lib/timelineDOM.ts +3 -32
- package/src/player/lib/timelineElementHelpers.ts +27 -0
- package/src/player/lib/timelineGroupInfo.ts +134 -0
- package/src/player/lib/timelineIframeHelpers.test.ts +21 -0
- package/src/player/lib/timelineIframeHelpers.ts +17 -0
- package/src/player/store/keyframeSlice.ts +79 -5
- package/src/player/store/playerStore.ts +37 -47
- package/src/player/store/playerStoreDevHandle.ts +22 -0
- package/src/player/store/playerStoreSelection.ts +41 -0
- package/src/player/store/timelineElement.ts +32 -0
- package/src/utils/hmrStableContext.ts +64 -0
- package/src/utils/timelineInspector.test.ts +35 -1
- package/src/utils/timelineInspector.ts +38 -0
- package/dist/assets/index-Ba9zbpT9.css +0 -1
- package/dist/assets/index-C6m2nHiX.js +0 -428
- package/src/hooks/useAudioSoloBridge.ts +0 -61
- package/src/hooks/useGroupLevel.ts +0 -36
- package/src/player/components/TimelineGroupBusStrip.test.tsx +0 -146
- package/src/player/components/TimelineGroupBusStrip.tsx +0 -103
- package/src/player/components/TimelineSoloButton.tsx +0 -32
- package/src/player/store/audioSoloSlice.test.ts +0 -113
- package/src/player/store/audioSoloSlice.ts +0 -43
- package/src/player/store/groupLevels.ts +0 -33
- /package/dist/{chunk-ZALQ3CW7.js.map → chunk-6BT6DTB4.js.map} +0 -0
- /package/dist/{domEditingLayers-XRVXRTNX.js.map → domEditingLayers-URA7BLWE.js.map} +0 -0
|
@@ -7,9 +7,9 @@ import { afterEach, describe, expect, it, vi } from "vitest";
|
|
|
7
7
|
import { TimelinePropertyLanes } from "./TimelinePropertyLanes";
|
|
8
8
|
import { TimelineTrackHeader } from "./TimelineTrackHeader";
|
|
9
9
|
import { defaultTimelineTheme } from "./timelineTheme";
|
|
10
|
-
import type
|
|
10
|
+
import { type TimelineElement } from "../store/playerStore";
|
|
11
11
|
import type { TimelineEditCallbacks } from "./timelineCallbacks";
|
|
12
|
-
import { getTimelineLaneTop, LABEL_COL_W } from "./timelineLayout";
|
|
12
|
+
import { getTimelineLaneTop, LABEL_COL_W, TRACK_H } from "./timelineLayout";
|
|
13
13
|
import { AUTOMATION_LANE_H } from "./automationLaneHeight";
|
|
14
14
|
|
|
15
15
|
(globalThis as unknown as { IS_REACT_ACT_ENVIRONMENT: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
|
|
@@ -72,6 +72,9 @@ interface RenderHeaderOptions {
|
|
|
72
72
|
onTogglePropertyGroupKeyframe?: TimelineEditCallbacks["onTogglePropertyGroupKeyframe"];
|
|
73
73
|
onToggleTrackHidden?: TimelineEditCallbacks["onToggleTrackHidden"];
|
|
74
74
|
onRemoveAutomationLane?: (target: string) => void;
|
|
75
|
+
isAudioTrack?: boolean;
|
|
76
|
+
isGroupMember?: boolean;
|
|
77
|
+
isTrackHidden?: boolean;
|
|
75
78
|
}
|
|
76
79
|
|
|
77
80
|
function renderHeader(options: RenderHeaderOptions = {}): {
|
|
@@ -82,7 +85,20 @@ function renderHeader(options: RenderHeaderOptions = {}): {
|
|
|
82
85
|
const host = document.createElement("div");
|
|
83
86
|
document.body.append(host);
|
|
84
87
|
const root = createRoot(host);
|
|
85
|
-
const render = (
|
|
88
|
+
const render = (raw: RenderHeaderOptions) => {
|
|
89
|
+
// Defaults resolved once, up front, rather than as a `??` per prop in the
|
|
90
|
+
// JSX — a dozen of those is a dozen branches through one arrow.
|
|
91
|
+
const next = {
|
|
92
|
+
keyframeClip: ELEMENT,
|
|
93
|
+
clipCount: 1,
|
|
94
|
+
animations: [POSITION, OPACITY],
|
|
95
|
+
currentTime: 0,
|
|
96
|
+
isAudioTrack: false,
|
|
97
|
+
isGroupMember: false,
|
|
98
|
+
isTrackHidden: false,
|
|
99
|
+
onToggleTrackHidden: vi.fn(),
|
|
100
|
+
...raw,
|
|
101
|
+
};
|
|
86
102
|
act(() => {
|
|
87
103
|
root.render(
|
|
88
104
|
<TimelineTrackHeader
|
|
@@ -93,17 +109,18 @@ function renderHeader(options: RenderHeaderOptions = {}): {
|
|
|
93
109
|
trackLabel="Hero card"
|
|
94
110
|
lanesId="timeline-lanes-track-0"
|
|
95
111
|
contentOrigin={LABEL_COL_W}
|
|
96
|
-
keyframeClip={next.keyframeClip
|
|
97
|
-
trackElements={next.trackElements ?? [next.keyframeClip
|
|
98
|
-
clipCount={next.clipCount
|
|
112
|
+
keyframeClip={next.keyframeClip}
|
|
113
|
+
trackElements={next.trackElements ?? [next.keyframeClip]}
|
|
114
|
+
clipCount={next.clipCount}
|
|
99
115
|
isExpanded={next.expanded !== false}
|
|
100
|
-
animations={next.animations
|
|
101
|
-
currentTime={next.currentTime
|
|
102
|
-
isTrackHidden={
|
|
103
|
-
isAudioTrack={
|
|
116
|
+
animations={next.animations}
|
|
117
|
+
currentTime={next.currentTime}
|
|
118
|
+
isTrackHidden={next.isTrackHidden}
|
|
119
|
+
isAudioTrack={next.isAudioTrack}
|
|
120
|
+
isGroupMember={next.isGroupMember}
|
|
104
121
|
theme={defaultTimelineTheme}
|
|
105
122
|
onToggleClipExpanded={vi.fn()}
|
|
106
|
-
onToggleTrackHidden={next.onToggleTrackHidden
|
|
123
|
+
onToggleTrackHidden={next.onToggleTrackHidden}
|
|
107
124
|
onTogglePropertyGroupKeyframe={next.onTogglePropertyGroupKeyframe}
|
|
108
125
|
onRemoveAutomationLane={next.onRemoveAutomationLane}
|
|
109
126
|
onSeek={next.onSeek}
|
|
@@ -122,6 +139,63 @@ function click(host: HTMLElement, label: string) {
|
|
|
122
139
|
}
|
|
123
140
|
|
|
124
141
|
describe("TimelineTrackHeader", () => {
|
|
142
|
+
// §5: gain stages multiply. A group fading to 0.42 under a clip fading to
|
|
143
|
+
// 0.80 plays at 0.34, and an author who drew both hears something quieter
|
|
144
|
+
// than either with nothing on screen to say why. Not a warning; an
|
|
145
|
+
// explanation.
|
|
146
|
+
it("says so when the clip's group is fading the same parameter", () => {
|
|
147
|
+
const automation = JSON.stringify({
|
|
148
|
+
version: 1,
|
|
149
|
+
lanes: [
|
|
150
|
+
{
|
|
151
|
+
target: "volume",
|
|
152
|
+
points: [
|
|
153
|
+
{ t: 0, v: 1 },
|
|
154
|
+
{ t: 2, v: 0.4 },
|
|
155
|
+
],
|
|
156
|
+
},
|
|
157
|
+
],
|
|
158
|
+
});
|
|
159
|
+
const clip: TimelineElement = {
|
|
160
|
+
...ELEMENT,
|
|
161
|
+
tag: "audio",
|
|
162
|
+
automation,
|
|
163
|
+
audioGroup: "voiceover",
|
|
164
|
+
audioGroupLabel: "Voiceover",
|
|
165
|
+
audioGroupAutomation: automation,
|
|
166
|
+
};
|
|
167
|
+
const view = renderHeader({ keyframeClip: clip, trackElements: [clip], animations: [] });
|
|
168
|
+
expect(view.host.textContent).toContain("Voiceover is also fading this.");
|
|
169
|
+
act(() => view.root.unmount());
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
// The same clip with an un-automated group must stay quiet — the note is
|
|
173
|
+
// only honest when the two curves actually multiply.
|
|
174
|
+
it("stays quiet when the group automates nothing", () => {
|
|
175
|
+
const automation = JSON.stringify({
|
|
176
|
+
version: 1,
|
|
177
|
+
lanes: [
|
|
178
|
+
{
|
|
179
|
+
target: "volume",
|
|
180
|
+
points: [
|
|
181
|
+
{ t: 0, v: 1 },
|
|
182
|
+
{ t: 2, v: 0.4 },
|
|
183
|
+
],
|
|
184
|
+
},
|
|
185
|
+
],
|
|
186
|
+
});
|
|
187
|
+
const clip: TimelineElement = {
|
|
188
|
+
...ELEMENT,
|
|
189
|
+
tag: "audio",
|
|
190
|
+
automation,
|
|
191
|
+
audioGroup: "voiceover",
|
|
192
|
+
audioGroupLabel: "Voiceover",
|
|
193
|
+
};
|
|
194
|
+
const view = renderHeader({ keyframeClip: clip, trackElements: [clip], animations: [] });
|
|
195
|
+
expect(view.host.textContent).not.toContain("is also fading this");
|
|
196
|
+
act(() => view.root.unmount());
|
|
197
|
+
});
|
|
198
|
+
|
|
125
199
|
// An expanded sub-composition child sits on the MASTER timeline at a
|
|
126
200
|
// host-absolute start, but its tweens are parsed from its own file and are
|
|
127
201
|
// local to it. Feeding the raw start straight into the clip-% math put every
|
|
@@ -180,6 +254,55 @@ describe("TimelineTrackHeader", () => {
|
|
|
180
254
|
act(() => view.root.unmount());
|
|
181
255
|
});
|
|
182
256
|
|
|
257
|
+
// The visibility control is the old hide eye. On an audio track it silences
|
|
258
|
+
// rather than hides, and the row already says so with a speaker elsewhere —
|
|
259
|
+
// so the eye's slot stays empty there. A non-audio track is untouched.
|
|
260
|
+
it("keeps the visibility control off audio track headers", () => {
|
|
261
|
+
const audio: TimelineElement = { ...ELEMENT, tag: "audio" };
|
|
262
|
+
const view = renderHeader({
|
|
263
|
+
keyframeClip: audio,
|
|
264
|
+
trackElements: [audio],
|
|
265
|
+
isAudioTrack: true,
|
|
266
|
+
animations: [],
|
|
267
|
+
});
|
|
268
|
+
const labels = Array.from(view.host.querySelectorAll("button")).map((b) =>
|
|
269
|
+
b.getAttribute("aria-label"),
|
|
270
|
+
);
|
|
271
|
+
expect(labels.some((l) => l && /^(Hide|Show) track/.test(l))).toBe(false);
|
|
272
|
+
expect(labels).not.toContain("Mute");
|
|
273
|
+
act(() => view.root.unmount());
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
// The escape hatch. `data-hidden` on audio silences it in preview and drops it
|
|
277
|
+
// from the render; the panel's "Muted" is the unrelated HTML `muted`
|
|
278
|
+
// attribute, and nothing else writes it. Withholding the eye unconditionally
|
|
279
|
+
// meant a track hidden by "Hide all" (or by hand, or before that rule existed)
|
|
280
|
+
// was silent with no control anywhere to bring it back.
|
|
281
|
+
it("offers the eye on an audio track that is already hidden, so it can be restored", () => {
|
|
282
|
+
const audio: TimelineElement = { ...ELEMENT, tag: "audio" };
|
|
283
|
+
const view = renderHeader({
|
|
284
|
+
keyframeClip: audio,
|
|
285
|
+
trackElements: [audio],
|
|
286
|
+
isAudioTrack: true,
|
|
287
|
+
isTrackHidden: true,
|
|
288
|
+
animations: [],
|
|
289
|
+
});
|
|
290
|
+
const labels = Array.from(view.host.querySelectorAll("button")).map((b) =>
|
|
291
|
+
b.getAttribute("aria-label"),
|
|
292
|
+
);
|
|
293
|
+
expect(labels.some((l) => l && /^Show track/.test(l))).toBe(true);
|
|
294
|
+
act(() => view.root.unmount());
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
it("keeps it on a non-audio track", () => {
|
|
298
|
+
const view = renderHeader({ isAudioTrack: false });
|
|
299
|
+
const labels = Array.from(view.host.querySelectorAll("button")).map((b) =>
|
|
300
|
+
b.getAttribute("aria-label"),
|
|
301
|
+
);
|
|
302
|
+
expect(labels.some((l) => l && /^Hide track/.test(l))).toBe(true);
|
|
303
|
+
act(() => view.root.unmount());
|
|
304
|
+
});
|
|
305
|
+
|
|
183
306
|
// The eye acts on the layer, so it has to be reachable without a pointer and
|
|
184
307
|
// in every disclosure state — a hover-gated eye is unusable by keyboard.
|
|
185
308
|
it("keeps the visibility eye mounted whether the layer is expanded or collapsed", () => {
|
|
@@ -614,7 +737,9 @@ describe("TimelineTrackHeader", () => {
|
|
|
614
737
|
it("names the header for the track, not for one of the clips on it", () => {
|
|
615
738
|
const view = renderHeader({ ...ROW, keyframeClip: NARRATION_2 });
|
|
616
739
|
expect(view.host.textContent).not.toContain("narration-2");
|
|
617
|
-
|
|
740
|
+
// Asserted on the rendered text, not a `title`: the name wraps now rather
|
|
741
|
+
// than truncating, so it no longer carries a tooltip to be found by.
|
|
742
|
+
expect(view.host.textContent).toContain("Track 1");
|
|
618
743
|
// Alone on the track it is still named for itself.
|
|
619
744
|
view.rerender({
|
|
620
745
|
...ROW,
|
|
@@ -622,7 +747,167 @@ describe("TimelineTrackHeader", () => {
|
|
|
622
747
|
trackElements: [NARRATION_2],
|
|
623
748
|
clipCount: 1,
|
|
624
749
|
});
|
|
625
|
-
expect(view.host.
|
|
750
|
+
expect(view.host.textContent).toContain("narration-2");
|
|
751
|
+
act(() => view.root.unmount());
|
|
752
|
+
});
|
|
753
|
+
});
|
|
754
|
+
|
|
755
|
+
describe("audio ids and canary gates", () => {
|
|
756
|
+
const VOICE: TimelineElement = {
|
|
757
|
+
id: "voice-1",
|
|
758
|
+
key: "index.html#voice-1",
|
|
759
|
+
domId: "voice-1",
|
|
760
|
+
tag: "audio",
|
|
761
|
+
start: 0,
|
|
762
|
+
duration: 5,
|
|
763
|
+
track: 0,
|
|
764
|
+
};
|
|
765
|
+
const VOICE_2: TimelineElement = {
|
|
766
|
+
...VOICE,
|
|
767
|
+
id: "voice-2",
|
|
768
|
+
key: "index.html#voice-2",
|
|
769
|
+
domId: "voice-2",
|
|
770
|
+
};
|
|
771
|
+
|
|
772
|
+
// A member row is `aria-level="2"`, and without this it looked identical to
|
|
773
|
+
// every top-level row — the nesting existed for a screen reader and not for
|
|
774
|
+
// an eye. B2's design called for the accent rail; only the semantics shipped.
|
|
775
|
+
it("indents a group member's row and gives it the accent rail", () => {
|
|
776
|
+
const view = renderHeader({
|
|
777
|
+
keyframeClip: VOICE,
|
|
778
|
+
animations: [],
|
|
779
|
+
expanded: false,
|
|
780
|
+
isAudioTrack: true,
|
|
781
|
+
});
|
|
782
|
+
const header = () => view.host.querySelector<HTMLElement>('[role="rowheader"]');
|
|
783
|
+
|
|
784
|
+
expect(header()?.style.paddingLeft).toBe("");
|
|
785
|
+
expect(header()?.style.borderLeft).toBe("");
|
|
786
|
+
expect(header()?.style.background).not.toContain("linear-gradient");
|
|
787
|
+
|
|
788
|
+
view.rerender({
|
|
789
|
+
keyframeClip: VOICE,
|
|
790
|
+
animations: [],
|
|
791
|
+
expanded: false,
|
|
792
|
+
isAudioTrack: true,
|
|
793
|
+
isGroupMember: true,
|
|
794
|
+
});
|
|
795
|
+
expect(header()?.style.paddingLeft).toBe("14px");
|
|
796
|
+
expect(header()?.style.borderLeft).toContain("2px");
|
|
797
|
+
// And a lighter gutter, so the row reads as sitting INSIDE its group
|
|
798
|
+
// rather than beside it. Overlaid on the theme's own fill rather than a
|
|
799
|
+
// hard-coded colour, so it follows whatever the gutter is.
|
|
800
|
+
expect(header()?.style.background).toContain("linear-gradient");
|
|
801
|
+
act(() => view.root.unmount());
|
|
802
|
+
});
|
|
803
|
+
|
|
804
|
+
it("offers the FX button on every audio track", () => {
|
|
805
|
+
const view = renderHeader({
|
|
806
|
+
keyframeClip: VOICE,
|
|
807
|
+
animations: [],
|
|
808
|
+
expanded: false,
|
|
809
|
+
isAudioTrack: true,
|
|
810
|
+
});
|
|
811
|
+
expect(view.host.querySelector('button[aria-label="Effects"]')).not.toBeNull();
|
|
812
|
+
act(() => view.root.unmount());
|
|
813
|
+
});
|
|
814
|
+
|
|
815
|
+
// A visual track has no chain to open, so the button must not follow the
|
|
816
|
+
// header onto every row.
|
|
817
|
+
it("withholds the FX button from a non-audio track", () => {
|
|
818
|
+
const view = renderHeader({
|
|
819
|
+
keyframeClip: ELEMENT,
|
|
820
|
+
animations: [],
|
|
821
|
+
expanded: false,
|
|
822
|
+
});
|
|
823
|
+
expect(view.host.querySelector('button[aria-label="Effects"]')).toBeNull();
|
|
824
|
+
act(() => view.root.unmount());
|
|
825
|
+
});
|
|
826
|
+
|
|
827
|
+
// A chain belongs to ONE bus, so a track carrying several ungrouped clips
|
|
828
|
+
// gets the pointer instead of the FX button — group first, then mix.
|
|
829
|
+
it("shows the group pointer on a multi-clip ungrouped audio track", () => {
|
|
830
|
+
const opts = {
|
|
831
|
+
keyframeClip: VOICE,
|
|
832
|
+
trackElements: [VOICE, VOICE_2],
|
|
833
|
+
clipCount: 2,
|
|
834
|
+
animations: [],
|
|
835
|
+
expanded: false,
|
|
836
|
+
isAudioTrack: true,
|
|
837
|
+
};
|
|
838
|
+
const pointer = (host: HTMLElement) =>
|
|
839
|
+
host.querySelector('button[aria-label="Effects — group these clips first"]');
|
|
840
|
+
const view = renderHeader(opts);
|
|
841
|
+
expect(pointer(view.host)).not.toBeNull();
|
|
842
|
+
// One clip needs no grouping — the real FX button takes its place.
|
|
843
|
+
view.rerender({ ...opts, trackElements: [VOICE], clipCount: 1 });
|
|
844
|
+
expect(pointer(view.host)).toBeNull();
|
|
845
|
+
expect(view.host.querySelector('button[aria-label="Effects"]')).not.toBeNull();
|
|
846
|
+
act(() => view.root.unmount());
|
|
847
|
+
});
|
|
848
|
+
|
|
849
|
+
// The header is a 48px column of exactly TWO lines — what the row is, then
|
|
850
|
+
// what you can do to it. The group pointer used to render as a sibling of
|
|
851
|
+
// both, making a third: 17 + 24 + 24 + gaps in a 48px box, which
|
|
852
|
+
// `justify-center` then spilled evenly out of the top and bottom. The name
|
|
853
|
+
// rode 10px above its own row and the pointer collided with the row below.
|
|
854
|
+
// The header GROWS by AUTOMATION_LANE_H for every open lane, and the lanes
|
|
855
|
+
// are absolutely positioned from its top. `justify-center` on the header
|
|
856
|
+
// itself therefore centred the two lines in the FULL height, so opening a
|
|
857
|
+
// lane pushed the name and its controls down on top of the lane rows.
|
|
858
|
+
it("pins the two lines to the top TRACK_H, whatever the header grows to", () => {
|
|
859
|
+
const automated: TimelineElement = {
|
|
860
|
+
...VOICE,
|
|
861
|
+
automation: JSON.stringify({
|
|
862
|
+
version: 1,
|
|
863
|
+
lanes: [{ target: "volume", points: [{ t: 0, v: 1 }] }],
|
|
864
|
+
}),
|
|
865
|
+
};
|
|
866
|
+
const view = renderHeader({
|
|
867
|
+
keyframeClip: automated,
|
|
868
|
+
trackElements: [automated],
|
|
869
|
+
clipCount: 1,
|
|
870
|
+
animations: [],
|
|
871
|
+
expanded: true,
|
|
872
|
+
isAudioTrack: true,
|
|
873
|
+
});
|
|
874
|
+
const header = view.host.querySelector<HTMLElement>('[role="rowheader"]');
|
|
875
|
+
const lines = header?.children[0] as HTMLElement | undefined;
|
|
876
|
+
expect(lines?.style.height).toBe(`${TRACK_H}px`);
|
|
877
|
+
// The lane row is a sibling of the wrapper, not inside it — it stacks
|
|
878
|
+
// BELOW the two lines rather than sharing their box.
|
|
879
|
+
expect((header?.children.length ?? 0) > 1).toBe(true);
|
|
880
|
+
act(() => view.root.unmount());
|
|
881
|
+
});
|
|
882
|
+
|
|
883
|
+
// The header is ONE line: name, clip count, then every control anchored to
|
|
884
|
+
// the right edge. It was two — a name line and a control line — which is
|
|
885
|
+
// what let a stray third child overflow the 48px box; now there is a single
|
|
886
|
+
// row and the controls share one right-aligned group.
|
|
887
|
+
it("keeps the name and every control on one line, controls to the right", () => {
|
|
888
|
+
const view = renderHeader({
|
|
889
|
+
keyframeClip: VOICE,
|
|
890
|
+
trackElements: [VOICE, VOICE_2],
|
|
891
|
+
clipCount: 2,
|
|
892
|
+
animations: [],
|
|
893
|
+
expanded: false,
|
|
894
|
+
isAudioTrack: true,
|
|
895
|
+
});
|
|
896
|
+
const header = view.host.querySelector<HTMLElement>('[role="rowheader"]');
|
|
897
|
+
// One TRACK_H-tall wrapper holding one line.
|
|
898
|
+
const lines = header?.children[0];
|
|
899
|
+
expect(lines?.children).toHaveLength(1);
|
|
900
|
+
// The controls live in a right-anchored group at the end of that line,
|
|
901
|
+
// after the name and the clip count — `ml-auto` is what holds the edge.
|
|
902
|
+
const line = lines?.children[0];
|
|
903
|
+
const controls = line?.lastElementChild as HTMLElement | null;
|
|
904
|
+
expect(controls?.className).toContain("ml-auto");
|
|
905
|
+
expect(
|
|
906
|
+
controls?.querySelector('button[aria-label="Effects — group these clips first"]'),
|
|
907
|
+
).not.toBeNull();
|
|
908
|
+
// And the clip count is beside the name, not out with the controls.
|
|
909
|
+
expect(controls?.querySelector('[aria-label="2 clips"]')).toBeNull();
|
|
910
|
+
expect(line?.querySelector('[aria-label="2 clips"]')).not.toBeNull();
|
|
626
911
|
act(() => view.root.unmount());
|
|
627
912
|
});
|
|
628
913
|
});
|