@hyperframes/studio 0.5.5 → 0.6.0-alpha.1

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 (74) hide show
  1. package/dist/assets/hyperframes-player-Cd8vYWxP.js +198 -0
  2. package/dist/assets/index-D04_ZoMm.js +107 -0
  3. package/dist/assets/index-UWFaHilT.css +1 -0
  4. package/dist/index.html +2 -2
  5. package/package.json +4 -4
  6. package/src/App.tsx +2621 -170
  7. package/src/components/LintModal.tsx +3 -4
  8. package/src/components/editor/DomEditOverlay.test.ts +241 -0
  9. package/src/components/editor/DomEditOverlay.tsx +1300 -0
  10. package/src/components/editor/MotionPanel.tsx +651 -0
  11. package/src/components/editor/PropertyPanel.test.ts +67 -0
  12. package/src/components/editor/PropertyPanel.tsx +2891 -207
  13. package/src/components/editor/TimelineLayerPanel.test.ts +42 -0
  14. package/src/components/editor/TimelineLayerPanel.tsx +113 -0
  15. package/src/components/editor/colorValue.test.ts +82 -0
  16. package/src/components/editor/colorValue.ts +175 -0
  17. package/src/components/editor/domEditing.test.ts +872 -0
  18. package/src/components/editor/domEditing.ts +993 -0
  19. package/src/components/editor/floatingPanel.test.ts +34 -0
  20. package/src/components/editor/floatingPanel.ts +54 -0
  21. package/src/components/editor/fontAssets.ts +32 -0
  22. package/src/components/editor/fontCatalog.ts +126 -0
  23. package/src/components/editor/gradientValue.test.ts +89 -0
  24. package/src/components/editor/gradientValue.ts +445 -0
  25. package/src/components/editor/manualEditingAvailability.test.ts +120 -0
  26. package/src/components/editor/manualEditingAvailability.ts +60 -0
  27. package/src/components/editor/manualEdits.test.ts +945 -0
  28. package/src/components/editor/manualEdits.ts +1397 -0
  29. package/src/components/editor/manualOffsetDrag.test.ts +140 -0
  30. package/src/components/editor/manualOffsetDrag.ts +307 -0
  31. package/src/components/editor/studioMotion.test.ts +355 -0
  32. package/src/components/editor/studioMotion.ts +632 -0
  33. package/src/components/nle/NLELayout.tsx +27 -4
  34. package/src/components/nle/NLEPreview.tsx +50 -5
  35. package/src/components/renders/RenderQueue.tsx +13 -62
  36. package/src/components/renders/useRenderQueue.ts +6 -30
  37. package/src/components/sidebar/AssetsTab.tsx +3 -4
  38. package/src/components/sidebar/CompositionsTab.test.ts +16 -1
  39. package/src/components/sidebar/CompositionsTab.tsx +117 -45
  40. package/src/components/sidebar/LeftSidebar.tsx +140 -125
  41. package/src/hooks/usePersistentEditHistory.test.ts +256 -0
  42. package/src/hooks/usePersistentEditHistory.ts +337 -0
  43. package/src/icons/SystemIcons.tsx +2 -0
  44. package/src/player/components/CompositionThumbnail.test.ts +19 -0
  45. package/src/player/components/CompositionThumbnail.tsx +50 -13
  46. package/src/player/components/EditModal.tsx +5 -20
  47. package/src/player/components/Player.tsx +18 -2
  48. package/src/player/components/Timeline.test.ts +20 -0
  49. package/src/player/components/Timeline.tsx +103 -21
  50. package/src/player/components/TimelineClip.test.ts +92 -0
  51. package/src/player/components/TimelineClip.tsx +241 -7
  52. package/src/player/components/timelineEditing.test.ts +16 -3
  53. package/src/player/components/timelineEditing.ts +10 -3
  54. package/src/player/hooks/useTimelinePlayer.test.ts +148 -19
  55. package/src/player/hooks/useTimelinePlayer.ts +287 -16
  56. package/src/player/store/playerStore.ts +2 -0
  57. package/src/utils/clipboard.test.ts +89 -0
  58. package/src/utils/clipboard.ts +57 -0
  59. package/src/utils/editHistory.test.ts +244 -0
  60. package/src/utils/editHistory.ts +218 -0
  61. package/src/utils/editHistoryStorage.test.ts +37 -0
  62. package/src/utils/editHistoryStorage.ts +99 -0
  63. package/src/utils/mediaTypes.ts +1 -1
  64. package/src/utils/sourcePatcher.test.ts +128 -1
  65. package/src/utils/sourcePatcher.ts +130 -18
  66. package/src/utils/studioFileHistory.test.ts +156 -0
  67. package/src/utils/studioFileHistory.ts +61 -0
  68. package/src/utils/timelineAssetDrop.test.ts +31 -11
  69. package/src/utils/timelineAssetDrop.ts +22 -2
  70. package/src/utils/timelineInspector.test.ts +79 -0
  71. package/src/utils/timelineInspector.ts +116 -0
  72. package/dist/assets/hyperframes-player-CEnWY28J.js +0 -417
  73. package/dist/assets/index-04Mp2wOn.css +0 -1
  74. package/dist/assets/index-960mgQMI.js +0 -93
