@hyperframes/studio 0.7.61 → 0.7.63

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 (84) hide show
  1. package/dist/assets/{hyperframes-player-Xvx2hkrc.js → hyperframes-player-BO2UXx-E.js} +1 -1
  2. package/dist/assets/{index-D-uFclFj.js → index-C0A0DlLk.js} +1 -1
  3. package/dist/assets/index-C_7k3kwr.js +426 -0
  4. package/dist/assets/index-Ceyz8Qt2.css +1 -0
  5. package/dist/assets/{index-BCpoiv9S.js → index-_72wCKSP.js} +1 -1
  6. package/dist/index.d.ts +12 -1
  7. package/dist/index.html +2 -2
  8. package/dist/index.js +3504 -2955
  9. package/dist/index.js.map +1 -1
  10. package/package.json +7 -7
  11. package/src/App.tsx +15 -10
  12. package/src/components/EditorShell.tsx +12 -0
  13. package/src/components/StudioLeftSidebar.tsx +3 -0
  14. package/src/components/editor/DomEditSelectionChrome.test.tsx +51 -0
  15. package/src/components/editor/PropertyPanelFlat.tsx +6 -0
  16. package/src/components/editor/compositionReliabilityFixture.integration.test.ts +105 -0
  17. package/src/components/editor/domEditing.test.ts +34 -0
  18. package/src/components/editor/propertyPanelColor.test.tsx +31 -0
  19. package/src/components/editor/propertyPanelColor.tsx +150 -28
  20. package/src/components/editor/propertyPanelCommitField.tsx +178 -0
  21. package/src/components/editor/propertyPanelFlatPrimitives.test.tsx +34 -0
  22. package/src/components/editor/propertyPanelFlatPrimitives.tsx +3 -0
  23. package/src/components/editor/propertyPanelFlatStyleSections.test.tsx +5 -1
  24. package/src/components/editor/propertyPanelFlatStyleSections.tsx +6 -0
  25. package/src/components/editor/propertyPanelFlatTextSection.test.tsx +47 -4
  26. package/src/components/editor/propertyPanelFlatTextSection.tsx +8 -0
  27. package/src/components/editor/propertyPanelInputCoverage.test.tsx +4 -1
  28. package/src/components/editor/propertyPanelPrimitives.tsx +3 -100
  29. package/src/components/editor/propertyPanelSections.tsx +2 -2
  30. package/src/components/editor/propertyPanelTypes.ts +2 -0
  31. package/src/components/editor/useInspectorGestureTransaction.test.tsx +37 -0
  32. package/src/components/editor/useInspectorGestureTransaction.ts +60 -0
  33. package/src/components/nle/TimelinePane.tsx +6 -0
  34. package/src/components/sidebar/CompositionsTab.drag.test.tsx +87 -0
  35. package/src/components/sidebar/CompositionsTab.tsx +45 -2
  36. package/src/components/sidebar/LeftSidebar.tsx +3 -0
  37. package/src/contexts/StudioContext.tsx +4 -0
  38. package/src/hooks/timelineEditingHelpers.ts +10 -4
  39. package/src/hooks/usePreviewPersistence.ts +7 -1
  40. package/src/hooks/useRazorSplit.history.test.tsx +21 -28
  41. package/src/hooks/useRazorSplit.test.tsx +20 -9
  42. package/src/hooks/useRazorSplit.testHelpers.ts +34 -22
  43. package/src/hooks/useRazorSplit.ts +78 -350
  44. package/src/hooks/useTimelineAddAtPlayhead.ts +15 -0
  45. package/src/hooks/useTimelineAssetDropOps.ts +52 -2
  46. package/src/hooks/useTimelineEditing.ts +19 -17
  47. package/src/hooks/useTimelineGroupEditing.ts +6 -7
  48. package/src/player/components/Timeline.tsx +12 -1
  49. package/src/player/components/timelineAuthoredMoveTarget.ts +77 -0
  50. package/src/player/components/timelineAuthoredTrack.ts +29 -0
  51. package/src/player/components/timelineCallbacks.ts +4 -0
  52. package/src/player/components/timelineClipDragCommit.test.ts +176 -0
  53. package/src/player/components/timelineClipDragCommit.ts +77 -105
  54. package/src/player/components/timelineClipDragPreview.test.ts +33 -1
  55. package/src/player/components/timelineClipDragPreview.ts +2 -1
  56. package/src/player/components/timelineCollision.test.ts +18 -0
  57. package/src/player/components/timelineCollision.ts +12 -9
  58. package/src/player/components/timelineDragDrop.ts +28 -7
  59. package/src/player/components/timelineEditCapabilities.ts +4 -1
  60. package/src/player/components/timelineGroupEditing.test.ts +10 -0
  61. package/src/player/components/timelineGroupEditing.ts +4 -3
  62. package/src/player/components/timelineLaneMoveRefresh.ts +14 -0
  63. package/src/player/components/timelineLayout.ts +3 -1
  64. package/src/player/components/timelineZMirror.ts +1 -1
  65. package/src/player/components/useTimelineClipDrag.ts +5 -0
  66. package/src/player/components/useTimelineEditPinning.ts +12 -0
  67. package/src/player/hooks/useExpandedTimelineElements.test.ts +42 -0
  68. package/src/player/hooks/useExpandedTimelineElements.ts +3 -0
  69. package/src/player/lib/playbackTypes.ts +2 -0
  70. package/src/player/lib/timelineDOM.test.ts +58 -0
  71. package/src/player/lib/timelineDOM.ts +18 -1
  72. package/src/player/lib/timelineElementHelpers.ts +21 -11
  73. package/src/player/store/playerStore.ts +2 -1
  74. package/src/utils/razorSplitTransaction.test.ts +175 -0
  75. package/src/utils/razorSplitTransaction.ts +200 -0
  76. package/src/utils/studioPreviewHelpers.test.ts +27 -0
  77. package/src/utils/timelineCompositionDrop.test.ts +12 -0
  78. package/src/utils/timelineCompositionDrop.ts +16 -0
  79. package/src/utils/timelineCompositionInsert.test.ts +142 -0
  80. package/src/utils/timelineCompositionInsert.ts +90 -0
  81. package/src/utils/timelineElementSplit.test.ts +14 -2
  82. package/src/utils/timelineElementSplit.ts +5 -1
  83. package/dist/assets/index-2mxh_HSy.js +0 -426
  84. package/dist/assets/index-BhWig0mx.css +0 -1
