@hyperframes/studio 0.7.90 → 0.7.92

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 (86) hide show
  1. package/dist/assets/{hyperframes-player-RS9ROX_S.js → hyperframes-player-9qOa08Tv.js} +1 -1
  2. package/dist/assets/{index-Bi-CwQDX.js → index-Bw4lNdtO.js} +1 -1
  3. package/dist/assets/{index-CquRJgm-.js → index-CDTN2ZVa.js} +1 -1
  4. package/dist/assets/index-DIntkrQl.js +428 -0
  5. package/dist/index.d.ts +20 -16
  6. package/dist/index.html +1 -1
  7. package/dist/index.js +6495 -5429
  8. package/dist/index.js.map +1 -1
  9. package/package.json +7 -7
  10. package/src/components/StudioRightPanel.tsx +1 -1
  11. package/src/components/editor/GsapAnimationList.tsx +84 -0
  12. package/src/components/editor/GsapAnimationSection.test.tsx +100 -11
  13. package/src/components/editor/GsapAnimationSection.tsx +11 -41
  14. package/src/components/editor/PropertyPanel.tsx +1 -1
  15. package/src/components/editor/PropertyPanelFlat.tsx +29 -26
  16. package/src/components/editor/gsapAnimationCallbacks.ts +0 -10
  17. package/src/components/editor/propertyPanelFlatDescriptors.ts +20 -0
  18. package/src/components/editor/propertyPanelFlatMotionSection.test.tsx +56 -55
  19. package/src/components/editor/propertyPanelFlatMotionSection.tsx +9 -37
  20. package/src/components/editor/propertyPanelTypes.ts +1 -1
  21. package/src/components/nle/useTimelineEditCallbacks.test.tsx +78 -4
  22. package/src/components/nle/useTimelineEditCallbacks.ts +12 -7
  23. package/src/components/renders/useRenderQueue.ts +16 -3
  24. package/src/components/renders/useRenderQueueTelemetry.test.tsx +104 -0
  25. package/src/contexts/DomEditContext.tsx +1 -1
  26. package/src/hooks/useDomEditSession.test.tsx +1 -1
  27. package/src/hooks/useDomEditSession.ts +1 -1
  28. package/src/hooks/useGsapKeyframeOps.test.tsx +83 -5
  29. package/src/hooks/useGsapKeyframeOps.ts +20 -9
  30. package/src/player/components/BeatStrip.test.tsx +374 -0
  31. package/src/player/components/BeatStrip.tsx +295 -37
  32. package/src/player/components/KeyframeDiamondContextMenu.test.tsx +16 -1
  33. package/src/player/components/KeyframeDiamondContextMenu.tsx +6 -5
  34. package/src/player/components/Timeline.test.ts +2 -3
  35. package/src/player/components/Timeline.tsx +47 -48
  36. package/src/player/components/TimelineCanvas.tsx +14 -71
  37. package/src/player/components/TimelineClip.tsx +6 -3
  38. package/src/player/components/TimelineClipDiamonds.test.tsx +442 -18
  39. package/src/player/components/TimelineClipDiamonds.tsx +66 -211
  40. package/src/player/components/TimelineGestureOverlay.test.tsx +90 -0
  41. package/src/player/components/TimelineGestureOverlay.tsx +96 -0
  42. package/src/player/components/TimelineLanes.test.tsx +0 -1
  43. package/src/player/components/TimelineLanes.tsx +3 -2
  44. package/src/player/components/TimelineOverlays.test.ts +161 -0
  45. package/src/player/components/TimelineOverlays.tsx +104 -11
  46. package/src/player/components/timelineCallbacks.ts +1 -1
  47. package/src/player/components/timelineClipDragGestureLifecycle.test.ts +18 -3
  48. package/src/player/components/timelineClipDragGestureLifecycle.ts +199 -51
  49. package/src/player/components/timelineClipDragPreview.test.ts +33 -0
  50. package/src/player/components/timelineClipDragPreview.ts +18 -12
  51. package/src/player/components/timelineClipDragTypes.ts +12 -0
  52. package/src/player/components/timelineDiamondTypes.ts +5 -15
  53. package/src/player/components/timelineDragDrop.test.tsx +206 -0
  54. package/src/player/components/timelineDragDrop.ts +152 -30
  55. package/src/player/components/timelineLaneProps.ts +0 -1
  56. package/src/player/components/timelineMarquee.test.ts +73 -25
  57. package/src/player/components/timelineMarquee.ts +44 -16
  58. package/src/player/components/timelineTestViewport.ts +13 -0
  59. package/src/player/components/timelineViewModel.ts +4 -4
  60. package/src/player/components/useTimelineClipDrag.resize.test.tsx +219 -17
  61. package/src/player/components/useTimelineClipDrag.ts +112 -70
  62. package/src/player/components/useTimelineClipRenderWindow.ts +4 -4
  63. package/src/player/components/useTimelineKeyframeHandlers.test.tsx +43 -7
  64. package/src/player/components/useTimelineKeyframeHandlers.ts +465 -2
  65. package/src/player/components/useTimelineLaneMoveRefresh.ts +7 -0
  66. package/src/player/components/useTimelineRangeSelection.test.tsx +204 -0
  67. package/src/player/components/useTimelineRangeSelection.ts +202 -69
  68. package/src/player/components/useTimelineRangeSelectionScrub.test.tsx +3 -2
  69. package/src/player/components/useTimelineRowVirtualization.ts +11 -4
  70. package/src/player/components/useTrackGapMenu.test.tsx +64 -0
  71. package/src/player/components/useTrackGapMenu.ts +32 -9
  72. package/src/player/store/keyframeSlice.ts +59 -18
  73. package/src/player/store/playerStore.test.ts +76 -0
  74. package/src/player/store/playerStore.ts +19 -11
  75. package/src/telemetry/canary.test.ts +325 -0
  76. package/src/telemetry/canary.ts +268 -0
  77. package/src/telemetry/client.test.ts +8 -3
  78. package/src/telemetry/client.ts +13 -43
  79. package/src/telemetry/config.ts +16 -2
  80. package/src/telemetry/distinctId.test.ts +35 -0
  81. package/src/telemetry/distinctId.ts +11 -2
  82. package/src/telemetry/policy.test.ts +81 -0
  83. package/src/telemetry/policy.ts +104 -0
  84. package/src/utils/studioTelemetry.test.ts +76 -0
  85. package/src/utils/studioTelemetry.ts +14 -6
  86. package/dist/assets/index-Cql15Yur.js +0 -428
