@hyperframes/studio 0.7.107 → 0.7.108
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-BLrfwq5o.js → hyperframes-player-QqavTWRK.js} +1 -1
- package/dist/assets/{index-BA9yhzfR.js → index-BQjXA1pz.js} +1 -1
- package/dist/assets/{index-BSBk5srs.js → index-BhHHMprP.js} +1 -1
- package/dist/assets/index-DNkh9mbV.css +1 -0
- package/dist/assets/index-GqONLcOl.js +428 -0
- package/dist/index.d.ts +85 -2
- package/dist/index.html +2 -2
- package/dist/index.js +17696 -12512
- package/dist/index.js.map +1 -1
- package/package.json +7 -7
- package/src/components/StudioRightPanel.tsx +18 -1
- package/src/components/TimelineToolbar.test.tsx +46 -0
- package/src/components/TimelineToolbar.tsx +17 -0
- package/src/components/editor/PropertyPanel.test.tsx +5 -1
- package/src/components/editor/PropertyPanel.tsx +7 -23
- package/src/components/editor/PropertyPanelFlat.tsx +25 -0
- package/src/components/editor/audioFxSummary.test.ts +75 -0
- package/src/components/editor/audioFxSummary.ts +36 -0
- package/src/components/editor/audioFxTelemetry.test.ts +129 -0
- package/src/components/editor/audioFxTelemetry.ts +230 -0
- package/src/components/editor/propertyPanelAudioFxGroup.test.tsx +1830 -0
- package/src/components/editor/propertyPanelAudioFxGroup.tsx +299 -0
- package/src/components/editor/propertyPanelAudioFxGroupUtils.ts +21 -0
- package/src/components/editor/propertyPanelAutomation.test.ts +60 -0
- package/src/components/editor/propertyPanelAutomation.ts +97 -0
- package/src/components/editor/propertyPanelFlatMediaSection.tsx +37 -9
- package/src/components/editor/propertyPanelFlatProps.ts +1 -0
- package/src/components/editor/propertyPanelFxAddMenu.tsx +209 -0
- package/src/components/editor/propertyPanelFxBandRuler.tsx +72 -0
- package/src/components/editor/propertyPanelFxCarveModule.tsx +382 -0
- package/src/components/editor/propertyPanelFxControls.test.tsx +115 -0
- package/src/components/editor/propertyPanelFxControls.tsx +356 -0
- package/src/components/editor/propertyPanelFxEqModule.tsx +196 -0
- package/src/components/editor/propertyPanelFxFamily.ts +83 -0
- package/src/components/editor/propertyPanelFxNodeOpenBody.tsx +269 -0
- package/src/components/editor/propertyPanelFxNodeParams.tsx +112 -0
- package/src/components/editor/propertyPanelFxNodeRow.tsx +339 -0
- package/src/components/editor/propertyPanelFxPresetMenu.tsx +159 -0
- package/src/components/editor/propertyPanelFxPresetRun.tsx +215 -0
- package/src/components/editor/propertyPanelFxPresetStyle.test.ts +142 -0
- package/src/components/editor/propertyPanelFxPresetStyle.ts +216 -0
- package/src/components/editor/propertyPanelFxRackChain.tsx +202 -0
- package/src/components/editor/propertyPanelFxSection.test.tsx +1747 -0
- package/src/components/editor/propertyPanelFxSection.tsx +550 -0
- package/src/components/editor/propertyPanelFxSectionTypes.ts +91 -0
- package/src/components/editor/propertyPanelTypes.ts +5 -0
- package/src/components/editor/useFxAudition.ts +95 -0
- package/src/components/editor/useFxCarve.ts +0 -0
- package/src/components/editor/useFxChainObserved.ts +83 -0
- package/src/components/editor/useFxLevelling.ts +235 -0
- package/src/components/editor/useVolumeAutomation.test.tsx +122 -0
- package/src/components/editor/useVolumeAutomation.ts +54 -0
- package/src/contexts/DomEditContext.tsx +11 -0
- package/src/hooks/useAppHotkeys.test.ts +198 -0
- package/src/hooks/useAppHotkeys.ts +30 -3
- package/src/hooks/useAutomationSelectionKeyboard.test.tsx +429 -0
- package/src/hooks/useAutomationSelectionKeyboard.ts +371 -0
- package/src/hooks/useDomEditAttributeCommits.ts +90 -10
- package/src/hooks/useDomEditCommits.test.tsx +38 -0
- package/src/hooks/useDomEditCommits.ts +2 -0
- package/src/hooks/useDomEditSession.ts +2 -0
- package/src/hooks/useDomEditTextCommits.ts +2 -0
- package/src/hooks/useElementLifecycleOps.ts +3 -0
- package/src/hooks/useLivePlayheadTime.ts +49 -0
- package/src/hooks/usePreviewPersistence.ts +7 -0
- package/src/hooks/useTimelineEditing.ts +3 -0
- package/src/player/components/AutomationSelectionMenu.tsx +76 -0
- package/src/player/components/AutomationValueInput.tsx +51 -0
- package/src/player/components/LayerDisclosureRow.tsx +5 -4
- package/src/player/components/Timeline.test.ts +84 -0
- package/src/player/components/TimelineAutomationLane.test.tsx +1720 -0
- package/src/player/components/TimelineAutomationLane.tsx +674 -0
- package/src/player/components/TimelineAutomationLaneSlot.test.tsx +193 -0
- package/src/player/components/TimelineLanes.test.tsx +24 -5
- package/src/player/components/TimelineLanes.tsx +142 -154
- package/src/player/components/TimelineTrackHeader.test.tsx +212 -1
- package/src/player/components/TimelineTrackHeader.tsx +166 -6
- package/src/player/components/TimelineTrackRow.tsx +8 -1
- package/src/player/components/automationClipboard.test.ts +94 -0
- package/src/player/components/automationClipboard.ts +136 -0
- package/src/player/components/automationGestureKeys.test.ts +49 -0
- package/src/player/components/automationGestureKeys.ts +49 -0
- package/src/player/components/automationLaneData.test.ts +270 -0
- package/src/player/components/automationLaneData.ts +258 -0
- package/src/player/components/automationLaneDragMath.ts +194 -0
- package/src/player/components/automationLaneGeometry.test.ts +239 -0
- package/src/player/components/automationLaneGeometry.ts +287 -0
- package/src/player/components/automationLaneHeight.ts +15 -0
- package/src/player/components/automationLanePointer.ts +20 -0
- package/src/player/components/automationLaneSelection.test.ts +183 -0
- package/src/player/components/automationLaneSelection.ts +135 -0
- package/src/player/components/automationShapes.test.ts +82 -0
- package/src/player/components/automationShapes.ts +70 -0
- package/src/player/components/automationSimplify.test.ts +48 -0
- package/src/player/components/automationSimplify.ts +51 -0
- package/src/player/components/timelineClipGestureHandlers.ts +223 -0
- package/src/player/components/timelineLaneProps.ts +19 -0
- package/src/player/components/timelineLayout.ts +11 -5
- package/src/player/components/useAutomationEdgeStretch.test.ts +27 -0
- package/src/player/components/useAutomationEdgeStretch.ts +269 -0
- package/src/player/components/useAutomationLaneGestures.ts +537 -0
- package/src/player/components/useAutomationLanes.test.tsx +112 -0
- package/src/player/components/useAutomationLanes.ts +170 -0
- package/src/player/components/useAutomationRangeDrag.ts +114 -0
- package/src/player/components/useTimelineTrackLayout.test.ts +107 -1
- package/src/player/components/useTimelineTrackLayout.ts +83 -7
- package/src/player/hooks/useExpandedTimelineElements.ts +7 -0
- package/src/player/hooks/useTimelinePlayer.ts +13 -1
- package/src/player/lib/automationStoreSync.test.ts +83 -0
- package/src/player/lib/automationStoreSync.ts +56 -0
- package/src/player/lib/timelineDOM.test.ts +33 -0
- package/src/player/lib/timelineDOM.ts +12 -0
- package/src/player/store/automationSelectionSlice.test.ts +30 -0
- package/src/player/store/automationSelectionSlice.ts +54 -0
- package/src/player/store/playerStore.test.ts +15 -0
- package/src/player/store/playerStore.ts +54 -65
- package/src/player/store/timelineElement.ts +78 -0
- package/src/styles/studio.css +89 -0
- package/src/telemetry/agentRuntime.test.ts +45 -0
- package/src/telemetry/agentRuntime.ts +62 -0
- package/src/telemetry/system.ts +8 -0
- package/src/utils/studioFileHistory.test.ts +49 -0
- package/src/utils/studioFileHistory.ts +17 -1
- package/src/utils/studioTelemetry.ts +16 -10
- package/dist/assets/index-dF-CmLZu.js +0 -428
- package/dist/assets/index-zQ4JFwwB.css +0 -1
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
import { trackStudioEvent } from "../utils/studioTelemetry";
|
|
15
15
|
import { applyUndoRestoreToPreview, type UndoRestoreFile } from "../utils/gsapUndoRestore";
|
|
16
16
|
import { usePlayerStore } from "../player";
|
|
17
|
+
import { syncStoredAutomationFromPreview } from "../player/lib/automationStoreSync";
|
|
17
18
|
|
|
18
19
|
/** The restore payload the undo/redo preview-sync consumes (from the history store). */
|
|
19
20
|
interface HistoryPreviewRestore {
|
|
@@ -207,7 +208,13 @@ export function usePreviewPersistence({
|
|
|
207
208
|
player.setElements([]);
|
|
208
209
|
player.setSelectedElementId(null);
|
|
209
210
|
player.setTimelineReady(false);
|
|
211
|
+
return;
|
|
210
212
|
}
|
|
213
|
+
// A soft restore patched the reverted attributes onto the live preview, but the
|
|
214
|
+
// player store keeps its own copy and that copy is what the automation lanes
|
|
215
|
+
// draw — so without this an undone envelope edit stayed invisible until a
|
|
216
|
+
// reload. The full path above clears the store and waits for discovery instead.
|
|
217
|
+
syncStoredAutomationFromPreview(previewIframeRef.current?.contentDocument ?? null);
|
|
211
218
|
},
|
|
212
219
|
[previewIframeRef, activeCompPathRef, reloadPreview],
|
|
213
220
|
);
|
|
@@ -449,6 +449,9 @@ export function useTimelineEditing({
|
|
|
449
449
|
kind: "timeline",
|
|
450
450
|
files: { [targetPath]: patchedContent },
|
|
451
451
|
readFile: async () => originalContent,
|
|
452
|
+
// remove-element already wrote the removal, so disk holds THAT — not the
|
|
453
|
+
// content read at the top. Undo still goes back to the original.
|
|
454
|
+
diskContent: { [targetPath]: removedContent },
|
|
452
455
|
writeFile: writeProjectFile,
|
|
453
456
|
recordEdit,
|
|
454
457
|
});
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Context menu for a right-click inside an automation time selection: the four
|
|
3
|
+
* utility shapes, then Simplify. Portal + dismiss handling mirror
|
|
4
|
+
* TrackGapContextMenu; rows never vanish — an inapplicable Simplify dims with
|
|
5
|
+
* a reason instead of leaving a shorter menu.
|
|
6
|
+
*/
|
|
7
|
+
import { memo } from "react";
|
|
8
|
+
import { createPortal } from "react-dom";
|
|
9
|
+
import { useContextMenuDismiss } from "../../hooks/useContextMenuDismiss";
|
|
10
|
+
import { AUTOMATION_SHAPES, type AutomationShapeId } from "./automationShapes";
|
|
11
|
+
|
|
12
|
+
interface AutomationSelectionMenuProps {
|
|
13
|
+
x: number;
|
|
14
|
+
y: number;
|
|
15
|
+
onClose(): void;
|
|
16
|
+
onInsertShape(shape: AutomationShapeId): void;
|
|
17
|
+
onSimplify(): void;
|
|
18
|
+
/** At least three points in the range — fewer has nothing to thin. */
|
|
19
|
+
canSimplify: boolean;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export const AutomationSelectionMenu = memo(function AutomationSelectionMenu({
|
|
23
|
+
x,
|
|
24
|
+
y,
|
|
25
|
+
onClose,
|
|
26
|
+
onInsertShape,
|
|
27
|
+
onSimplify,
|
|
28
|
+
canSimplify,
|
|
29
|
+
}: AutomationSelectionMenuProps) {
|
|
30
|
+
const menuRef = useContextMenuDismiss(onClose);
|
|
31
|
+
const row =
|
|
32
|
+
"block w-full px-2 py-1 text-left text-[11px] text-panel-text-1 hover:bg-panel-bg-3 disabled:opacity-40";
|
|
33
|
+
// Same edge-clamping precedent as TrackGapContextMenu: without it a
|
|
34
|
+
// right-click near the bottom/right of the timeline renders this menu
|
|
35
|
+
// partially off-screen.
|
|
36
|
+
const menuWidth = 140;
|
|
37
|
+
const menuHeight = AUTOMATION_SHAPES.length * 24 + 32;
|
|
38
|
+
const overflowY = y + menuHeight - window.innerHeight;
|
|
39
|
+
const adjustedX = x + menuWidth > window.innerWidth ? x - menuWidth : x;
|
|
40
|
+
const adjustedY = overflowY > 0 ? y - overflowY - 8 : y;
|
|
41
|
+
return createPortal(
|
|
42
|
+
<div
|
|
43
|
+
ref={menuRef}
|
|
44
|
+
className="hf-automation-menu fixed z-50 min-w-[140px] rounded border border-panel-border-input bg-panel-bg-2 py-1 shadow-lg"
|
|
45
|
+
style={{ left: adjustedX, top: adjustedY }}
|
|
46
|
+
>
|
|
47
|
+
{AUTOMATION_SHAPES.map((shape) => (
|
|
48
|
+
<button
|
|
49
|
+
key={shape.id}
|
|
50
|
+
type="button"
|
|
51
|
+
className={row}
|
|
52
|
+
onClick={() => {
|
|
53
|
+
onInsertShape(shape.id);
|
|
54
|
+
onClose();
|
|
55
|
+
}}
|
|
56
|
+
>
|
|
57
|
+
{shape.label}
|
|
58
|
+
</button>
|
|
59
|
+
))}
|
|
60
|
+
<div className="my-1 border-t border-panel-border-input" />
|
|
61
|
+
<button
|
|
62
|
+
type="button"
|
|
63
|
+
className={row}
|
|
64
|
+
disabled={!canSimplify}
|
|
65
|
+
title={canSimplify ? undefined : "Fewer than three points in the selection"}
|
|
66
|
+
onClick={() => {
|
|
67
|
+
onSimplify();
|
|
68
|
+
onClose();
|
|
69
|
+
}}
|
|
70
|
+
>
|
|
71
|
+
Simplify
|
|
72
|
+
</button>
|
|
73
|
+
</div>,
|
|
74
|
+
document.body,
|
|
75
|
+
);
|
|
76
|
+
});
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Typing an exact value into a breakpoint.
|
|
3
|
+
*
|
|
4
|
+
* Dragging is how an envelope is shaped, but it cannot land on a number: -6.0 dB
|
|
5
|
+
* is not a pixel you can find. This is the lane's keyboard route to one value,
|
|
6
|
+
* kept in its own file so the lane component stays about the pointer.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
export interface AutomationValueInputProps {
|
|
10
|
+
text: string;
|
|
11
|
+
/** Left edge in the lane's own coordinates. */
|
|
12
|
+
leftPx: number;
|
|
13
|
+
label: string;
|
|
14
|
+
onChange(text: string): void;
|
|
15
|
+
/** Enter or blur: apply what was typed. */
|
|
16
|
+
onCommit(): void;
|
|
17
|
+
/** Escape: leave the point where it was. */
|
|
18
|
+
onCancel(): void;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function AutomationValueInput({
|
|
22
|
+
text,
|
|
23
|
+
leftPx,
|
|
24
|
+
label,
|
|
25
|
+
onChange,
|
|
26
|
+
onCommit,
|
|
27
|
+
onCancel,
|
|
28
|
+
}: AutomationValueInputProps) {
|
|
29
|
+
return (
|
|
30
|
+
<input
|
|
31
|
+
// pointer-events-auto: the lane band around it takes none, so that clips
|
|
32
|
+
// sharing the row do not cover each other's envelopes (see the lane).
|
|
33
|
+
className="hf-automation-value pointer-events-auto absolute rounded-[3px] border border-panel-border-input bg-panel-bg-2 px-1 font-mono text-[9px] text-panel-text-1"
|
|
34
|
+
style={{ left: leftPx, top: 1, width: 44, zIndex: 4 }}
|
|
35
|
+
// The lane is a pointer surface, and the timeline above it owns single-key
|
|
36
|
+
// shortcuts. Without stopping both, a press lands on the lane instead of
|
|
37
|
+
// the field and a typed "5" scrubs the transport.
|
|
38
|
+
onPointerDown={(e) => e.stopPropagation()}
|
|
39
|
+
onKeyDown={(e) => {
|
|
40
|
+
e.stopPropagation();
|
|
41
|
+
if (e.key === "Enter") onCommit();
|
|
42
|
+
if (e.key === "Escape") onCancel();
|
|
43
|
+
}}
|
|
44
|
+
onChange={(e) => onChange(e.target.value)}
|
|
45
|
+
onBlur={onCommit}
|
|
46
|
+
value={text}
|
|
47
|
+
aria-label={`${label} value`}
|
|
48
|
+
autoFocus
|
|
49
|
+
/>
|
|
50
|
+
);
|
|
51
|
+
}
|
|
@@ -1,12 +1,11 @@
|
|
|
1
1
|
import { CaretRight } from "@phosphor-icons/react";
|
|
2
|
-
import type { TimelineElement } from "../store/playerStore";
|
|
3
2
|
import { TRACK_H } from "./timelineLayout";
|
|
4
3
|
import { TrackClipCount } from "./TrackClipCount";
|
|
5
4
|
|
|
6
5
|
// Layer row (Figma order: disclosure ▸/▾, diamond, name) — the disclosure lives
|
|
7
6
|
// here, not on the clip bar, and re-expands a collapsed layer.
|
|
8
7
|
export function LayerDisclosureRow({
|
|
9
|
-
|
|
8
|
+
name,
|
|
10
9
|
clipCount,
|
|
11
10
|
isExpanded,
|
|
12
11
|
gutterBackground,
|
|
@@ -15,7 +14,10 @@ export function LayerDisclosureRow({
|
|
|
15
14
|
onToggleClipExpanded,
|
|
16
15
|
children,
|
|
17
16
|
}: {
|
|
18
|
-
|
|
17
|
+
/** What this row is called. The active clip's own name when it is alone on the
|
|
18
|
+
* track; the track itself once it holds several, since naming a shared row
|
|
19
|
+
* after one of its clips reads as if the rows under it were that clip's. */
|
|
20
|
+
name: string;
|
|
19
21
|
clipCount: number;
|
|
20
22
|
isExpanded: boolean;
|
|
21
23
|
gutterBackground: string;
|
|
@@ -31,7 +33,6 @@ export function LayerDisclosureRow({
|
|
|
31
33
|
/** Trailing controls that act on the LAYER (the visibility eye), not on a lane. */
|
|
32
34
|
children?: React.ReactNode;
|
|
33
35
|
}) {
|
|
34
|
-
const name = keyframeClip.label ?? keyframeClip.domId ?? keyframeClip.id;
|
|
35
36
|
return (
|
|
36
37
|
<div
|
|
37
38
|
className="absolute left-0 top-0 flex items-center gap-1.5 overflow-hidden px-1.5 text-[11px]"
|
|
@@ -37,6 +37,7 @@ import {
|
|
|
37
37
|
getTimelineLaneTop,
|
|
38
38
|
createTimelineRowGeometry,
|
|
39
39
|
} from "./timelineLayout";
|
|
40
|
+
import { AUTOMATION_LANE_H } from "./automationLaneHeight";
|
|
40
41
|
import { formatTime } from "../lib/time";
|
|
41
42
|
import { usePlayerStore } from "../store/playerStore";
|
|
42
43
|
import { TimelineEditProvider } from "../../contexts/TimelineEditContext";
|
|
@@ -578,6 +579,89 @@ describe("Timeline provider boundary", () => {
|
|
|
578
579
|
act(() => root.unmount());
|
|
579
580
|
});
|
|
580
581
|
|
|
582
|
+
// The caret belongs to the row, not to whichever clip on it is selected: the
|
|
583
|
+
// automation lanes below it are the track's, shared per property. Toggling one
|
|
584
|
+
// clip left the row's state depending on the selection, and a collapse that
|
|
585
|
+
// only dropped the active clip left the row stuck open.
|
|
586
|
+
it("expands and collapses every clip on a shared track together", () => {
|
|
587
|
+
const host = createSizedTimelineHost(720);
|
|
588
|
+
const automation = JSON.stringify({
|
|
589
|
+
version: 1,
|
|
590
|
+
lanes: [{ target: "volume", points: [{ t: 0, v: 1 }] }],
|
|
591
|
+
});
|
|
592
|
+
usePlayerStore.setState({
|
|
593
|
+
duration: 8,
|
|
594
|
+
timelineReady: true,
|
|
595
|
+
elements: [
|
|
596
|
+
{ id: "narration-1", tag: "audio", start: 0, duration: 4, track: 0, automation },
|
|
597
|
+
{ id: "narration-2", tag: "audio", start: 4, duration: 4, track: 0, automation },
|
|
598
|
+
],
|
|
599
|
+
});
|
|
600
|
+
const root = createRoot(host);
|
|
601
|
+
act(() => root.render(React.createElement(Timeline)));
|
|
602
|
+
|
|
603
|
+
const row = host.querySelector<HTMLElement>('[data-el-id="narration-1"]')?.parentElement
|
|
604
|
+
?.parentElement;
|
|
605
|
+
// A row of several clips is named for the track, so the caret is too.
|
|
606
|
+
const caret = () => host.querySelector<HTMLButtonElement>('button[aria-label$=" keyframes"]');
|
|
607
|
+
expect(caret()?.getAttribute("aria-label")).toBe("Expand Track 1 keyframes");
|
|
608
|
+
|
|
609
|
+
act(() => caret()?.click());
|
|
610
|
+
// One shared volume row, and BOTH clips hold it open.
|
|
611
|
+
expectTrackExpansion(row, ["narration-1", "narration-2"], TRACK_H + AUTOMATION_LANE_H);
|
|
612
|
+
|
|
613
|
+
// Every clip bar on the row is capped to one track height. Only the clip
|
|
614
|
+
// owning the property lanes used to be, so its siblings stretched the whole
|
|
615
|
+
// expanded row and painted their waveforms over the envelopes below.
|
|
616
|
+
expect(
|
|
617
|
+
["narration-1", "narration-2"].map(
|
|
618
|
+
(id) => host.querySelector<HTMLElement>(`[data-el-id="${id}"]`)?.style.height,
|
|
619
|
+
),
|
|
620
|
+
).toEqual([`${TRACK_H - 2 * CLIP_Y}px`, `${TRACK_H - 2 * CLIP_Y}px`]);
|
|
621
|
+
|
|
622
|
+
act(() => caret()?.click());
|
|
623
|
+
expectTrackExpansion(row, [], TRACK_H);
|
|
624
|
+
act(() => root.unmount());
|
|
625
|
+
});
|
|
626
|
+
|
|
627
|
+
// The lanes are the row's, and selecting a clip must not rebuild them. They
|
|
628
|
+
// used to hang off the active clip's property lanes, so clicking a sibling
|
|
629
|
+
// moved the whole subtree into a different element and remounted every lane —
|
|
630
|
+
// which threw away each one's hover state and any gesture in flight. Pressing
|
|
631
|
+
// a lane to select its clip therefore made the handles vanish under the
|
|
632
|
+
// pointer, which is the one gesture the read-only lane exists to support.
|
|
633
|
+
it("keeps the automation lanes mounted when the selection moves along the row", () => {
|
|
634
|
+
const host = createSizedTimelineHost(720);
|
|
635
|
+
const automation = JSON.stringify({
|
|
636
|
+
version: 1,
|
|
637
|
+
lanes: [{ target: "volume", points: [{ t: 0, v: 1 }] }],
|
|
638
|
+
});
|
|
639
|
+
usePlayerStore.setState({
|
|
640
|
+
duration: 8,
|
|
641
|
+
timelineReady: true,
|
|
642
|
+
selectedElementId: "narration-2",
|
|
643
|
+
elements: [
|
|
644
|
+
{ id: "narration-1", tag: "audio", start: 0, duration: 4, track: 0, automation },
|
|
645
|
+
{ id: "narration-2", tag: "audio", start: 4, duration: 4, track: 0, automation },
|
|
646
|
+
],
|
|
647
|
+
});
|
|
648
|
+
const root = createRoot(host);
|
|
649
|
+
act(() => root.render(React.createElement(Timeline)));
|
|
650
|
+
act(() => host.querySelector<HTMLButtonElement>('button[aria-label$=" keyframes"]')?.click());
|
|
651
|
+
|
|
652
|
+
const before = [...host.querySelectorAll(".hf-automation-lane")];
|
|
653
|
+
expect(before).toHaveLength(2);
|
|
654
|
+
|
|
655
|
+
act(() => usePlayerStore.setState({ selectedElementId: "narration-1" }));
|
|
656
|
+
|
|
657
|
+
// Identity, not deep equality: a remount produces structurally identical
|
|
658
|
+
// nodes, so only `toBe` can tell the two apart.
|
|
659
|
+
const after = [...host.querySelectorAll(".hf-automation-lane")];
|
|
660
|
+
expect(after).toHaveLength(before.length);
|
|
661
|
+
after.forEach((node, index) => expect(node).toBe(before[index]));
|
|
662
|
+
act(() => root.unmount());
|
|
663
|
+
});
|
|
664
|
+
|
|
581
665
|
it("marks every clip in selectedElementIds as selected", () => {
|
|
582
666
|
const host = createSizedTimelineHost(720);
|
|
583
667
|
|