@hyperframes/studio 0.8.35 → 0.8.36

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 (78) hide show
  1. package/dist/assets/{hyperframes-player-lPOqAC3l.js → hyperframes-player-BIpJmC69.js} +1 -1
  2. package/dist/assets/index-BmPeyP0k.js +441 -0
  3. package/dist/assets/{index-ywAULfGT.js → index-Df3VgdgL.js} +1 -1
  4. package/dist/assets/{index-RPEcu_bT.js → index-z2_QKypm.js} +1 -1
  5. package/dist/index.html +1 -1
  6. package/dist/index.js +61 -195
  7. package/dist/index.js.map +1 -1
  8. package/package.json +7 -7
  9. package/src/App.tsx +0 -7
  10. package/src/components/StudioRightPanel.tsx +0 -7
  11. package/src/components/StudioRightPanel.types.ts +0 -2
  12. package/src/components/editor/SnapToolbar.test.tsx +0 -2
  13. package/src/components/editor/manualEdits.test.ts +3 -3
  14. package/src/components/editor/manualEdits.ts +1 -1
  15. package/src/components/editor/manualEditsParsing.ts +12 -21
  16. package/src/components/panels/VariablesOtherCompositions.tsx +1 -4
  17. package/src/components/panels/VariablesPanel.tsx +1 -5
  18. package/src/components/studioColorGradingScope.ts +0 -4
  19. package/src/hooks/elementLifecycleOpsTestUtils.ts +0 -1
  20. package/src/hooks/externalFileChangeOwnership.test.ts +40 -1
  21. package/src/hooks/gsapScriptCommitTypes.ts +0 -1
  22. package/src/hooks/sdkSelfWriteRegistry.ts +1 -1
  23. package/src/hooks/timelineAudioGroupCreate.ts +0 -7
  24. package/src/hooks/timelineAudioGroupVolume.test.ts +0 -1
  25. package/src/hooks/timelineAudioGroupVolume.ts +0 -6
  26. package/src/hooks/timelineEditingHelpers.test.ts +0 -3
  27. package/src/hooks/timelineEditingHelpers.ts +0 -10
  28. package/src/hooks/timelineElementFxAttribute.ts +0 -6
  29. package/src/hooks/timelineTrackVisibility.test.ts +0 -17
  30. package/src/hooks/timelineTrackVisibility.ts +0 -15
  31. package/src/hooks/useAppHotkeys.previewForwarding.test.tsx +0 -1
  32. package/src/hooks/useAppHotkeys.textEditing.test.tsx +0 -2
  33. package/src/hooks/useAppHotkeys.ts +1 -7
  34. package/src/hooks/useAudioGroupCarveAssignment.test.tsx +0 -1
  35. package/src/hooks/useClipboard.ts +1 -11
  36. package/src/hooks/useDomEditCommits.test.tsx +0 -2
  37. package/src/hooks/useDomEditCommits.ts +2 -21
  38. package/src/hooks/useDomEditSession.test.tsx +0 -1
  39. package/src/hooks/useDomEditSession.ts +0 -7
  40. package/src/hooks/useEditorSave.test.tsx +0 -1
  41. package/src/hooks/useEditorSave.ts +2 -6
  42. package/src/hooks/useElementLifecycleOps.ts +0 -3
  43. package/src/hooks/useExternalFileChangeCoordinator.test.tsx +54 -0
  44. package/src/hooks/useExternalFileChangeCoordinator.ts +38 -22
  45. package/src/hooks/useFileManager.projectOwnership.test.tsx +0 -1
  46. package/src/hooks/useFileManager.ts +0 -3
  47. package/src/hooks/useGroupCommits.ts +0 -2
  48. package/src/hooks/useGsapScriptCommits.test.tsx +0 -2
  49. package/src/hooks/useGsapScriptCommits.ts +2 -5
  50. package/src/hooks/usePreviewInteraction.test.ts +14 -5
  51. package/src/hooks/usePreviewInteraction.ts +3 -3
  52. package/src/hooks/useProjectCompositionVariables.ts +3 -6
  53. package/src/hooks/useRazorSplit.history.test.tsx +0 -2
  54. package/src/hooks/useRazorSplit.test.ts +0 -2
  55. package/src/hooks/useRazorSplit.test.tsx +0 -1
  56. package/src/hooks/useRazorSplit.ts +0 -6
  57. package/src/hooks/useSdkSession.ts +3 -11
  58. package/src/hooks/useSlideshowPersist.ts +1 -5
  59. package/src/hooks/useTimelineAssetDropOps.ts +0 -6
  60. package/src/hooks/useTimelineDeleteOps.ts +0 -4
  61. package/src/hooks/useTimelineEditing.test.tsx +0 -3
  62. package/src/hooks/useTimelineEditing.ts +0 -16
  63. package/src/hooks/useTimelineEditingTypes.ts +0 -1
  64. package/src/hooks/useTimelineGroupEditing.ts +0 -6
  65. package/src/hooks/useVariablesPersist.ts +0 -3
  66. package/src/player/components/Player.tsx +18 -5
  67. package/src/player/components/playerModuleRetry.test.ts +41 -0
  68. package/src/player/lib/playbackTypes.ts +7 -0
  69. package/src/player/lib/timelineIframeHelpers.test.ts +30 -0
  70. package/src/player/lib/timelineIframeHelpers.ts +16 -0
  71. package/src/utils/sdkCutover.familyGate.test.ts +0 -1
  72. package/src/utils/sdkCutover.gate.test.ts +0 -1
  73. package/src/utils/sdkCutover.test.ts +0 -27
  74. package/src/utils/sdkEditTransaction.ts +0 -4
  75. package/src/utils/setSlideshowManifest.test.ts +0 -1
  76. package/src/utils/studioFileHistory.ts +0 -1
  77. package/dist/assets/index-BRr1JoHX.js +0 -441
  78. package/src/hooks/useSdkSession.test.ts +0 -32