@@ -0,0 +1,374 @@
1
+ // @vitest-environment happy-dom
2
+
3
+ import React, { act } from "react";
4
+ import { createRoot, type Root } from "react-dom/client";
5
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
6
+ import type { TimelineElement } from "../store/playerStore";
7
+ import { usePlayerStore } from "../store/playerStore";
8
+ import { BeatStrip } from "./BeatStrip";
9
+
10
+ (globalThis as unknown as { IS_REACT_ACT_ENVIRONMENT: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
11
+
12
+ const roots: Root[] = [];
13
+ const requestSeek = vi.fn();
14
+ const commitBeatEdits = usePlayerStore.getState().commitBeatEdits;
15
+ const commitBeatEditsSpy = vi.fn((...args: Parameters<typeof commitBeatEdits>) =>
16
+ commitBeatEdits(...args),
17
+ );
18
+ const MUSIC_ELEMENT: TimelineElement = {
19
+ id: "background-music",
20
+ label: "Music",
21
+ tag: "audio",
22
+ src: "music.mp3",
23
+ start: 0,
24
+ duration: 10,
25
+ track: 0,
26
+ timelineRole: "music",
27
+ };
28
+ const BEAT_ANALYSIS = {
29
+ beatTimes: [1, 3],
30
+ beatStrengths: [0.5, 0.8],
31
+ bpm: 120,
32
+ bpmConfidence: "high" as const,
33
+ channelData: null,
34
+ sampleRate: 48_000,
35
+ peak: 1,
36
+ };
37
+
38
+ function pointerEvent(type: string, init: PointerEventInit): Event {
39
+ if (typeof PointerEvent === "function") return new PointerEvent(type, init);
40
+ const event = new MouseEvent(type, init);
41
+ Object.defineProperty(event, "pointerId", { value: init.pointerId ?? 0 });
42
+ return event;
43
+ }
44
+
45
+ function mountBeatStrip(renderTimeRange?: { start: number; end: number }) {
46
+ const viewport = document.createElement("div");
47
+ viewport.dataset.timelineScrollViewport = "";
48
+ Object.defineProperties(viewport, {
49
+ clientWidth: { configurable: true, value: 500 },
50
+ clientHeight: { configurable: true, value: 300 },
51
+ scrollWidth: { configurable: true, value: 2_000 },
52
+ scrollHeight: { configurable: true, value: 2_000 },
53
+ });
54
+ viewport.getBoundingClientRect = () => new DOMRect(0, 0, 1_000, 500);
55
+ Object.assign(viewport, {
56
+ setPointerCapture: vi.fn(),
57
+ hasPointerCapture: vi.fn(() => true),
58
+ releasePointerCapture: vi.fn(),
59
+ });
60
+ document.body.appendChild(viewport);
61
+ const root = createRoot(viewport);
62
+ roots.push(root);
63
+ act(() => {
64
+ root.render(
65
+ <BeatStrip
66
+ beatTimes={[1, 3]}
67
+ beatStrengths={[0.5, 0.8]}
68
+ pps={100}
69
+ renderTimeRange={renderTimeRange}
70
+ />,
71
+ );
72
+ });
73
+ return { root, viewport };
74
+ }
75
+
76
+ function firstBeat(): HTMLDivElement {
77
+ const beat = document.querySelector<HTMLDivElement>(
78
+ '[title="Drag to move · double-click to delete"]',
79
+ );
80
+ if (!beat) throw new Error("Expected a beat handle");
81
+ return beat;
82
+ }
83
+
84
+ function startBeatDrag(clientX = 100, pointerId = 1): void {
85
+ act(() => {
86
+ firstBeat().dispatchEvent(
87
+ pointerEvent("pointerdown", { bubbles: true, button: 0, clientX, clientY: 100, pointerId }),
88
+ );
89
+ });
90
+ expect(usePlayerStore.getState().beatDragging).toBe(true);
91
+ }
92
+
93
+ function releaseBeatDrag(clientX = 140, pointerId = 1): void {
94
+ act(() => {
95
+ window.dispatchEvent(
96
+ pointerEvent("pointerup", { bubbles: true, clientX, clientY: 100, pointerId }),
97
+ );
98
+ });
99
+ }
100
+
101
+ function expectCancelledBeatDrag(): void {
102
+ expect(commitBeatEditsSpy).not.toHaveBeenCalled();
103
+ expect(usePlayerStore.getState().beatDragging).toBe(false);
104
+ }
105
+
106
+ function expectCommittedBeatAt(time: number): void {
107
+ expect(commitBeatEditsSpy).toHaveBeenCalledExactlyOnceWith(expect.anything(), "move beat");
108
+ expect(usePlayerStore.getState().beatEdits?.added[0]?.time).toBe(time);
109
+ }
110
+
111
+ beforeEach(() => {
112
+ commitBeatEditsSpy.mockClear();
113
+ requestSeek.mockReset();
114
+ usePlayerStore.setState({
115
+ timelineSessionEpoch: 1,
116
+ timelineProjectId: "project-a",
117
+ beatDragging: false,
118
+ requestSeek,
119
+ elements: [MUSIC_ELEMENT],
120
+ beatAnalysis: BEAT_ANALYSIS,
121
+ beatEdits: null,
122
+ commitBeatEdits: commitBeatEditsSpy,
123
+ });
124
+ });
125
+
126
+ afterEach(() => {
127
+ act(() => window.dispatchEvent(new Event("blur")));
128
+ for (const root of roots.splice(0)) act(() => root.unmount());
129
+ document.body.innerHTML = "";
130
+ vi.unstubAllGlobals();
131
+ });
132
+
133
+ describe("BeatStrip gesture ownership", () => {
134
+ it("commits once after its source row unmounts", () => {
135
+ const { root } = mountBeatStrip();
136
+ startBeatDrag();
137
+ act(() => root.render(null));
138
+ expect(usePlayerStore.getState().beatDragging).toBe(true);
139
+
140
+ act(() => {
141
+ window.dispatchEvent(
142
+ pointerEvent("pointermove", {
143
+ bubbles: true,
144
+ clientX: 140,
145
+ clientY: 100,
146
+ pointerId: 1,
147
+ }),
148
+ );
149
+ window.dispatchEvent(
150
+ pointerEvent("pointerup", {
151
+ bubbles: true,
152
+ clientX: 140,
153
+ clientY: 100,
154
+ pointerId: 1,
155
+ }),
156
+ );
157
+ window.dispatchEvent(
158
+ pointerEvent("pointerup", {
159
+ bubbles: true,
160
+ clientX: 160,
161
+ clientY: 100,
162
+ pointerId: 1,
163
+ }),
164
+ );
165
+ });
166
+
167
+ expectCommittedBeatAt(1.4);
168
+ expect(usePlayerStore.getState().beatDragging).toBe(false);
169
+ });
170
+
171
+ it("includes viewport scrolling in the final beat time", () => {
172
+ const { viewport } = mountBeatStrip();
173
+ startBeatDrag(100);
174
+ viewport.scrollLeft = 50;
175
+
176
+ releaseBeatDrag(110);
177
+ expectCommittedBeatAt(1.6);
178
+ });
179
+
180
+ it("cancels without mutation on pointer cancel, Escape, and project switch", () => {
181
+ mountBeatStrip();
182
+ startBeatDrag();
183
+ act(() => {
184
+ window.dispatchEvent(pointerEvent("pointercancel", { pointerId: 1 }));
185
+ window.dispatchEvent(pointerEvent("pointerup", { clientX: 140, clientY: 100, pointerId: 1 }));
186
+ });
187
+
188
+ startBeatDrag();
189
+ act(() => window.dispatchEvent(new KeyboardEvent("keydown", { key: "Escape" })));
190
+
191
+ startBeatDrag();
192
+ act(() => usePlayerStore.getState().beginTimelineSession("project-b"));
193
+ releaseBeatDrag();
194
+ expectCancelledBeatDrag();
195
+ });
196
+
197
+ it("cancels without mutation when the captured music source disappears", () => {
198
+ mountBeatStrip();
199
+ startBeatDrag();
200
+ act(() => usePlayerStore.setState({ elements: [] }));
201
+ releaseBeatDrag();
202
+ expectCancelledBeatDrag();
203
+ });
204
+
205
+ it("cancels when the captured beat disappears during the gesture", () => {
206
+ mountBeatStrip();
207
+ startBeatDrag();
208
+ act(() => {
209
+ usePlayerStore.setState({
210
+ beatAnalysis: { ...BEAT_ANALYSIS, beatTimes: [3], beatStrengths: [0.8] },
211
+ });
212
+ });
213
+ releaseBeatDrag();
214
+ expectCancelledBeatDrag();
215
+ });
216
+
217
+ it("releases the actor when the owning timeline viewport unmounts", async () => {
218
+ const { viewport } = mountBeatStrip();
219
+ startBeatDrag();
220
+
221
+ await act(async () => {
222
+ viewport.remove();
223
+ await Promise.resolve();
224
+ });
225
+ releaseBeatDrag();
226
+ expectCancelledBeatDrag();
227
+ });
228
+
229
+ it("ignores unrelated pointers and keeps the active beat rendered outside the time window", () => {
230
+ const { root } = mountBeatStrip();
231
+ startBeatDrag();
232
+ act(() => {
233
+ window.dispatchEvent(
234
+ pointerEvent("pointermove", {
235
+ bubbles: true,
236
+ clientX: 150,
237
+ clientY: 100,
238
+ pointerId: 2,
239
+ }),
240
+ );
241
+ window.dispatchEvent(
242
+ pointerEvent("pointerup", {
243
+ bubbles: true,
244
+ clientX: 150,
245
+ clientY: 100,
246
+ pointerId: 2,
247
+ }),
248
+ );
249
+ root.render(
250
+ <BeatStrip
251
+ beatTimes={[1, 3]}
252
+ beatStrengths={[0.5, 0.8]}
253
+ pps={100}
254
+ renderTimeRange={{ start: 2.5, end: 3.5 }}
255
+ />,
256
+ );
257
+ });
258
+
259
+ expect(
260
+ document.querySelectorAll('[title="Drag to move · double-click to delete"]'),
261
+ ).toHaveLength(2);
262
+ expect(commitBeatEditsSpy).not.toHaveBeenCalled();
263
+
264
+ act(() => {
265
+ window.dispatchEvent(
266
+ pointerEvent("pointerup", {
267
+ bubbles: true,
268
+ clientX: 150,
269
+ clientY: 100,
270
+ pointerId: 1,
271
+ }),
272
+ );
273
+ });
274
+ expect(commitBeatEditsSpy).toHaveBeenCalledOnce();
275
+ });
276
+
277
+ it("keeps the dragged beat pinned by time when another beat is inserted before it", () => {
278
+ const { root } = mountBeatStrip();
279
+ startBeatDrag();
280
+ act(() => {
281
+ window.dispatchEvent(
282
+ pointerEvent("pointermove", {
283
+ bubbles: true,
284
+ clientX: 140,
285
+ clientY: 100,
286
+ pointerId: 1,
287
+ }),
288
+ );
289
+ usePlayerStore.setState({
290
+ beatAnalysis: {
291
+ ...BEAT_ANALYSIS,
292
+ beatTimes: [0.5, 1, 3],
293
+ beatStrengths: [0.3, 0.5, 0.8],
294
+ },
295
+ });
296
+ root.render(<BeatStrip beatTimes={[0.5, 1, 3]} beatStrengths={[0.3, 0.5, 0.8]} pps={100} />);
297
+ });
298
+
299
+ const lefts = Array.from(
300
+ document.querySelectorAll<HTMLDivElement>('[title="Drag to move · double-click to delete"]'),
301
+ (beat) => beat.style.left,
302
+ );
303
+ expect(lefts).toContain("134px");
304
+
305
+ releaseBeatDrag(140);
306
+ expectCommittedBeatAt(1.4);
307
+ });
308
+
309
+ it("keeps the first pointer in control when a second touch starts", () => {
310
+ mountBeatStrip();
311
+ startBeatDrag();
312
+ const beats = document.querySelectorAll<HTMLDivElement>(
313
+ '[title="Drag to move · double-click to delete"]',
314
+ );
315
+ act(() => {
316
+ beats[1]?.dispatchEvent(
317
+ pointerEvent("pointerdown", {
318
+ bubbles: true,
319
+ button: 0,
320
+ clientX: 300,
321
+ clientY: 100,
322
+ pointerId: 2,
323
+ }),
324
+ );
325
+ });
326
+
327
+ releaseBeatDrag(140, 1);
328
+ expectCommittedBeatAt(1.4);
329
+ });
330
+
331
+ it("lets Escape bubble before cancelling the active drag", () => {
332
+ mountBeatStrip();
333
+ startBeatDrag();
334
+ const sawEscape = vi.fn();
335
+ document.addEventListener("keydown", sawEscape, { once: true });
336
+
337
+ act(() => {
338
+ firstBeat().dispatchEvent(new KeyboardEvent("keydown", { key: "Escape", bubbles: true }));
339
+ });
340
+
341
+ expect(sawEscape).toHaveBeenCalledOnce();
342
+ expectCancelledBeatDrag();
343
+ });
344
+
345
+ it("does not autoscroll or mutate below the drag threshold", () => {
346
+ const requestAnimationFrame = vi.fn(() => 1);
347
+ vi.stubGlobal("requestAnimationFrame", requestAnimationFrame);
348
+ vi.stubGlobal("cancelAnimationFrame", vi.fn());
349
+ mountBeatStrip();
350
+ startBeatDrag(990);
351
+
352
+ act(() => {
353
+ window.dispatchEvent(
354
+ pointerEvent("pointermove", {
355
+ bubbles: true,
356
+ clientX: 991,
357
+ clientY: 100,
358
+ pointerId: 1,
359
+ }),
360
+ );
361
+ window.dispatchEvent(
362
+ pointerEvent("pointerup", {
363
+ bubbles: true,
364
+ clientX: 991,
365
+ clientY: 100,
366
+ pointerId: 1,
367
+ }),
368
+ );
369
+ });
370
+
371
+ expect(requestAnimationFrame).not.toHaveBeenCalled();
372
+ expect(commitBeatEditsSpy).not.toHaveBeenCalled();
373
+ });
374
+ });