@hyperframes/studio 0.7.107 → 0.7.109

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.
Files changed (126) hide show
  1. package/dist/assets/{hyperframes-player-BLrfwq5o.js → hyperframes-player-BYgqAPib.js} +1 -1
  2. package/dist/assets/{index-BSBk5srs.js → index-Blxia-Lt.js} +1 -1
  3. package/dist/assets/{index-BA9yhzfR.js → index-CIRxodzK.js} +1 -1
  4. package/dist/assets/index-CfBGQdrX.js +428 -0
  5. package/dist/assets/index-DNkh9mbV.css +1 -0
  6. package/dist/index.d.ts +85 -2
  7. package/dist/index.html +2 -2
  8. package/dist/index.js +17696 -12512
  9. package/dist/index.js.map +1 -1
  10. package/package.json +7 -7
  11. package/src/components/StudioRightPanel.tsx +18 -1
  12. package/src/components/TimelineToolbar.test.tsx +46 -0
  13. package/src/components/TimelineToolbar.tsx +17 -0
  14. package/src/components/editor/PropertyPanel.test.tsx +5 -1
  15. package/src/components/editor/PropertyPanel.tsx +7 -23
  16. package/src/components/editor/PropertyPanelFlat.tsx +25 -0
  17. package/src/components/editor/audioFxSummary.test.ts +75 -0
  18. package/src/components/editor/audioFxSummary.ts +36 -0
  19. package/src/components/editor/audioFxTelemetry.test.ts +129 -0
  20. package/src/components/editor/audioFxTelemetry.ts +230 -0
  21. package/src/components/editor/propertyPanelAudioFxGroup.test.tsx +1830 -0
  22. package/src/components/editor/propertyPanelAudioFxGroup.tsx +299 -0
  23. package/src/components/editor/propertyPanelAudioFxGroupUtils.ts +21 -0
  24. package/src/components/editor/propertyPanelAutomation.test.ts +60 -0
  25. package/src/components/editor/propertyPanelAutomation.ts +97 -0
  26. package/src/components/editor/propertyPanelFlatMediaSection.tsx +37 -9
  27. package/src/components/editor/propertyPanelFlatProps.ts +1 -0
  28. package/src/components/editor/propertyPanelFxAddMenu.tsx +209 -0
  29. package/src/components/editor/propertyPanelFxBandRuler.tsx +72 -0
  30. package/src/components/editor/propertyPanelFxCarveModule.tsx +382 -0
  31. package/src/components/editor/propertyPanelFxControls.test.tsx +115 -0
  32. package/src/components/editor/propertyPanelFxControls.tsx +356 -0
  33. package/src/components/editor/propertyPanelFxEqModule.tsx +196 -0
  34. package/src/components/editor/propertyPanelFxFamily.ts +83 -0
  35. package/src/components/editor/propertyPanelFxNodeOpenBody.tsx +269 -0
  36. package/src/components/editor/propertyPanelFxNodeParams.tsx +112 -0
  37. package/src/components/editor/propertyPanelFxNodeRow.tsx +339 -0
  38. package/src/components/editor/propertyPanelFxPresetMenu.tsx +159 -0
  39. package/src/components/editor/propertyPanelFxPresetRun.tsx +215 -0
  40. package/src/components/editor/propertyPanelFxPresetStyle.test.ts +142 -0
  41. package/src/components/editor/propertyPanelFxPresetStyle.ts +216 -0
  42. package/src/components/editor/propertyPanelFxRackChain.tsx +202 -0
  43. package/src/components/editor/propertyPanelFxSection.test.tsx +1747 -0
  44. package/src/components/editor/propertyPanelFxSection.tsx +550 -0
  45. package/src/components/editor/propertyPanelFxSectionTypes.ts +91 -0
  46. package/src/components/editor/propertyPanelTypes.ts +5 -0
  47. package/src/components/editor/useFxAudition.ts +95 -0
  48. package/src/components/editor/useFxCarve.ts +0 -0
  49. package/src/components/editor/useFxChainObserved.ts +83 -0
  50. package/src/components/editor/useFxLevelling.ts +235 -0
  51. package/src/components/editor/useVolumeAutomation.test.tsx +122 -0
  52. package/src/components/editor/useVolumeAutomation.ts +54 -0
  53. package/src/contexts/DomEditContext.tsx +11 -0
  54. package/src/hooks/useAppHotkeys.test.ts +198 -0
  55. package/src/hooks/useAppHotkeys.ts +30 -3
  56. package/src/hooks/useAutomationSelectionKeyboard.test.tsx +429 -0
  57. package/src/hooks/useAutomationSelectionKeyboard.ts +371 -0
  58. package/src/hooks/useDomEditAttributeCommits.ts +90 -10
  59. package/src/hooks/useDomEditCommits.test.tsx +38 -0
  60. package/src/hooks/useDomEditCommits.ts +2 -0
  61. package/src/hooks/useDomEditSession.ts +2 -0
  62. package/src/hooks/useDomEditTextCommits.ts +2 -0
  63. package/src/hooks/useElementLifecycleOps.ts +3 -0
  64. package/src/hooks/useLivePlayheadTime.ts +49 -0
  65. package/src/hooks/usePreviewPersistence.ts +7 -0
  66. package/src/hooks/useTimelineEditing.ts +3 -0
  67. package/src/player/components/AutomationSelectionMenu.tsx +76 -0
  68. package/src/player/components/AutomationValueInput.tsx +51 -0
  69. package/src/player/components/LayerDisclosureRow.tsx +5 -4
  70. package/src/player/components/Timeline.test.ts +84 -0
  71. package/src/player/components/TimelineAutomationLane.test.tsx +1720 -0
  72. package/src/player/components/TimelineAutomationLane.tsx +674 -0
  73. package/src/player/components/TimelineAutomationLaneSlot.test.tsx +193 -0
  74. package/src/player/components/TimelineLanes.test.tsx +24 -5
  75. package/src/player/components/TimelineLanes.tsx +142 -154
  76. package/src/player/components/TimelineTrackHeader.test.tsx +212 -1
  77. package/src/player/components/TimelineTrackHeader.tsx +166 -6
  78. package/src/player/components/TimelineTrackRow.tsx +8 -1
  79. package/src/player/components/automationClipboard.test.ts +94 -0
  80. package/src/player/components/automationClipboard.ts +136 -0
  81. package/src/player/components/automationGestureKeys.test.ts +49 -0
  82. package/src/player/components/automationGestureKeys.ts +49 -0
  83. package/src/player/components/automationLaneData.test.ts +270 -0
  84. package/src/player/components/automationLaneData.ts +258 -0
  85. package/src/player/components/automationLaneDragMath.ts +194 -0
  86. package/src/player/components/automationLaneGeometry.test.ts +239 -0
  87. package/src/player/components/automationLaneGeometry.ts +287 -0
  88. package/src/player/components/automationLaneHeight.ts +15 -0
  89. package/src/player/components/automationLanePointer.ts +20 -0
  90. package/src/player/components/automationLaneSelection.test.ts +183 -0
  91. package/src/player/components/automationLaneSelection.ts +135 -0
  92. package/src/player/components/automationShapes.test.ts +82 -0
  93. package/src/player/components/automationShapes.ts +70 -0
  94. package/src/player/components/automationSimplify.test.ts +48 -0
  95. package/src/player/components/automationSimplify.ts +51 -0
  96. package/src/player/components/timelineClipGestureHandlers.ts +223 -0
  97. package/src/player/components/timelineLaneProps.ts +19 -0
  98. package/src/player/components/timelineLayout.ts +11 -5
  99. package/src/player/components/useAutomationEdgeStretch.test.ts +27 -0
  100. package/src/player/components/useAutomationEdgeStretch.ts +269 -0
  101. package/src/player/components/useAutomationLaneGestures.ts +537 -0
  102. package/src/player/components/useAutomationLanes.test.tsx +112 -0
  103. package/src/player/components/useAutomationLanes.ts +170 -0
  104. package/src/player/components/useAutomationRangeDrag.ts +114 -0
  105. package/src/player/components/useTimelineTrackLayout.test.ts +107 -1
  106. package/src/player/components/useTimelineTrackLayout.ts +83 -7
  107. package/src/player/hooks/useExpandedTimelineElements.ts +7 -0
  108. package/src/player/hooks/useTimelinePlayer.ts +13 -1
  109. package/src/player/lib/automationStoreSync.test.ts +83 -0
  110. package/src/player/lib/automationStoreSync.ts +56 -0
  111. package/src/player/lib/timelineDOM.test.ts +33 -0
  112. package/src/player/lib/timelineDOM.ts +12 -0
  113. package/src/player/store/automationSelectionSlice.test.ts +30 -0
  114. package/src/player/store/automationSelectionSlice.ts +54 -0
  115. package/src/player/store/playerStore.test.ts +15 -0
  116. package/src/player/store/playerStore.ts +54 -65
  117. package/src/player/store/timelineElement.ts +78 -0
  118. package/src/styles/studio.css +89 -0
  119. package/src/telemetry/agentRuntime.test.ts +45 -0
  120. package/src/telemetry/agentRuntime.ts +62 -0
  121. package/src/telemetry/system.ts +8 -0
  122. package/src/utils/studioFileHistory.test.ts +49 -0
  123. package/src/utils/studioFileHistory.ts +17 -1
  124. package/src/utils/studioTelemetry.ts +16 -10
  125. package/dist/assets/index-dF-CmLZu.js +0 -428
  126. package/dist/assets/index-zQ4JFwwB.css +0 -1