@@ -0,0 +1,355 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { Window } from "happy-dom";
3
+ import type { DomEditSelection } from "./domEditing";
4
+ import {
5
+ STUDIO_MOTION_TIMELINE_ID,
6
+ applyStudioMotionManifest,
7
+ buildStudioGsapPresetMotion,
8
+ clampStudioCustomEasePoints,
9
+ controlPointsForGsapEase,
10
+ emptyStudioMotionManifest,
11
+ getStudioMotionForSelection,
12
+ isStudioMotionManifestPath,
13
+ parseStudioCustomEaseData,
14
+ parseStudioMotionManifest,
15
+ removeStudioMotionForSelection,
16
+ serializeStudioCustomEaseData,
17
+ serializeStudioMotionManifest,
18
+ upsertStudioGsapMotion,
19
+ } from "./studioMotion";
20
+
21
+ function createSelection(): DomEditSelection {
22
+ return {
23
+ element: {} as HTMLElement,
24
+ id: "card",
25
+ selector: "#card",
26
+ selectorIndex: undefined,
27
+ sourceFile: "index.html",
28
+ compositionPath: "index.html",
29
+ compositionSrc: undefined,
30
+ isCompositionHost: false,
31
+ label: "Card",
32
+ tagName: "div",
33
+ boundingBox: { x: 0, y: 0, width: 100, height: 100 },
34
+ textContent: null,
35
+ dataAttributes: {},
36
+ inlineStyles: {},
37
+ computedStyles: {},
38
+ textFields: [],
39
+ capabilities: {
40
+ canSelect: true,
41
+ canEditStyles: true,
42
+ canMove: false,
43
+ canResize: false,
44
+ canApplyManualOffset: true,
45
+ canApplyManualSize: true,
46
+ canApplyManualRotation: true,
47
+ },
48
+ };
49
+ }
50
+
51
+ function createDocument(markup: string): Document {
52
+ const window = new Window();
53
+ window.document.body.innerHTML = markup;
54
+ return window.document;
55
+ }
56
+
57
+ function installFakeGsap(window: Window): {
58
+ fromToCalls: Array<{
59
+ target: HTMLElement;
60
+ from: Record<string, unknown>;
61
+ to: Record<string, unknown>;
62
+ at: number;
63
+ }>;
64
+ timeCalls: number[];
65
+ customEaseCalls: Array<{ id: string; data: string }>;
66
+ killCalls: number;
67
+ } {
68
+ const state = {
69
+ fromToCalls: [] as Array<{
70
+ target: HTMLElement;
71
+ from: Record<string, unknown>;
72
+ to: Record<string, unknown>;
73
+ at: number;
74
+ }>,
75
+ timeCalls: [] as number[],
76
+ customEaseCalls: [] as Array<{ id: string; data: string }>,
77
+ killCalls: 0,
78
+ };
79
+ const timeline = {
80
+ fromTo(
81
+ target: HTMLElement,
82
+ from: Record<string, unknown>,
83
+ to: Record<string, unknown>,
84
+ at: number,
85
+ ) {
86
+ state.fromToCalls.push({ target, from, to, at });
87
+ return timeline;
88
+ },
89
+ time(value: number) {
90
+ state.timeCalls.push(value);
91
+ return timeline;
92
+ },
93
+ pause() {
94
+ return timeline;
95
+ },
96
+ kill() {
97
+ state.killCalls += 1;
98
+ },
99
+ duration() {
100
+ return 3;
101
+ },
102
+ };
103
+ (
104
+ window as unknown as {
105
+ gsap: {
106
+ timeline: () => typeof timeline;
107
+ set: (target: HTMLElement, vars: Record<string, unknown>) => void;
108
+ };
109
+ CustomEase: { create: (id: string, data: string) => void };
110
+ __timelines?: Record<string, unknown>;
111
+ }
112
+ ).gsap = {
113
+ timeline: () => timeline,
114
+ set(target, vars) {
115
+ if (vars.clearProps === "transform,opacity,visibility") {
116
+ target.style.removeProperty("transform");
117
+ target.style.removeProperty("opacity");
118
+ target.style.removeProperty("visibility");
119
+ }
120
+ },
121
+ };
122
+ (
123
+ window as unknown as {
124
+ CustomEase: { create: (id: string, data: string) => void };
125
+ }
126
+ ).CustomEase = {
127
+ create(id, data) {
128
+ state.customEaseCalls.push({ id, data });
129
+ },
130
+ };
131
+ return state;
132
+ }
133
+
134
+ describe("studio motion manifest", () => {
135
+ it("round-trips draggable GSAP CustomEase control points", () => {
136
+ const points = parseStudioCustomEaseData("M0,0 C0.18,0.9 0.32,1.2 1,1");
137
+
138
+ expect(points).toEqual({ x1: 0.18, y1: 0.9, x2: 0.32, y2: 1.2 });
139
+ expect(serializeStudioCustomEaseData(points!)).toBe("M0,0 C0.18,0.9 0.32,1.2 1,1");
140
+ expect(parseStudioCustomEaseData("cubic-bezier(0.1, 0.2, 0.3, 1)")).toBeNull();
141
+ });
142
+
143
+ it("clamps custom ease handles to a safe GSAP range and exposes preset previews", () => {
144
+ expect(
145
+ clampStudioCustomEasePoints({
146
+ x1: -2,
147
+ y1: -2,
148
+ x2: 2,
149
+ y2: 3,
150
+ }),
151
+ ).toEqual({ x1: 0, y1: -0.6, x2: 1, y2: 1.6 });
152
+ expect(controlPointsForGsapEase("power3.out")).toEqual({
153
+ x1: 0.165,
154
+ y1: 0.84,
155
+ x2: 0.44,
156
+ y2: 1,
157
+ });
158
+ });
159
+
160
+ it("creates preset GSAP motions with deterministic from/to lanes", () => {
161
+ expect(
162
+ buildStudioGsapPresetMotion("fade-up", {
163
+ start: 0.2,
164
+ duration: 0.9,
165
+ distance: 44,
166
+ ease: "power3.out",
167
+ }),
168
+ ).toMatchObject({
169
+ start: 0.2,
170
+ duration: 0.9,
171
+ ease: "power3.out",
172
+ from: { y: 44, autoAlpha: 0 },
173
+ to: { y: 0, autoAlpha: 1 },
174
+ });
175
+ expect(
176
+ buildStudioGsapPresetMotion("slide", {
177
+ start: 0,
178
+ duration: 0.7,
179
+ distance: 60,
180
+ direction: "right",
181
+ ease: "back.out(1.4)",
182
+ }),
183
+ ).toMatchObject({
184
+ from: { x: -60, autoAlpha: 0 },
185
+ to: { x: 0, autoAlpha: 1 },
186
+ });
187
+ expect(
188
+ buildStudioGsapPresetMotion("pop", {
189
+ start: 0,
190
+ duration: 0.5,
191
+ distance: 30,
192
+ ease: "elastic.out(1, 0.45)",
193
+ }),
194
+ ).toMatchObject({
195
+ from: { scale: 0.88, autoAlpha: 0 },
196
+ to: { scale: 1, autoAlpha: 1 },
197
+ });
198
+ });
199
+
200
+ it("upserts and serializes GSAP motion by stable target", () => {
201
+ const selection = createSelection();
202
+ const manifest = upsertStudioGsapMotion(emptyStudioMotionManifest(), selection, {
203
+ start: 0.25,
204
+ duration: 0.8,
205
+ ease: "power3.out",
206
+ from: { x: 0, y: 44, scale: 1, autoAlpha: 0 },
207
+ to: { x: 0, y: 0, scale: 1, autoAlpha: 1 },
208
+ });
209
+ const updated = upsertStudioGsapMotion(manifest, selection, {
210
+ start: 0.5,
211
+ duration: 1.2,
212
+ ease: "back.out(1.7)",
213
+ from: { x: -20, y: 0, scale: 0.92, autoAlpha: 0 },
214
+ to: { x: 0, y: 0, scale: 1, autoAlpha: 1 },
215
+ });
216
+
217
+ expect(updated.motions).toHaveLength(1);
218
+ expect(updated.motions[0]).toMatchObject({
219
+ kind: "gsap-motion",
220
+ target: { sourceFile: "index.html", selector: "#card", id: "card" },
221
+ start: 0.5,
222
+ duration: 1.2,
223
+ ease: "back.out(1.7)",
224
+ from: { x: -20, y: 0, scale: 0.92, autoAlpha: 0 },
225
+ to: { x: 0, y: 0, scale: 1, autoAlpha: 1 },
226
+ });
227
+ expect(parseStudioMotionManifest(serializeStudioMotionManifest(updated))).toEqual(updated);
228
+ expect(getStudioMotionForSelection(updated, selection)?.duration).toBe(1.2);
229
+ });
230
+
231
+ it("rejects malformed motions without throwing and removes selected motion", () => {
232
+ const parsed = parseStudioMotionManifest(`{
233
+ "motions": [
234
+ { "kind": "gsap-motion", "target": { "sourceFile": "index.html", "id": "card" }, "start": 0, "duration": 0 },
235
+ { "kind": "gsap-motion", "target": { "sourceFile": "index.html", "id": "card" }, "start": 0, "duration": 1, "ease": "power2.out", "from": { "x": 0 }, "to": { "x": 20 } }
236
+ ]
237
+ }`);
238
+
239
+ expect(parsed.motions).toHaveLength(1);
240
+ expect(removeStudioMotionForSelection(parsed, createSelection()).motions).toEqual([]);
241
+ expect(isStudioMotionManifestPath(".hyperframes/studio-motion.json")).toBe(true);
242
+ expect(isStudioMotionManifestPath("index.html")).toBe(false);
243
+ });
244
+
245
+ it("builds a paused GSAP timeline, registers it, and restores Studio-owned props on rebuild", () => {
246
+ const document = createDocument(
247
+ '<div id="card" style="transform: rotate(5deg); opacity: 0.8"></div>',
248
+ );
249
+ const win = document.defaultView;
250
+ if (!win) throw new Error("window fixture missing");
251
+ const gsapState = installFakeGsap(win as Window);
252
+ const card = document.getElementById("card");
253
+ if (!(card instanceof win.HTMLElement)) throw new Error("card fixture missing");
254
+ const manifest = upsertStudioGsapMotion(emptyStudioMotionManifest(), createSelection(), {
255
+ start: 0.25,
256
+ duration: 0.8,
257
+ ease: "power3.out",
258
+ from: { x: 0, y: 44, scale: 1, autoAlpha: 0 },
259
+ to: { x: 0, y: 0, scale: 1, autoAlpha: 1 },
260
+ });
261
+
262
+ expect(applyStudioMotionManifest(document, manifest, "index.html", 0.4)).toBe(1);
263
+ expect(gsapState.fromToCalls[0]).toMatchObject({
264
+ target: card,
265
+ from: { x: 0, y: 44, scale: 1, autoAlpha: 0 },
266
+ to: { x: 0, y: 0, scale: 1, autoAlpha: 1, duration: 0.8, ease: "power3.out" },
267
+ at: 0.25,
268
+ });
269
+ expect(gsapState.timeCalls).toContain(0.4);
270
+ expect(
271
+ (win as unknown as { __timelines?: Record<string, unknown> }).__timelines?.[
272
+ STUDIO_MOTION_TIMELINE_ID
273
+ ],
274
+ ).toBeTruthy();
275
+
276
+ card.style.setProperty("transform", "matrix(1, 0, 0, 1, 10, 20)");
277
+ card.style.setProperty("opacity", "0.1");
278
+ expect(applyStudioMotionManifest(document, emptyStudioMotionManifest(), "index.html", 0)).toBe(
279
+ 0,
280
+ );
281
+ expect(gsapState.killCalls).toBe(1);
282
+ expect(card.style.getPropertyValue("transform")).toBe("rotate(5deg)");
283
+ expect(card.style.getPropertyValue("opacity")).toBe("0.8");
284
+ expect(
285
+ (win as unknown as { __timelines?: Record<string, unknown> }).__timelines?.[
286
+ STUDIO_MOTION_TIMELINE_ID
287
+ ],
288
+ ).toBeUndefined();
289
+ });
290
+
291
+ it("rebuilds from the latest in-memory manifest when a second layer is added", () => {
292
+ const document = createDocument('<div id="card"></div><div id="badge"></div>');
293
+ const win = document.defaultView;
294
+ if (!win) throw new Error("window fixture missing");
295
+ const gsapState = installFakeGsap(win as Window);
296
+ const badgeSelection = {
297
+ ...createSelection(),
298
+ id: "badge",
299
+ selector: "#badge",
300
+ label: "Badge",
301
+ };
302
+ const firstManifest = upsertStudioGsapMotion(emptyStudioMotionManifest(), createSelection(), {
303
+ start: 0,
304
+ duration: 0.6,
305
+ ease: "power3.out",
306
+ from: { y: 32, autoAlpha: 0 },
307
+ to: { y: 0, autoAlpha: 1 },
308
+ });
309
+ const nextManifest = upsertStudioGsapMotion(firstManifest, badgeSelection, {
310
+ start: 0,
311
+ duration: 0.6,
312
+ ease: "power3.out",
313
+ customEase: {
314
+ id: "studio-badge-ease",
315
+ data: "M0,0 C0.2,0.9 0.28,1 1,1",
316
+ },
317
+ from: { x: -32, autoAlpha: 0 },
318
+ to: { x: 0, autoAlpha: 1 },
319
+ });
320
+
321
+ expect(applyStudioMotionManifest(document, firstManifest, "index.html", 0.3)).toBe(1);
322
+ expect(applyStudioMotionManifest(document, nextManifest, "index.html", 0.3)).toBe(2);
323
+ expect(gsapState.fromToCalls.at(-2)?.target.id).toBe("card");
324
+ expect(gsapState.fromToCalls.at(-1)).toMatchObject({
325
+ target: document.getElementById("badge"),
326
+ from: { x: -32, autoAlpha: 0 },
327
+ to: { x: 0, autoAlpha: 1, duration: 0.6, ease: "studio-badge-ease" },
328
+ at: 0,
329
+ });
330
+ });
331
+
332
+ it("registers CustomEase data when the selected GSAP plugin is available", () => {
333
+ const document = createDocument('<div id="card"></div>');
334
+ const win = document.defaultView;
335
+ if (!win) throw new Error("window fixture missing");
336
+ const gsapState = installFakeGsap(win as Window);
337
+ const manifest = upsertStudioGsapMotion(emptyStudioMotionManifest(), createSelection(), {
338
+ start: 0,
339
+ duration: 1,
340
+ ease: "studio-card-bounce",
341
+ customEase: {
342
+ id: "studio-card-bounce",
343
+ data: "M0,0 C0.18,0.9 0.32,1 1,1",
344
+ },
345
+ from: { y: 44 },
346
+ to: { y: 0 },
347
+ });
348
+
349
+ expect(applyStudioMotionManifest(document, manifest, "index.html", 0)).toBe(1);
350
+ expect(gsapState.customEaseCalls).toEqual([
351
+ { id: "studio-card-bounce", data: "M0,0 C0.18,0.9 0.32,1 1,1" },
352
+ ]);
353
+ expect(gsapState.fromToCalls[0]?.to.ease).toBe("studio-card-bounce");
354
+ });
355
+ });