@hyperframes/studio 0.8.34 → 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.
- package/dist/assets/{hyperframes-player-D4aSRr_M.js → hyperframes-player-BIpJmC69.js} +1 -1
- package/dist/assets/index-BmPeyP0k.js +441 -0
- package/dist/assets/{index-aNyQBoJv.js → index-Df3VgdgL.js} +1 -1
- package/dist/assets/{index-hch4ktcC.js → index-z2_QKypm.js} +1 -1
- package/dist/index.html +1 -1
- package/dist/index.js +76 -209
- package/dist/index.js.map +1 -1
- package/package.json +7 -7
- package/src/App.tsx +0 -7
- package/src/components/StudioRightPanel.tsx +0 -7
- package/src/components/StudioRightPanel.types.ts +0 -2
- package/src/components/editor/SnapToolbar.test.tsx +0 -2
- package/src/components/editor/manualEdits.test.ts +3 -3
- package/src/components/editor/manualEdits.ts +1 -1
- package/src/components/editor/manualEditsParsing.ts +12 -21
- package/src/components/panels/VariablesOtherCompositions.tsx +1 -4
- package/src/components/panels/VariablesPanel.tsx +1 -5
- package/src/components/studioColorGradingScope.ts +0 -4
- package/src/hooks/elementLifecycleOpsTestUtils.ts +0 -1
- package/src/hooks/externalFileChangeOwnership.test.ts +40 -1
- package/src/hooks/gsapScriptCommitTypes.ts +0 -1
- package/src/hooks/sdkSelfWriteRegistry.ts +1 -1
- package/src/hooks/timelineAudioGroupCreate.ts +0 -7
- package/src/hooks/timelineAudioGroupVolume.test.ts +0 -1
- package/src/hooks/timelineAudioGroupVolume.ts +0 -6
- package/src/hooks/timelineEditingHelpers.test.ts +0 -3
- package/src/hooks/timelineEditingHelpers.ts +0 -10
- package/src/hooks/timelineElementFxAttribute.ts +0 -6
- package/src/hooks/timelineTrackVisibility.test.ts +0 -17
- package/src/hooks/timelineTrackVisibility.ts +0 -15
- package/src/hooks/useAppHotkeys.previewForwarding.test.tsx +0 -1
- package/src/hooks/useAppHotkeys.textEditing.test.tsx +0 -2
- package/src/hooks/useAppHotkeys.ts +1 -7
- package/src/hooks/useAudioGroupCarveAssignment.test.tsx +0 -1
- package/src/hooks/useClipboard.ts +1 -11
- package/src/hooks/useDomEditCommits.test.tsx +0 -2
- package/src/hooks/useDomEditCommits.ts +2 -21
- package/src/hooks/useDomEditSession.test.tsx +0 -1
- package/src/hooks/useDomEditSession.ts +0 -7
- package/src/hooks/useEditorSave.test.tsx +0 -1
- package/src/hooks/useEditorSave.ts +2 -6
- package/src/hooks/useElementLifecycleOps.ts +0 -3
- package/src/hooks/useExternalFileChangeCoordinator.test.tsx +54 -0
- package/src/hooks/useExternalFileChangeCoordinator.ts +38 -22
- package/src/hooks/useFileManager.projectOwnership.test.tsx +0 -1
- package/src/hooks/useFileManager.ts +0 -3
- package/src/hooks/useGroupCommits.ts +0 -2
- package/src/hooks/useGsapScriptCommits.test.tsx +0 -2
- package/src/hooks/useGsapScriptCommits.ts +2 -5
- package/src/hooks/usePreviewInteraction.test.ts +14 -5
- package/src/hooks/usePreviewInteraction.ts +3 -3
- package/src/hooks/useProjectCompositionVariables.ts +3 -6
- package/src/hooks/useRazorSplit.history.test.tsx +0 -2
- package/src/hooks/useRazorSplit.test.ts +0 -2
- package/src/hooks/useRazorSplit.test.tsx +0 -1
- package/src/hooks/useRazorSplit.ts +0 -6
- package/src/hooks/useSdkSession.ts +3 -11
- package/src/hooks/useSlideshowPersist.ts +1 -5
- package/src/hooks/useTimelineAssetDropOps.ts +0 -6
- package/src/hooks/useTimelineDeleteOps.ts +0 -4
- package/src/hooks/useTimelineEditing.test.tsx +0 -3
- package/src/hooks/useTimelineEditing.ts +0 -16
- package/src/hooks/useTimelineEditingTypes.ts +0 -1
- package/src/hooks/useTimelineGroupEditing.ts +0 -6
- package/src/hooks/useVariablesPersist.ts +0 -3
- package/src/player/components/Player.tsx +18 -5
- package/src/player/components/playerModuleRetry.test.ts +41 -0
- package/src/player/lib/playbackTypes.ts +7 -0
- package/src/player/lib/timelineIframeHelpers.test.ts +30 -0
- package/src/player/lib/timelineIframeHelpers.ts +16 -0
- package/src/utils/sdkCutover.familyGate.test.ts +0 -1
- package/src/utils/sdkCutover.gate.test.ts +0 -1
- package/src/utils/sdkCutover.test.ts +0 -27
- package/src/utils/sdkEditTransaction.ts +0 -4
- package/src/utils/setSlideshowManifest.test.ts +0 -1
- package/src/utils/studioFileHistory.ts +0 -1
- package/src/webmcp/types.ts +5 -1
- package/src/webmcp/useStudioAgentTools.polyfill.test.tsx +219 -0
- package/src/webmcp/useStudioAgentTools.test.tsx +81 -67
- package/src/webmcp/useStudioAgentTools.ts +49 -28
- package/src/webmcp/webmcpTestUtils.ts +56 -0
- package/dist/assets/index-CkxPQxLJ.js +0 -441
- 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
|
-
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
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
|
-
|
|
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;
|
|
@@ -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);
|
|
@@ -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;
|
package/src/webmcp/types.ts
CHANGED
|
@@ -57,8 +57,12 @@ export interface ModelContextTool {
|
|
|
57
57
|
* A rejection is NOT a usable error channel: the spec discards the reason and
|
|
58
58
|
* rejects the caller with a bare UnknownError. Resolve with a tagged failure
|
|
59
59
|
* instead. See `toolResult.ts`.
|
|
60
|
+
*
|
|
61
|
+
* `options` is optional here although the spec always passes it: the
|
|
62
|
+
* `@mcp-b/global` polyfill (through 5.1.0) calls `execute(input)` with no
|
|
63
|
+
* second argument, so a Studio handler must not depend on receiving one.
|
|
60
64
|
*/
|
|
61
|
-
execute: (input: object, options
|
|
65
|
+
execute: (input: object, options?: ToolExecuteCallbackOptions) => Promise<unknown>;
|
|
62
66
|
annotations?: ModelContextToolAnnotations;
|
|
63
67
|
}
|
|
64
68
|
|