@@ -0,0 +1,198 @@
1
+ // @vitest-environment happy-dom
2
+ import { afterEach, describe, expect, it, vi } from "vitest";
3
+ import { dispatchModifierKey, dispatchPlainKey } from "./useAppHotkeys";
4
+ import { usePlayerStore } from "../player/store/playerStore";
5
+ import { clearAutomationClipboard, copyRange } from "../player/components/automationClipboard";
6
+ import { VOLUME_RANGE } from "@hyperframes/core/audio-automation";
7
+ import type { TimelineElement } from "../player/store/timelineElement";
8
+
9
+ /** Minimal valid fixture — TimelineElement only requires these five fields. */
10
+ const bgmElement: TimelineElement = {
11
+ id: "bgm",
12
+ key: "bgm",
13
+ tag: "audio",
14
+ start: 0,
15
+ duration: 6,
16
+ track: 0,
17
+ };
18
+
19
+ /** Every callback dispatchPlainKey can reach, so a test can assert which one
20
+ * a key resolved to. Unannotated on purpose: the parameter type is not
21
+ * exported, and structural inference checks it at the call site. */
22
+ function callbacks() {
23
+ return {
24
+ handleTimelineElementDelete: vi.fn(async () => {}),
25
+ handleTimelineElementSplit: vi.fn(async () => {}),
26
+ handleDomEditElementDelete: vi.fn(async () => {}),
27
+ handleUndo: vi.fn(async () => {}),
28
+ handleRedo: vi.fn(async () => {}),
29
+ handleCopy: vi.fn(() => false),
30
+ handlePaste: vi.fn(async () => {}),
31
+ handleCut: vi.fn(async () => false),
32
+ onResetKeyframes: vi.fn(() => true),
33
+ onDeleteSelectedKeyframes: vi.fn(),
34
+ showToast: vi.fn(),
35
+ leftSidebarRef: { current: null },
36
+ domEditSelectionRef: { current: null },
37
+ };
38
+ }
39
+
40
+ const press = (key: string) =>
41
+ new KeyboardEvent("keydown", { key, bubbles: true, cancelable: true });
42
+
43
+ const chord = (key: string) =>
44
+ new KeyboardEvent("keydown", { key, metaKey: true, bubbles: true, cancelable: true });
45
+
46
+ afterEach(() => {
47
+ clearAutomationClipboard();
48
+ usePlayerStore.getState().clearAutomationSelection();
49
+ usePlayerStore.setState({
50
+ elements: [],
51
+ selectedElementId: null,
52
+ selectedElementIds: new Set<string>(),
53
+ selectedKeyframes: new Set<string>(),
54
+ });
55
+ });
56
+
57
+ describe("dispatchPlainKey — Delete arbitration", () => {
58
+ const selectBgm = () =>
59
+ usePlayerStore.setState({ elements: [bgmElement], selectedElementId: "bgm" });
60
+
61
+ const selectRange = () =>
62
+ usePlayerStore
63
+ .getState()
64
+ .setAutomationSelection({ elementKey: "bgm", target: "volume", t0: 2, t1: 4 });
65
+
66
+ it("deletes the selected clip when no automation range is active", () => {
67
+ selectBgm();
68
+ const cb = callbacks();
69
+ const e = press("Delete");
70
+ dispatchPlainKey(e, "delete", cb);
71
+ // The pre-existing contract, pinned so the new guard cannot widen.
72
+ expect(cb.handleTimelineElementDelete).toHaveBeenCalledTimes(1);
73
+ expect(e.defaultPrevented).toBe(true);
74
+ });
75
+
76
+ it("leaves the clip alone when an automation range is active", () => {
77
+ // The bug: this listener is on window/capture so it runs BEFORE
78
+ // useAutomationSelectionKeyboard's document/capture handler. Without the
79
+ // guard, clearing a 2s automation range deleted the whole audio clip.
80
+ selectBgm();
81
+ selectRange();
82
+ const cb = callbacks();
83
+ const e = press("Delete");
84
+ dispatchPlainKey(e, "delete", cb);
85
+ expect(cb.handleTimelineElementDelete).not.toHaveBeenCalled();
86
+ // Must NOT be consumed: the automation handler downstream still needs it.
87
+ expect(e.defaultPrevented).toBe(false);
88
+ });
89
+
90
+ it("leaves keyframe reset alone when an automation range is active", () => {
91
+ // Backspace's reset-keyframes branch sits below the guard, so it has to be
92
+ // covered too — otherwise Backspace wiped every keyframe on the clip.
93
+ selectBgm();
94
+ usePlayerStore.setState({
95
+ keyframeCache: new Map([["bgm", { targets: [], version: 0 }]]),
96
+ });
97
+ selectRange();
98
+ const cb = callbacks();
99
+ const e = press("Backspace");
100
+ dispatchPlainKey(e, "backspace", cb);
101
+ expect(cb.onResetKeyframes).not.toHaveBeenCalled();
102
+ expect(cb.handleTimelineElementDelete).not.toHaveBeenCalled();
103
+ expect(e.defaultPrevented).toBe(false);
104
+ });
105
+
106
+ it("still lets a keyframe selection win over an automation range", () => {
107
+ // Ordering: the keyframe guard precedes the automation one, so a keyframe
108
+ // selection keeps Delete even with a range showing.
109
+ selectBgm();
110
+ selectRange();
111
+ usePlayerStore.setState({ selectedKeyframes: new Set(["bgm:opacity:0"]) });
112
+ const cb = callbacks();
113
+ const e = press("Delete");
114
+ dispatchPlainKey(e, "delete", cb);
115
+ expect(cb.onDeleteSelectedKeyframes).toHaveBeenCalledTimes(1);
116
+ expect(cb.handleTimelineElementDelete).not.toHaveBeenCalled();
117
+ expect(e.defaultPrevented).toBe(true);
118
+ });
119
+ });
120
+
121
+ describe("dispatchModifierKey — Cmd+C/Cmd+V arbitration", () => {
122
+ const clip: TimelineElement = {
123
+ id: "bgm",
124
+ key: "bgm",
125
+ tag: "audio",
126
+ start: 0,
127
+ duration: 6,
128
+ track: 0,
129
+ };
130
+
131
+ it("lets the clip clipboard have Cmd+C when no automation range is active", () => {
132
+ usePlayerStore.setState({ elements: [clip], selectedElementId: "bgm" });
133
+ const cb = callbacks();
134
+ dispatchModifierKey(chord("c"), "c", cb);
135
+ expect(cb.handleCopy).toHaveBeenCalled();
136
+ });
137
+
138
+ it("keeps Cmd+C from the clip clipboard when an automation range is active", () => {
139
+ // Both clipboards arming on one press double-wrote and toasted "Copied clip".
140
+ usePlayerStore.setState({ elements: [clip], selectedElementId: "bgm" });
141
+ usePlayerStore.getState().setAutomationSelection({
142
+ elementKey: "bgm",
143
+ target: "volume",
144
+ t0: 1,
145
+ t1: 3,
146
+ });
147
+ const cb = callbacks();
148
+ const e = chord("c");
149
+ expect(dispatchModifierKey(e, "c", cb)).toBe(true);
150
+ expect(cb.handleCopy).not.toHaveBeenCalled();
151
+ // No preventDefault: the automation handler downstream still needs the key.
152
+ expect(e.defaultPrevented).toBe(false);
153
+ });
154
+
155
+ it("lets the clip clipboard have Cmd+V when the automation clipboard is empty", () => {
156
+ // Nothing to paste means nothing to claim — the clip paste should still run.
157
+ clearAutomationClipboard();
158
+ usePlayerStore.setState({ elements: [clip], selectedElementId: "bgm" });
159
+ usePlayerStore.getState().setAutomationSelection({
160
+ elementKey: "bgm",
161
+ target: "volume",
162
+ t0: 1,
163
+ t1: 3,
164
+ });
165
+ const cb = callbacks();
166
+ dispatchModifierKey(chord("v"), "v", cb);
167
+ expect(cb.handlePaste).toHaveBeenCalled();
168
+ });
169
+
170
+ it("keeps Cmd+V from duplicating the clip while an automation paste is pending", () => {
171
+ clearAutomationClipboard();
172
+ copyRange(
173
+ null,
174
+ {
175
+ target: "volume",
176
+ points: [
177
+ { t: 1, v: 1 },
178
+ { t: 3, v: 0.25 },
179
+ ],
180
+ },
181
+ VOLUME_RANGE,
182
+ 1,
183
+ 3,
184
+ );
185
+ usePlayerStore.setState({ elements: [clip], selectedElementId: "bgm" });
186
+ usePlayerStore.getState().setAutomationSelection({
187
+ elementKey: "bgm",
188
+ target: "volume",
189
+ t0: 1,
190
+ t1: 3,
191
+ });
192
+ const cb = callbacks();
193
+ const e = chord("v");
194
+ dispatchModifierKey(e, "v", cb);
195
+ expect(cb.handlePaste).not.toHaveBeenCalled();
196
+ expect(e.defaultPrevented).toBe(false);
197
+ });
198
+ });
@@ -1,10 +1,12 @@
1
1
  import { useCallback, useEffect, useRef } from "react";