@@ -166,6 +166,14 @@ export function patchIframeDomTiming(
166
166
  // Cross-origin or mid-navigation — file save is enqueued; iframe patch is best-effort.
167
167
  }
168
168
  }
169
+
170
+ export function playbackStartAttributeForElement(
171
+ element: Pick<TimelineElement, "kind" | "playbackStartAttr">,
172
+ ): "data-media-start" | "data-playback-start" {
173
+ return element.playbackStartAttr === "playback-start" || element.kind === "composition"
174
+ ? "data-playback-start"
175
+ : "data-media-start";
176
+ }
169
177
  // fallow-ignore-next-line complexity
170
178
  function resolveResizePlaybackStart(
171
179
  original: string,
@@ -174,8 +182,7 @@ function resolveResizePlaybackStart(
174
182
  updates: Pick<TimelineElement, "start" | "playbackStart">,
175
183
  ): { attrName: string; value: number } | null {
176
184
  if (updates.playbackStart != null) {
177
- const attrName =
178
- element.playbackStartAttr === "playback-start" ? "playback-start" : "media-start";
185
+ const attrName = playbackStartAttributeForElement(element).slice("data-".length);
179
186
  return { attrName, value: updates.playbackStart };
180
187
  }
181
188
  const trimDelta = updates.start - element.start;
@@ -185,8 +192,7 @@ function resolveResizePlaybackStart(
185
192
  readAttributeByTarget(original, target, "media-start");
186
193
  const current = raw != null ? parseFloat(raw) : undefined;
187
194
  if (current == null || !Number.isFinite(current)) return null;
188
- const attrName =
189
- element.playbackStartAttr === "playback-start" ? "playback-start" : "media-start";
195
+ const attrName = playbackStartAttributeForElement(element).slice("data-".length);
190
196
  return {
191
197
  attrName,
192
198
  value: Math.max(0, current + trimDelta * Math.max(element.playbackRate ?? 1, 0.1)),
@@ -208,13 +208,19 @@ export function usePreviewPersistence({
208
208
  // attributes onto the live DOM and re-runs the timeline at the SAME playhead,
209
209
  // falling back to reloadPreview for anything structural (split/delete undo),
210
210
  // multi-file, sub-comp, or a permanent soft-reload failure.
211
- applyUndoRestoreToPreview(
211
+ const strategy = applyUndoRestoreToPreview(
212
212
  previewIframeRef.current,
213
213
  activeCompPathRef.current,
214
214
  restore.files,
215
215
  usePlayerStore.getState().currentTime,
216
216
  reloadPreview,
217
217
  );
218
+ if (strategy === "full") {
219
+ const player = usePlayerStore.getState();
220
+ player.setElements([]);
221
+ player.setSelectedElementId(null);
222
+ player.setTimelineReady(false);
223
+ }
218
224
  },
219
225
  [previewIframeRef, activeCompPathRef, reloadPreview],
220
226
  );
@@ -67,43 +67,36 @@ function mountRazorSplit(opts: { gsap?: boolean; previewStamp?: boolean } = {}):
67
67
  onChange: () => {},
68
68
  });
69
69
 
70
- // Faithful stand-in for the studio-server file-mutation endpoints: the server
71
- // writes the split to disk itself, then returns the patched content.
70
+ // Faithful stand-in for the atomic server cut: one forward file write and one
71
+ // response carrying the canonical history snapshots.
72
72
  const fetchMock = vi.fn(async (url: string, init?: RequestInit) => {
73
73
  const u = String(url);
74
- if (u.includes("/gsap-mutations/")) {
75
- if (opts.gsap) {
76
- // Mirror the server: rewrites the GSAP script for the new id, writes to
77
- // disk, returns the final content.
78
- disk["index.html"] = SPLIT_GSAP;
79
- const version = `"test-gsap-${SPLIT_GSAP.length}"`;
80
- return new Response(JSON.stringify({ ok: true, after: SPLIT_GSAP, version }), {
81
- status: 200,
82
- headers: { "Content-Type": "application/json", ETag: version },
83
- });
84
- }
85
- // The fixture has no GSAP script — mirror the server's 400 response.
86
- return new Response(JSON.stringify({ error: "no GSAP script found in file" }), {
87
- status: 400,
88
- headers: { "Content-Type": "application/json" },
89
- });
90
- }
91
- if (u.includes("/file-mutations/split-element/")) {
92
- disk["index.html"] = SPLIT;
93
- const version = `"test-split-${SPLIT.length}"`;
74
+ if (u.includes("/file-mutations/split-batch")) {
75
+ const before = disk["index.html"];
76
+ disk["index.html"] = finalContent;
77
+ const version = `"test-cut-${finalContent.length}"`;
94
78
  return new Response(
95
79
  JSON.stringify({
96
80
  ok: true,
97
- changed: true,
98
- content: SPLIT,
99
- newId: "clip1-split",
100
- version,
81
+ outcome: "committed",
82
+ files: [
83
+ {
84
+ path: "index.html",
85
+ before,
86
+ after: finalContent,
87
+ version,
88
+ writeToken: "test-cut",
89
+ splitCount: 1,
90
+ skippedSelectors: [],
91
+ },
92
+ ],
101
93
  }),
102
- { status: 200, headers: { "Content-Type": "application/json", ETag: version } },
94
+ { status: 200, headers: { "Content-Type": "application/json" } },
103
95
  );
104
96
  }
105
97
  if (u.includes("/files/")) {
106
- return new Response(JSON.stringify({ content: disk["index.html"] }), {
98
+ const content = disk["index.html"];
99
+ return new Response(JSON.stringify({ content, version: `"test-${content.length}"` }), {
107
100
  status: 200,
108
101
  headers: { "Content-Type": "application/json" },
109
102
  });
@@ -17,19 +17,30 @@ function jsonResponse(body: unknown, status = 200): Response {
17
17
  describe("useRazorSplit mutation versions", () => {
18
18
  afterEach(() => vi.restoreAllMocks());
19
19
 
20
- it("observes each out-of-band mutation version before the OCC writer runs", async () => {
20
+ it("observes the batch version without a redundant client forward write", async () => {
21
21
  const original = '<div id="clip" data-start="0" data-duration="4">Clip</div>';
22
22
  const htmlSplit =
23
23
  '<div id="clip" data-start="0" data-duration="2">Clip</div><div id="clip-split" data-start="2" data-duration="2">Clip</div>';
24
24
  const final = `${htmlSplit}<script>window.__timelines = {}</script>`;
25
25
  vi.spyOn(globalThis, "fetch").mockImplementation(async (input) => {
26
26
  const url = String(input);
27
- if (url.includes("/files/")) return jsonResponse({ content: original });
28
- if (url.includes("/file-mutations/split-element/")) {
29
- return jsonResponse({ ok: true, changed: true, content: htmlSplit, version: '"v-html"' });
30
- }
31
- if (url.includes("/gsap-mutations/")) {
32
- return jsonResponse({ ok: true, changed: true, after: final, version: '"v-gsap"' });
27
+ if (url.includes("/files/")) return jsonResponse({ content: original, version: '"v0"' });
28
+ if (url.includes("/file-mutations/split-batch")) {
29
+ return jsonResponse({
30
+ ok: true,
31
+ outcome: "committed",
32
+ files: [
33
+ {
34
+ path: "index.html",
35
+ before: original,
36
+ after: final,
37
+ version: '"v-cut"',
38
+ writeToken: "cut-1",
39
+ splitCount: 1,
40
+ skippedSelectors: [],
41
+ },
42
+ ],
43
+ });
33
44
  }
34
45
  throw new Error(`Unexpected request: ${url}`);
35
46
  });
@@ -78,8 +89,8 @@ describe("useRazorSplit mutation versions", () => {
78
89
  );
79
90
  });
80
91
 
81
- expect(order).toEqual(['observe:index.html:"v-html"', 'observe:index.html:"v-gsap"', "write"]);
82
- expect(writeProjectFile).toHaveBeenCalledWith("index.html", final, original);
92
+ expect(order).toEqual(['observe:index.html:"v-cut"']);
93
+ expect(writeProjectFile).not.toHaveBeenCalled();
83
94
  expect(recordEdit).toHaveBeenCalledTimes(1);
84
95
 
85
96
  act(() => root.unmount());
@@ -8,15 +8,19 @@ export interface SplitBody {
8
8
  elementDuration: number;
9
9
  }
10
10
 
11
+ interface BatchFileBody {
12
+ path: string;
13
+ targets: SplitBody[];
14
+ }
15
+
11
16
  function decodePathFromUrl(url: string, marker: string): string {
12
17
  const encoded = url.slice(url.indexOf(marker) + marker.length);
13
18
  return decodeURIComponent(encoded);
14
19
  }
15
20
 
16
21
  /**
17
- * Fetch mock shared by both harnesses: GSAP mutations 400 (no script in fixtures),
18
- * split-element writes a `<!--split-->` marker so `changed` is true, and file reads
19
- * echo the in-memory `disk`. `onSplit` (when set) records each split request's body.
22
+ * Fetch mock shared by both harnesses: the atomic split batch writes each file
23
+ * once and returns its canonical snapshots; file reads echo the in-memory disk.
20
24
  */
21
25
  export function createSplitFetchMock(
22
26
  disk: Record<string, string>,
@@ -24,31 +28,39 @@ export function createSplitFetchMock(
24
28
  ) {
25
29
  return vi.fn(async (url: string, init?: RequestInit) => {
26
30
  const u = String(url);
27
- if (u.includes("/gsap-mutations/")) {
28
- // No GSAP script in the fixtures mirror the server's 400 response.
29
- return new Response(JSON.stringify({ error: "no GSAP script found in file" }), {
30
- status: 400,
31
- headers: { "Content-Type": "application/json" },
31
+ if (u.includes("/file-mutations/split-batch")) {
32
+ const body = JSON.parse(String(init?.body)) as { files: BatchFileBody[] };
33
+ const files = body.files.map((file) => {
34
+ const before = disk[file.path];
35
+ for (const target of file.targets) onSplit?.(file.path, target);
36
+ const after = `${before}${"<!--split-->".repeat(file.targets.length)}`;
37
+ const version = `"test-${file.path}-${after.length}"`;
38
+ return {
39
+ path: file.path,
40
+ before,
41
+ after,
42
+ version,
43
+ writeToken: "test-cut",
44
+ splitCount: file.targets.length,
45
+ skippedSelectors: [],
46
+ };
32
47
  });
33
- }
34
- if (u.includes("/file-mutations/split-element/")) {
35
- const path = decodePathFromUrl(u, "/file-mutations/split-element/");
36
- onSplit?.(path, JSON.parse(String(init?.body)) as SplitBody);
37
- // Return content that differs from the original so `changed` is true.
38
- const after = `${disk[path]}<!--split-->`;
39
- const version = `"test-${path}-${after.length}"`;
40
- disk[path] = after; // server writes the split to disk
41
- return new Response(JSON.stringify({ ok: true, changed: true, content: after, version }), {
48
+ for (const file of files) disk[file.path] = file.after;
49
+ return new Response(JSON.stringify({ ok: true, outcome: "committed", files }), {
42
50
  status: 200,
43
- headers: { "Content-Type": "application/json", ETag: version },
51
+ headers: { "Content-Type": "application/json" },
44
52
  });
45
53
  }
46
54
  if (u.includes("/files/")) {
47
55
  const path = decodePathFromUrl(u, "/files/").replace(/\?.*$/, "");
48
- return new Response(JSON.stringify({ content: disk[path] ?? "" }), {
49
- status: 200,
50
- headers: { "Content-Type": "application/json" },
51
- });
56
+ const content = disk[path] ?? "";
57
+ return new Response(
58
+ JSON.stringify({ content, version: `"test-${path}-${content.length}"` }),
59
+ {
60
+ status: 200,
61
+ headers: { "Content-Type": "application/json" },
62
+ },
63
+ );
52
64
  }
53
65
  void init;
54
66
  throw new Error(`unexpected fetch: ${u}`);