@@ -43,7 +43,6 @@ export function useTimelineEditing({
43
43
  writeProjectFile,
44
44
  observeProjectFileVersion,
45
45
  recordEdit,
46
- domEditSaveTimestampRef,
47
46
  reloadPreview,
48
47
  previewIframeRef,
49
48
  pendingTimelineEditPathRef,
@@ -82,7 +81,6 @@ export function useTimelineEditing({
82
81
  buildPatches,
83
82
  writeProjectFile,
84
83
  recordEdit,
85
- domEditSaveTimestampRef,
86
84
  pendingTimelineEditPathRef,
87
85
  coalesceKey,
88
86
  }),
@@ -99,7 +97,6 @@ export function useTimelineEditing({
99
97
  activeCompPath,
100
98
  recordEdit,
101
99
  writeProjectFile,
102
- domEditSaveTimestampRef,
103
100
  pendingTimelineEditPathRef,
104
101
  showToast,
105
102
  isRecordingRef,
@@ -108,7 +105,6 @@ export function useTimelineEditing({
108
105
  );
109
106
  const groupEditing = useTimelineGroupEditing({
110
107
  activeCompPath,
111
- domEditSaveTimestampRef,
112
108
  editQueueRef,
113
109
  forceReloadSdkSession,
114
110
  invalidateGsapCache,
@@ -211,7 +207,6 @@ export function useTimelineEditing({
211
207
  editHistory: { recordEdit },
212
208
  writeProjectFile,
213
209
  reloadPreview,
214
- domEditSaveTimestampRef,
215
210
  compositionPath: activeCompPath,
216
211
  // Capture on-disk bytes as the undo `before` so undoing a timing move
217
212
  // restores the file verbatim, not a normalized full-DOM re-emit.
@@ -244,7 +239,6 @@ export function useTimelineEditing({
244
239
  recordEdit,
245
240
  writeProjectFile,
246
241
  reloadPreview,
247
- domEditSaveTimestampRef,
248
242
  timelineElements,
249
243
  handleDomZIndexReorderCommitRef,
250
244
  showToast,
@@ -317,7 +311,6 @@ export function useTimelineEditing({
317
311
  editHistory: { recordEdit },
318
312
  writeProjectFile,
319
313
  reloadPreview,
320
- domEditSaveTimestampRef,
321
314
  compositionPath: activeCompPath,
322
315
  // Capture on-disk bytes as the undo `before` so undoing a timing
323
316
  // resize restores the file verbatim, not a normalized full-DOM re-emit.
@@ -346,7 +339,6 @@ export function useTimelineEditing({
346
339
  recordEdit,
347
340
  writeProjectFile,
348
341
  reloadPreview,
349
- domEditSaveTimestampRef,
350
342
  showToast,
351
343
  invalidateGsapCache,
352
344
  ],
@@ -359,7 +351,6 @@ export function useTimelineEditing({
359
351
  showToast,
360
352
  writeProjectFile,
361
353
  recordEdit,
362
- domEditSaveTimestampRef,
363
354
  previewIframeRef,
364
355
  pendingTimelineEditPathRef,
365
356
  isRecordingRef,
@@ -372,7 +363,6 @@ export function useTimelineEditing({
372
363
  showToast,
373
364
  writeProjectFile,
374
365
  recordEdit,
375
- domEditSaveTimestampRef,
376
366
  previewIframeRef,
377
367
  pendingTimelineEditPathRef,
378
368
  isRecordingRef,
@@ -385,7 +375,6 @@ export function useTimelineEditing({
385
375
  showToast,
386
376
  writeProjectFile,
387
377
  recordEdit,
388
- domEditSaveTimestampRef,
389
378
  previewIframeRef,
390
379
  pendingTimelineEditPathRef,
391
380
  isRecordingRef,
@@ -397,7 +386,6 @@ export function useTimelineEditing({
397
386
  showToast,
398
387
  writeProjectFile,
399
388
  recordEdit,
400
- domEditSaveTimestampRef,
401
389
  previewIframeRef,
402
390
  pendingTimelineEditPathRef,
403
391
  isRecordingRef,
@@ -409,7 +397,6 @@ export function useTimelineEditing({
409
397
  showToast,
410
398
  writeProjectFile,
411
399
  recordEdit,
412
- domEditSaveTimestampRef,
413
400
  previewIframeRef,
414
401
  pendingTimelineEditPathRef,
415
402
  isRecordingRef,
@@ -422,7 +409,6 @@ export function useTimelineEditing({
422
409
  showToast,
423
410
  writeProjectFile,
424
411
  recordEdit,
425
- domEditSaveTimestampRef,
426
412
  reloadPreview,
427
413
  isRecordingRef,
428
414
  forceReloadSdkSession,
@@ -437,7 +423,6 @@ export function useTimelineEditing({
437
423
  showToast,
438
424
  writeProjectFile,
439
425
  recordEdit,
440
- domEditSaveTimestampRef,
441
426
  reloadPreview,
442
427
  uploadProjectFiles,
443
428
  isRecordingRef,
@@ -454,7 +439,6 @@ export function useTimelineEditing({
454
439
  writeProjectFile,
455
440
  observeProjectFileVersion,
456
441
  recordEdit,
457
- domEditSaveTimestampRef,
458
442
  reloadPreview,
459
443
  isRecordingRef,
460
444
  forceReloadSdkSession,
@@ -35,7 +35,6 @@ export interface UseTimelineEditingOptions {
35
35
  writeProjectFile: (path: string, content: string, expectedContent?: string) => Promise<void>;
36
36
  observeProjectFileVersion?: (path: string, version: string | null) => void;
37
37
  recordEdit: (input: RecordEditInput) => Promise<void>;
38
- domEditSaveTimestampRef: MutableRefObject<number>;
39
38
  reloadPreview: () => void;
40
39
  previewIframeRef: RefObject<HTMLIFrameElement | null>;
41
40
  pendingTimelineEditPathRef: MutableRefObject<Set<string>>;
@@ -51,7 +51,6 @@ export interface TimelineGroupCommitOptions {
51
51
 
52
52
  interface UseTimelineGroupEditingOptions {
53
53
  activeCompPath: string | null;
54
- domEditSaveTimestampRef: MutableRefObject<number>;
55
54
  editQueueRef: MutableRefObject<Promise<unknown>>;
56
55
  forceReloadSdkSession?: () => void;
57
56
  invalidateGsapCache?: () => void;
@@ -108,7 +107,6 @@ function resizeHasPlaybackStartAdjustment(change: TimelineGroupResizeChange): bo
108
107
 
109
108
  export function useTimelineGroupEditing({
110
109
  activeCompPath,
111
- domEditSaveTimestampRef,
112
110
  editQueueRef,
113
111
  forceReloadSdkSession,
114
112
  invalidateGsapCache,
@@ -160,7 +158,6 @@ export function useTimelineGroupEditing({
160
158
  changes: batchChanges,
161
159
  writeProjectFile,
162
160
  recordEdit,
163
- domEditSaveTimestampRef,
164
161
  pendingTimelineEditPathRef,
165
162
  coalesceKey,
166
163
  coalesceMs,
@@ -169,7 +166,6 @@ export function useTimelineGroupEditing({
169
166
  },
170
167
  [
171
168
  activeCompPath,
172
- domEditSaveTimestampRef,
173
169
  forceReloadSdkSession,
174
170
  pendingTimelineEditPathRef,
175
171
  recordEdit,
@@ -209,7 +205,6 @@ export function useTimelineGroupEditing({
209
205
  editHistory: { recordEdit },
210
206
  writeProjectFile,
211
207
  reloadPreview,
212
- domEditSaveTimestampRef,
213
208
  compositionPath: activeCompPath,
214
209
  readProjectFile: (path) => readFileContent(projectIdRef.current ?? "", path),
215
210
  publishSession: publishSdkSession,
@@ -225,7 +220,6 @@ export function useTimelineGroupEditing({
225
220
  },
226
221
  [
227
222
  activeCompPath,
228
- domEditSaveTimestampRef,
229
223
  projectIdRef,
230
224
  publishSdkSession,
231
225
  recordEdit,
@@ -20,7 +20,6 @@ export function useVariablesPersist({
20
20
  writeProjectFile,
21
21
  recordEdit,
22
22
  reloadPreview,
23
- domEditSaveTimestampRef,
24
23
  publishSdkSession,
25
24
  }: UseVariablesPersistParams): (
26
25
  label: string,
@@ -39,7 +38,6 @@ export function useVariablesPersist({
39
38
  editHistory: { recordEdit },
40
39
  writeProjectFile,
41
40
  reloadPreview,
42
- domEditSaveTimestampRef,
43
41
  compositionPath: activeCompPath,
44
42
  readProjectFile,
45
43
  publishSession: publishSdkSession,
@@ -56,7 +54,6 @@ export function useVariablesPersist({
56
54
  writeProjectFile,
57
55
  recordEdit,
58
56
  reloadPreview,
59
- domEditSaveTimestampRef,
60
57
  publishSdkSession,
61
58
  ],
62
59
  );
@@ -4,9 +4,23 @@ import { isLottieAnimationLoaded } from "@hyperframes/core/runtime/lottie-readin
4
4
  import { useMountEffect } from "../../hooks/useMountEffect";
5
5
  import { applyPreviewVariablesToUrl } from "../../hooks/previewVariablesStore";
6
6
  import { HyperframesLoader } from "../../components/ui";
7
- // NOTE: importing "@hyperframes/player" registers a class extending HTMLElement
8
- // at module load, which throws under SSR. Defer the import to the mount effect
9
- // so it only runs in the browser.
7
+ // Importing "@hyperframes/player" registers a class extending HTMLElement at
8
+ // module load, which throws under SSR, hence the dynamic import behind a
9
+ // `typeof window` guard. Kicking it here rather than in the mount effect puts
10
+ // the chunk request in flight before the shell's first layout. Clearing the memo
11
+ // on rejection stops one failure poisoning every later mount; the browser's
12
+ // module map still caches a failed fetch, so recovery is a page reload.
13
+ let playerModule: Promise<unknown> | null = null;
14
+
15
+ export function loadPlayerModule(): Promise<unknown> {
16
+ playerModule ??= import("@hyperframes/player").catch((err: unknown) => {
17
+ playerModule = null;
18
+ throw err;
19
+ });
20
+ return playerModule;
21
+ }
22
+
23
+ if (typeof window !== "undefined") void loadPlayerModule().catch(() => {});
10
24
 
11
25
  interface PlayerProps {
12
26
  projectId?: string;
@@ -166,8 +180,7 @@ export const Player = forwardRef<HTMLIFrameElement, PlayerProps>(
166
180
  let canceled = false;
167
181
  let cleanup: (() => void) | undefined;
168
182
 
169
- // Dynamic import registers the custom element in the browser only.
170
- import("@hyperframes/player").then(() => {
183
+ void loadPlayerModule().then(() => {
171
184
  if (canceled) return;
172
185
 
173
186
  // Create the web component imperatively to avoid JSX custom-element typing.
@@ -0,0 +1,41 @@
1
+ // @vitest-environment happy-dom
2
+
3
+ import { describe, expect, it, vi } from "vitest";
4
+
5
+ // The module-scope kick in Player.tsx consumes the first attempt at import time,
6
+ // so this starts failing; the first test asserts that kick happened.
7
+ const state = vi.hoisted(() => ({ attempts: 0, failing: true }));
8
+
9
+ vi.mock("@hyperframes/player", () => {
10
+ state.attempts += 1;
11
+ if (state.failing) throw new Error("chunk load failed");
12
+ return {};
13
+ });
14
+
15
+ import { loadPlayerModule } from "./Player";
16
+
17
+ describe("loadPlayerModule", () => {
18
+ it("kicks the import at module scope, before any mount", () => {
19
+ expect(state.attempts).toBe(1);
20
+ });
21
+
22
+ it("does not hand a later mount the promise a failed load already rejected", async () => {
23
+ // vitest rewraps a throwing mock factory, so assert the rejection, not its text.
24
+ await expect(loadPlayerModule()).rejects.toThrow();
25
+ const failedAttempts = state.attempts;
26
+
27
+ state.failing = false;
28
+
29
+ await expect(loadPlayerModule()).resolves.toBeDefined();
30
+ expect(state.attempts).toBeGreaterThan(failedAttempts);
31
+ });
32
+
33
+ it("hands every mount the same resolved promise", async () => {
34
+ state.failing = false;
35
+ const first = loadPlayerModule();
36
+ await first;
37
+
38
+ expect(loadPlayerModule()).toBe(first);
39
+ expect(loadPlayerModule()).toBe(first);
40
+ });
41
+ });
@@ -60,4 +60,11 @@ export type IframeWindow = Window & {
60
60
  __timeline?: TimelineLike;
61
61
  __timelines?: Record<string, TimelineLike>;
62
62
  __clipManifest?: ClipManifest;
63
+ /** Declared runtime-side in core's window.d.ts, which this package cannot see.
64
+ * Every member stays optional and is optional-called, so a runtime that
65
+ * predates the hook degrades to a no-op instead of throwing. */
66
+ __hf?: {
67
+ leasePausedMedia?: (el: HTMLMediaElement) => void;
68
+ releasePausedMedia?: (el: HTMLMediaElement) => void;
69
+ };
63
70
  };
@@ -123,6 +123,36 @@ describe("scrubPreviewAudio", () => {
123
123
  expect(voiceover.play).not.toHaveBeenCalled();
124
124
  stopScrubPreviewAudio();
125
125
  });
126
+
127
+ /** A scrub audition is media running under a paused clock, which the runtime now
128
+ * stops on sight. So it borrows the element. That a leased element survives the
129
+ * tick is asserted runtime-side in core's `transportPark.test.ts`; here the
130
+ * contract is that the hook is called with the right element and given back. */
131
+ it("borrows the element from the runtime for the audition and returns it on stop", () => {
132
+ const iframe = document.createElement("iframe");
133
+ document.body.append(iframe);
134
+ const previewDoc = iframe.contentDocument;
135
+ if (!previewDoc?.body) throw new Error("expected an iframe document");
136
+
137
+ const music = previewDoc.createElement("audio");
138
+ music.id = "music";
139
+ music.play = vi.fn(async () => {});
140
+ music.pause = vi.fn();
141
+ previewDoc.body.append(music);
142
+
143
+ const leasePausedMedia = vi.fn();
144
+ const releasePausedMedia = vi.fn();
145
+ (previewDoc.defaultView as IframeWindow).__hf = { leasePausedMedia, releasePausedMedia };
146
+
147
+ scrubPreviewAudio(iframe, 0.5, "music", 1);
148
+
149
+ expect(leasePausedMedia).toHaveBeenCalledWith(music);
150
+ expect(releasePausedMedia).not.toHaveBeenCalled();
151
+
152
+ stopScrubPreviewAudio();
153
+
154
+ expect(releasePausedMedia).toHaveBeenCalledWith(music);
155
+ });
126
156
  });
127
157
 
128
158
  describe("applyPreviewAudioFlags", () => {
@@ -245,11 +245,24 @@ function resolveScrubAudioEl(doc: Document, musicId?: string | null): HTMLAudioE
245
245
  );
246
246
  }
247
247
 
248
+ /** The runtime stops any media running under a paused clock, and a scrub audition
249
+ * IS media running under a paused clock, so it has to borrow the element. Every
250
+ * hop is optional: a runtime predating the hook must no-op, not throw. Wrapped in
251
+ * named calls so `applyScrub` does not carry the optional chains' branches. */
252
+ function leaseScrubElement(el: HTMLAudioElement): void {
253
+ (el.ownerDocument.defaultView as IframeWindow | null)?.__hf?.leasePausedMedia?.(el);
254
+ }
255
+
256
+ function releaseScrubElement(el: HTMLAudioElement): void {
257
+ (el.ownerDocument.defaultView as IframeWindow | null)?.__hf?.releasePausedMedia?.(el);
258
+ }
259
+
248
260
  function applyScrub(el: HTMLAudioElement, audioFileTime: number, previewVolume: number): void {
249
261
  if (scrubAudioEl && scrubAudioEl !== el) stopScrubPreviewAudio();
250
262
  if (scrubPrevMuted === null) scrubPrevMuted = el.muted;
251
263
  if (scrubPrevVolume === null) scrubPrevVolume = el.volume;
252
264
  scrubAudioEl = el;
265
+ leaseScrubElement(el);
253
266
  try {
254
267
  el.muted = false;
255
268
  el.volume = SCRUB_VOLUME * normalizePreviewVolume(previewVolume);
@@ -296,6 +309,9 @@ export function stopScrubPreviewAudio(): void {
296
309
  const el = scrubAudioEl;
297
310
  scrubAudioEl = null;
298
311
  if (!el) return;
312
+ // `scrubStopTimer` guarantees this runs within ~140 ms of the last scrub, so
313
+ // the borrow cannot outlive the audition.
314
+ releaseScrubElement(el);
299
315
  try {
300
316
  el.pause();
301
317
  if (scrubPrevMuted !== null) el.muted = scrubPrevMuted;
@@ -18,7 +18,6 @@ const deps = {
18
18
  editHistory: { recordEdit: vi.fn() },
19
19
  writeProjectFile: vi.fn(),
20
20
  reloadPreview: vi.fn(),
21
- domEditSaveTimestampRef: { current: 0 },
22
21
  } as never;
23
22
 
24
23
  describe("SDK family gate mapping", () => {
@@ -28,7 +28,6 @@ const makeDeps = () =>
28
28
  editHistory: { recordEdit: vi.fn().mockResolvedValue(undefined) },
29
29
  writeProjectFile: vi.fn().mockResolvedValue(undefined),
30
30
  reloadPreview: vi.fn(),
31
- domEditSaveTimestampRef: { current: 0 },
32
31
  }) as never;
33
32
 
34
33
  describe("dark-launch gate — STUDIO_SDK_CUTOVER_ENABLED=false ⇒ persist declines", () => {
@@ -14,7 +14,6 @@ import {
14
14
  import { openComposition } from "@hyperframes/sdk";
15
15
  import { createMemoryAdapter } from "@hyperframes/sdk/adapters/memory";
16
16
  import type { PatchOperation } from "./sourcePatcher";
17
- import type { MutableRefObject } from "react";
18
17
 
19
18
  vi.mock("../components/editor/manualEditingAvailability", () => ({
20
19
  STUDIO_SDK_CUTOVER_ENABLED: true,
@@ -153,13 +152,10 @@ describe("shouldUseSdkCutover", () => {
153
152
  });
154
153
 
155
154
  describe("sdkCutoverPersist", () => {
156
- const makeRef = <T>(val: T): MutableRefObject<T> => ({ current: val });
157
-
158
155
  const makeDeps = (overrides: Partial<Parameters<typeof sdkCutoverPersist>[5]> = {}) => ({
159
156
  editHistory: { recordEdit: vi.fn().mockResolvedValue(undefined) },
160
157
  writeProjectFile: vi.fn().mockResolvedValue(undefined),
161
158
  reloadPreview: vi.fn(),
162
- domEditSaveTimestampRef: makeRef(0),
163
159
  ...candidateTestDeps(),
164
160
  ...overrides,
165
161
  });
@@ -453,7 +449,6 @@ window.__timelines = { main: tl };</script></div>
453
449
  disk = content;
454
450
  }),
455
451
  reloadPreview: vi.fn(),
456
- domEditSaveTimestampRef: { current: 0 },
457
452
  publishSession,
458
453
  },
459
454
  mutate,
@@ -485,7 +480,6 @@ window.__timelines = { main: tl };</script></div>
485
480
  editHistory: { recordEdit: vi.fn().mockResolvedValue(undefined) },
486
481
  writeProjectFile,
487
482
  reloadPreview: vi.fn(),
488
- domEditSaveTimestampRef: { current: 0 },
489
483
  createCandidateSession: async (source) => {
490
484
  candidate = await openComposition(source, { history: false });
491
485
  disposeCandidate = vi.spyOn(candidate, "dispose");
@@ -525,7 +519,6 @@ window.__timelines = { main: tl };</script></div>
525
519
  order.push("write");
526
520
  }),
527
521
  reloadPreview: vi.fn(() => order.push("refresh")),
528
- domEditSaveTimestampRef: { current: 0 },
529
522
  publishSession: ({ candidate }) => {
530
523
  order.push("publish");
531
524
  published = candidate;
@@ -559,7 +552,6 @@ window.__timelines = { main: tl };</script></div>
559
552
  editHistory: { recordEdit },
560
553
  writeProjectFile,
561
554
  reloadPreview: vi.fn(),
562
- domEditSaveTimestampRef: { current: 0 },
563
555
  publishSession: ({ candidate }) => {
564
556
  published = candidate;
565
557
  throw new Error("cleanup after publish failed");
@@ -591,7 +583,6 @@ window.__timelines = { main: tl };</script></div>
591
583
  writeProjectFile,
592
584
  readProjectFile: vi.fn(async () => disk),
593
585
  reloadPreview: vi.fn(),
594
- domEditSaveTimestampRef: { current: 0 },
595
586
  publishSession: ({ candidate }) => {
596
587
  published.push(candidate);
597
588
  return "published";
@@ -632,7 +623,6 @@ window.__timelines = { main: tl };</script></div>
632
623
  throw new Error("transient read failure");
633
624
  }),
634
625
  reloadPreview: vi.fn(),
635
- domEditSaveTimestampRef: { current: 0 },
636
626
  publishSession: vi.fn().mockReturnValue("published"),
637
627
  };
638
628
 
@@ -683,7 +673,6 @@ window.__timelines = { main: tl };</script></div>
683
673
  readProjectFile: vi.fn().mockResolvedValue(html),
684
674
  reloadPreview: vi.fn(),
685
675
  refresh,
686
- domEditSaveTimestampRef: { current: 0 },
687
676
  createCandidateSession: vi.fn().mockResolvedValue(candidateA),
688
677
  publishSession: ({ candidate, expectedSession, targetPath }) => {
689
678
  if (activePath !== targetPath || currentSession !== expectedSession) {
@@ -724,7 +713,6 @@ describe("persistSdkSerialize — shared per-file transaction boundary", () => {
724
713
  }),
725
714
  readProjectFile: vi.fn(async () => disk.current),
726
715
  reloadPreview: vi.fn(),
727
- domEditSaveTimestampRef: { current: 0 },
728
716
  });
729
717
 
730
718
  it("rebases overlapping whole-file transforms on the latest committed bytes", async () => {
@@ -814,12 +802,10 @@ describe("persistSdkSerialize — shared per-file transaction boundary", () => {
814
802
  });
815
803
 
816
804
  describe("sdkDeletePersist", () => {
817
- const makeRef = <T>(val: T): MutableRefObject<T> => ({ current: val });
818
805
  const makeDeps = () => ({
819
806
  editHistory: { recordEdit: vi.fn().mockResolvedValue(undefined) },
820
807
  writeProjectFile: vi.fn().mockResolvedValue(undefined),
821
808
  reloadPreview: vi.fn(),
822
- domEditSaveTimestampRef: makeRef(0),
823
809
  ...candidateTestDeps(),
824
810
  });
825
811
 
@@ -893,12 +879,10 @@ describe("sdkDeletePersist", () => {
893
879
  });
894
880
 
895
881
  describe("sdkTimingPersist", () => {
896
- const makeRef = <T>(val: T): MutableRefObject<T> => ({ current: val });
897
882
  const makeDeps = () => ({
898
883
  editHistory: { recordEdit: vi.fn().mockResolvedValue(undefined) },
899
884
  writeProjectFile: vi.fn().mockResolvedValue(undefined),
900
885
  reloadPreview: vi.fn(),
901
- domEditSaveTimestampRef: makeRef(0),
902
886
  ...candidateTestDeps(),
903
887
  });
904
888
 
@@ -1006,7 +990,6 @@ describe("sdkTimingPersist", () => {
1006
990
  });
1007
991
 
1008
992
  describe("sdkGsapTweenPersist — undo baseline (finding #12)", () => {
1009
- const makeRef = <T>(val: T): MutableRefObject<T> => ({ current: val });
1010
993
  const makeSession = () =>
1011
994
  ({
1012
995
  getElement: vi.fn().mockReturnValue({ id: "hf-box" }),
@@ -1023,7 +1006,6 @@ describe("sdkGsapTweenPersist — undo baseline (finding #12)", () => {
1023
1006
  editHistory: { recordEdit: vi.fn().mockResolvedValue(undefined) },
1024
1007
  writeProjectFile: vi.fn().mockResolvedValue(undefined),
1025
1008
  reloadPreview: vi.fn(),
1026
- domEditSaveTimestampRef: makeRef(0),
1027
1009
  readProjectFile: vi.fn().mockResolvedValue("<html>on-disk gsap bytes</html>"),
1028
1010
  ...candidateTestDeps(),
1029
1011
  };
@@ -1046,8 +1028,6 @@ describe("sdkGsapTweenPersist — undo baseline (finding #12)", () => {
1046
1028
  });
1047
1029
 
1048
1030
  describe("sdkGsapTweenPersist — per-file serialization (finding #8)", () => {
1049
- const makeRef = <T>(val: T): MutableRefObject<T> => ({ current: val });
1050
-
1051
1031
  it("routes the read-modify-write through the shared file coordinator", async () => {
1052
1032
  const order: string[] = [];
1053
1033
  let writeResolve: (() => void) | null = null;
@@ -1071,7 +1051,6 @@ describe("sdkGsapTweenPersist — per-file serialization (finding #8)", () => {
1071
1051
  return Promise.resolve();
1072
1052
  }),
1073
1053
  reloadPreview: vi.fn(),
1074
- domEditSaveTimestampRef: makeRef(0),
1075
1054
  ...candidateTestDeps(),
1076
1055
  };
1077
1056
 
@@ -1116,12 +1095,10 @@ describe("sdkGsapTweenPersist — per-file serialization (finding #8)", () => {
1116
1095
  });
1117
1096
 
1118
1097
  describe("sdkGsapTweenPersist", () => {
1119
- const makeRef = <T>(val: T): MutableRefObject<T> => ({ current: val });
1120
1098
  const makeDeps = () => ({
1121
1099
  editHistory: { recordEdit: vi.fn().mockResolvedValue(undefined) },
1122
1100
  writeProjectFile: vi.fn().mockResolvedValue(undefined),
1123
1101
  reloadPreview: vi.fn(),
1124
- domEditSaveTimestampRef: makeRef(0),
1125
1102
  ...candidateTestDeps(),
1126
1103
  });
1127
1104
 
@@ -1232,12 +1209,10 @@ describe("sdkGsapTweenPersist", () => {
1232
1209
  });
1233
1210
 
1234
1211
  describe("sdkGsapKeyframePersist", () => {
1235
- const makeRef = <T>(val: T): MutableRefObject<T> => ({ current: val });
1236
1212
  const makeDeps = () => ({
1237
1213
  editHistory: { recordEdit: vi.fn().mockResolvedValue(undefined) },
1238
1214
  writeProjectFile: vi.fn().mockResolvedValue(undefined),
1239
1215
  reloadPreview: vi.fn(),
1240
- domEditSaveTimestampRef: makeRef(0),
1241
1216
  ...candidateTestDeps(),
1242
1217
  });
1243
1218
 
@@ -1303,12 +1278,10 @@ describe("sdkGsapKeyframePersist", () => {
1303
1278
  });
1304
1279
 
1305
1280
  describe("sdkCutoverPersist — GSAP script preservation (integration)", () => {
1306
- const makeRef = <T>(val: T): MutableRefObject<T> => ({ current: val });
1307
1281
  const makeDeps = () => ({
1308
1282
  editHistory: { recordEdit: vi.fn().mockResolvedValue(undefined) },
1309
1283
  writeProjectFile: vi.fn().mockResolvedValue(undefined),
1310
1284
  reloadPreview: vi.fn(),
1311
- domEditSaveTimestampRef: makeRef(0),
1312
1285
  ...candidateTestDeps(),
1313
1286
  });
1314
1287
 
@@ -1,4 +1,3 @@
1
- import type { MutableRefObject } from "react";
2
1
  import { openComposition, type Composition } from "@hyperframes/sdk";
3
2
  import type { EditHistoryKind } from "./editHistory";
4
3
  import { hashContent, markSelfWrite } from "../hooks/sdkSelfWriteRegistry";
@@ -39,7 +38,6 @@ export interface CutoverDeps {
39
38
  */
40
39
  writeProjectFile: (path: string, content: string, expectedContent?: string) => Promise<void>;
41
40
  reloadPreview: () => void;
42
- domEditSaveTimestampRef: MutableRefObject<number>;
43
41
  refresh?: (after: string) => void;
44
42
  compositionPath?: string | null;
45
43
  readProjectFile?: (path: string) => Promise<string>;
@@ -153,7 +151,6 @@ async function rollbackWrite(
153
151
  cause: Error,
154
152
  ): Promise<Error> {
155
153
  try {
156
- deps.domEditSaveTimestampRef.current = Date.now();
157
154
  markSelfWrite(targetPath, originalContent);
158
155
  await deps.writeProjectFile(targetPath, originalContent, expectedCurrentContent);
159
156
  return cause;
@@ -172,7 +169,6 @@ async function writeAndRecord(
172
169
  deps: CutoverDeps,
173
170
  options?: CutoverOptions,
174
171
  ): Promise<Error | null> {
175
- deps.domEditSaveTimestampRef.current = Date.now();
176
172
  markSelfWrite(targetPath, after);
177
173
  try {
178
174
  await deps.writeProjectFile(targetPath, after, originalContent);
@@ -59,7 +59,6 @@ describe("persistSlideshowManifest — op construction", () => {
59
59
  editHistory: { recordEdit: vi.fn().mockResolvedValue(undefined) },
60
60
  writeProjectFile,
61
61
  reloadPreview: vi.fn(),
62
- domEditSaveTimestampRef: { current: 0 },
63
62
  };
64
63
  }
65
64
 
@@ -16,7 +16,6 @@ export interface DomEditCommitBaseParams {
16
16
  activeCompPath: string | null;
17
17
  showToast: (message: string, tone?: "error" | "info") => void;
18
18
  writeProjectFile: ProjectFileWriter;
19
- domEditSaveTimestampRef: MutableRefObject<number>;
20
19
  editHistory: { recordEdit: (entry: RecordEditInput) => Promise<void> };
21
20
  projectIdRef: MutableRefObject<string | null>;
22
21
  reloadPreview: () => void;