2
+ import { automationOwnsKey } from "./useAutomationSelectionKeyboard";
2
3
  import { usePlayerStore } from "../player";
3
4
  import type { TimelineElement } from "../player";
4
5
  import type { DomEditSelection } from "../components/editor/domEditing";
5
6
  import type { LeftSidebarHandle } from "../components/sidebar/LeftSidebar";
6
7
  import { STUDIO_MOTION_PATH } from "../components/editor/studioMotion";
7
8
  import { isTypingTarget } from "../utils/typingTarget";
9
+ import { isEditableTarget } from "../utils/timelineDiscovery";
8
10
  import { shouldIgnoreHistoryShortcut } from "../utils/studioHelpers";
9
11
  import { canSplitElement } from "../utils/timelineElementSplit";
10
12
  import { trackStudioEvent } from "../utils/studioTelemetry";
@@ -158,7 +160,14 @@ interface HotkeyCallbacks {
158
160
  showToast: (message: string, tone?: "error" | "info") => void;
159
161
  }
160
162
 
161
- function dispatchModifierKey(event: KeyboardEvent, key: string, cb: HotkeyCallbacks): boolean {
163
+ /** Exported for tests, like dispatchPlainKey below: lets the Cmd+C/Cmd+V
164
+ * arbitration between an automation range and the clip clipboard be asserted
165
+ * without standing up the whole hook. */
166
+ export function dispatchModifierKey(
167
+ event: KeyboardEvent,
168
+ key: string,
169
+ cb: HotkeyCallbacks,
170
+ ): boolean {
162
171
  if (
163
172
  !shouldIgnoreHistoryShortcut(event.target) &&
164
173
  handleUndoRedoKey(
@@ -195,7 +204,15 @@ function dispatchModifierKey(event: KeyboardEvent, key: string, cb: HotkeyCallba
195
204
  return true;
196
205
  }
197
206
 
198
- if (!event.shiftKey && !event.altKey && !isTypingTarget(event.target)) {
207
+ if (!event.shiftKey && !event.altKey && !isEditableTarget(event.target)) {
208
+ // An active automation range owns Cmd+C/Cmd+V, the same way it owns Delete
209
+ // below. This listener is on window/capture and runs before
210
+ // useAutomationSelectionKeyboard's document/capture handler, so without
211
+ // this the clip clipboard also claimed the key: Cmd+V duplicated the clip
212
+ // while the automation paste wrote the same file, and Cmd+C armed both
213
+ // clipboards and toasted "Copied clip". Return without preventDefault so
214
+ // the downstream handler still sees the key.
215
+ if (automationOwnsKey(event)) return true;
199
216
  if (key === "c") {
200
217
  if (cb.handleCopy()) {
201
218
  event.preventDefault();
@@ -222,7 +239,10 @@ function dispatchModifierKey(event: KeyboardEvent, key: string, cb: HotkeyCallba
222
239
  }
223
240
 
224
241
  // fallow-ignore-next-line complexity
225
- function dispatchPlainKey(event: KeyboardEvent, key: string, cb: HotkeyCallbacks): void {
242
+ /** Exported for tests: the unmodified-key half of the dispatcher, so the
243
+ * Delete arbitration between keyframes, an automation range and the clip can
244
+ * be asserted without standing up the whole hook. */
245
+ export function dispatchPlainKey(event: KeyboardEvent, key: string, cb: HotkeyCallbacks): void {
226
246
  if (key === "f" && !event.shiftKey && !event.altKey) {
227
247
  event.preventDefault();
228
248
  if (document.fullscreenElement) void document.exitFullscreen();
@@ -288,6 +308,13 @@ function dispatchPlainKey(event: KeyboardEvent, key: string, cb: HotkeyCallbacks
288
308
  event.preventDefault();
289
309
  return;
290
310
  }
311
+ // An active automation range owns Delete: useAutomationSelectionKeyboard
312
+ // empties the range in place, pinning the anchors. Fall through WITHOUT
313
+ // preventDefault so that document-level handler still sees the key — this
314
+ // listener is on window/capture, so it runs first and everything below
315
+ // would otherwise win. Without this the press reaches the clip delete
316
+ // below and destroys the whole clip the lane belongs to.
317
+ if (usePlayerStore.getState().automationSelection) return;
291
318
  if (event.key === "Backspace") {
292
319
  const { selectedElementId, keyframeCache } = usePlayerStore.getState();
293
320
  if (selectedElementId && keyframeCache.has(selectedElementId) && cb.onResetKeyframes()) {