@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.
- package/dist/assets/{hyperframes-player-lPOqAC3l.js → hyperframes-player-BIpJmC69.js} +1 -1
- package/dist/assets/index-BmPeyP0k.js +441 -0
- package/dist/assets/{index-ywAULfGT.js → index-Df3VgdgL.js} +1 -1
- package/dist/assets/{index-RPEcu_bT.js → index-z2_QKypm.js} +1 -1
- package/dist/index.html +1 -1
- package/dist/index.js +61 -195
- 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/dist/assets/index-BRr1JoHX.js +0 -441
- package/src/hooks/useSdkSession.test.ts +0 -32
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hyperframes/studio",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.36",
|
|
4
4
|
"description": "",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -48,11 +48,11 @@
|
|
|
48
48
|
"gsap": "^3.13.0",
|
|
49
49
|
"marked": "^14.1.4",
|
|
50
50
|
"mediabunny": "^1.45.3",
|
|
51
|
-
"@hyperframes/core": "0.8.
|
|
52
|
-
"@hyperframes/
|
|
53
|
-
"@hyperframes/
|
|
54
|
-
"@hyperframes/studio-server": "0.8.
|
|
55
|
-
"@hyperframes/sdk": "0.8.
|
|
51
|
+
"@hyperframes/core": "0.8.36",
|
|
52
|
+
"@hyperframes/player": "0.8.36",
|
|
53
|
+
"@hyperframes/parsers": "0.8.36",
|
|
54
|
+
"@hyperframes/studio-server": "0.8.36",
|
|
55
|
+
"@hyperframes/sdk": "0.8.36"
|
|
56
56
|
},
|
|
57
57
|
"devDependencies": {
|
|
58
58
|
"@types/react": "19",
|
|
@@ -69,7 +69,7 @@
|
|
|
69
69
|
"vite": "^6.4.2",
|
|
70
70
|
"vitest": "^4.1.11",
|
|
71
71
|
"zustand": "^5.0.0",
|
|
72
|
-
"@hyperframes/producer": "0.8.
|
|
72
|
+
"@hyperframes/producer": "0.8.36"
|
|
73
73
|
},
|
|
74
74
|
"peerDependencies": {
|
|
75
75
|
"react": "19",
|
package/src/App.tsx
CHANGED
|
@@ -104,7 +104,6 @@ export function StudioApp() {
|
|
|
104
104
|
rightPanelTab: initialUrlStateRef.current.rightPanelTab,
|
|
105
105
|
});
|
|
106
106
|
const editHistory = usePersistentEditHistory({ projectId });
|
|
107
|
-
const domEditSaveTimestampRef = useRef(0);
|
|
108
107
|
const handleDomZIndexReorderCommitRef = useRef<TimelineZIndexReorderCommit | null>(null);
|
|
109
108
|
const pendingTimelineEditPathRef = useRef(new Set<string>());
|
|
110
109
|
const isGestureRecordingRef = useRef(false);
|
|
@@ -113,7 +112,6 @@ export function StudioApp() {
|
|
|
113
112
|
projectId,
|
|
114
113
|
showToast,
|
|
115
114
|
recordEdit: editHistory.recordEdit,
|
|
116
|
-
domEditSaveTimestampRef,
|
|
117
115
|
setRefreshKey,
|
|
118
116
|
});
|
|
119
117
|
const masterCompPath = useMemo(
|
|
@@ -161,7 +159,6 @@ export function StudioApp() {
|
|
|
161
159
|
writeProjectFile: fileManager.writeProjectFile,
|
|
162
160
|
observeProjectFileVersion: fileManager.observeProjectFileVersion,
|
|
163
161
|
recordEdit: editHistory.recordEdit,
|
|
164
|
-
domEditSaveTimestampRef,
|
|
165
162
|
reloadPreview,
|
|
166
163
|
previewIframeRef,
|
|
167
164
|
pendingTimelineEditPathRef,
|
|
@@ -224,7 +221,6 @@ export function StudioApp() {
|
|
|
224
221
|
showToast,
|
|
225
222
|
writeProjectFile: fileManager.writeProjectFile,
|
|
226
223
|
recordEdit: editHistory.recordEdit,
|
|
227
|
-
domEditSaveTimestampRef,
|
|
228
224
|
reloadPreview,
|
|
229
225
|
handleTimelineElementDelete: timelineEditing.handleTimelineElementDelete,
|
|
230
226
|
handleDomEditElementDelete: domEditDeleteBridge,
|
|
@@ -240,7 +236,6 @@ export function StudioApp() {
|
|
|
240
236
|
readOptionalProjectFile: fileManager.readOptionalProjectFile,
|
|
241
237
|
readProjectFile: fileManager.readProjectFile,
|
|
242
238
|
writeProjectFile: fileManager.writeProjectFile,
|
|
243
|
-
domEditSaveTimestampRef,
|
|
244
239
|
showToast,
|
|
245
240
|
syncHistoryPreviewAfterApply: previewPersistence.syncHistoryPreviewAfterApply,
|
|
246
241
|
waitForPendingDomEditSaves: previewPersistence.waitForPendingDomEditSaves,
|
|
@@ -281,7 +276,6 @@ export function StudioApp() {
|
|
|
281
276
|
readProjectFile: fileManager.readProjectFile,
|
|
282
277
|
writeProjectFile: fileManager.writeProjectFile,
|
|
283
278
|
updateEditingFileContent: fileManager.updateEditingFileContent,
|
|
284
|
-
domEditSaveTimestampRef,
|
|
285
279
|
editHistory: { recordEdit: editHistory.recordEdit },
|
|
286
280
|
fileTree: fileManager.fileTree,
|
|
287
281
|
importedFontAssetsRef: fileManager.importedFontAssetsRef,
|
|
@@ -526,7 +520,6 @@ export function StudioApp() {
|
|
|
526
520
|
publishSdkSession={sdkHandle.publish}
|
|
527
521
|
forceReloadSdkSession={sdkHandle.forceReload}
|
|
528
522
|
reloadPreview={reloadPreview}
|
|
529
|
-
domEditSaveTimestampRef={domEditSaveTimestampRef}
|
|
530
523
|
recordEdit={editHistory.recordEdit}
|
|
531
524
|
onToggleElementHidden={timelineEditing.handleToggleElementHidden}
|
|
532
525
|
onAutoGroupCarveSources={timelineEditing.handleAutoGroupCarveSources}
|
|
@@ -42,7 +42,6 @@ export function StudioRightPanel({
|
|
|
42
42
|
publishSdkSession,
|
|
43
43
|
forceReloadSdkSession,
|
|
44
44
|
reloadPreview,
|
|
45
|
-
domEditSaveTimestampRef,
|
|
46
45
|
recordEdit,
|
|
47
46
|
onToggleElementHidden,
|
|
48
47
|
onAutoGroupCarveSources,
|
|
@@ -139,7 +138,6 @@ export function StudioRightPanel({
|
|
|
139
138
|
writeProjectFile,
|
|
140
139
|
recordEdit,
|
|
141
140
|
reloadPreview,
|
|
142
|
-
domEditSaveTimestampRef,
|
|
143
141
|
publishSdkSession,
|
|
144
142
|
});
|
|
145
143
|
|
|
@@ -152,7 +150,6 @@ export function StudioRightPanel({
|
|
|
152
150
|
writeProjectFile,
|
|
153
151
|
recordEdit,
|
|
154
152
|
reloadPreview,
|
|
155
|
-
domEditSaveTimestampRef,
|
|
156
153
|
publishSdkSession,
|
|
157
154
|
coalesceKey: activeCompPath ? `slideshow-notes:${activeCompPath}` : "slideshow-notes",
|
|
158
155
|
});
|
|
@@ -201,7 +198,6 @@ export function StudioRightPanel({
|
|
|
201
198
|
selectedSourceFile: domEditSelection?.sourceFile || activeCompPath || "index.html",
|
|
202
199
|
fileTree,
|
|
203
200
|
projectId,
|
|
204
|
-
domEditSaveTimestampRef,
|
|
205
201
|
waitForPendingDomEditSaves,
|
|
206
202
|
readProjectFile,
|
|
207
203
|
writeProjectFile,
|
|
@@ -217,7 +213,6 @@ export function StudioRightPanel({
|
|
|
217
213
|
}),
|
|
218
214
|
[
|
|
219
215
|
activeCompPath,
|
|
220
|
-
domEditSaveTimestampRef,
|
|
221
216
|
domEditSelection?.sourceFile,
|
|
222
217
|
fileTree,
|
|
223
218
|
projectId,
|
|
@@ -272,7 +267,6 @@ export function StudioRightPanel({
|
|
|
272
267
|
writeProjectFile={writeProjectFile}
|
|
273
268
|
recordEdit={recordEdit}
|
|
274
269
|
reloadPreview={reloadPreview}
|
|
275
|
-
domEditSaveTimestampRef={domEditSaveTimestampRef}
|
|
276
270
|
forceReloadSharedSdkSession={forceReloadSdkSession}
|
|
277
271
|
>
|
|
278
272
|
<PropertyPanel
|
|
@@ -436,7 +430,6 @@ export function StudioRightPanel({
|
|
|
436
430
|
sdkSession={sdkSession}
|
|
437
431
|
publishSdkSession={publishSdkSession}
|
|
438
432
|
reloadPreview={reloadPreview}
|
|
439
|
-
domEditSaveTimestampRef={domEditSaveTimestampRef}
|
|
440
433
|
recordEdit={recordEdit}
|
|
441
434
|
/>
|
|
442
435
|
) : layersPaneOpen && designPaneOpen && !STUDIO_FLAT_INSPECTOR_ENABLED ? (
|
|
@@ -6,7 +6,6 @@
|
|
|
6
6
|
* component's own diffs small and readable.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
-
import type { MutableRefObject } from "react";
|
|
10
9
|
import type { StudioEditPersistenceProps } from "./panels/VariablesPanel";
|
|
11
10
|
import type { BlockParam } from "@hyperframes/core/registry";
|
|
12
11
|
import type { Composition } from "@hyperframes/sdk";
|
|
@@ -44,7 +43,6 @@ export interface StudioRightPanelProps extends StudioEditPersistenceProps {
|
|
|
44
43
|
*/
|
|
45
44
|
forceReloadSdkSession?: () => void;
|
|
46
45
|
reloadPreview: () => void;
|
|
47
|
-
domEditSaveTimestampRef: MutableRefObject<number>;
|
|
48
46
|
recordEdit: (entry: {
|
|
49
47
|
label: string;
|
|
50
48
|
kind: EditHistoryKind;
|
|
@@ -30,7 +30,6 @@ function renderToolbar(onSnapChange = vi.fn()) {
|
|
|
30
30
|
function AppHotkeyHarness() {
|
|
31
31
|
const domEditSelectionRef = useRef<DomEditSelection | null>(null);
|
|
32
32
|
const clearDomSelectionRef = useRef<() => void>(() => undefined);
|
|
33
|
-
const domEditSaveTimestampRef = useRef(0);
|
|
34
33
|
const leftSidebarRef = useRef<LeftSidebarHandle | null>(null);
|
|
35
34
|
|
|
36
35
|
useAppHotkeys({
|
|
@@ -47,7 +46,6 @@ function AppHotkeyHarness() {
|
|
|
47
46
|
readOptionalProjectFile: vi.fn(async () => ""),
|
|
48
47
|
readProjectFile: vi.fn(async () => ""),
|
|
49
48
|
writeProjectFile: vi.fn(async () => undefined),
|
|
50
|
-
domEditSaveTimestampRef,
|
|
51
49
|
showToast: vi.fn(),
|
|
52
50
|
syncHistoryPreviewAfterApply: vi.fn(async () => undefined),
|
|
53
51
|
waitForPendingDomEditSaves: vi.fn(async () => undefined),
|
|
@@ -62,9 +62,9 @@ describe("studio manual edits", () => {
|
|
|
62
62
|
expect(readStudioFileChangePath({ path: ".hyperframes/studio-manual-edits.json" })).toBe(
|
|
63
63
|
".hyperframes/studio-manual-edits.json",
|
|
64
64
|
);
|
|
65
|
-
expect(readStudioFileChangePath({
|
|
66
|
-
|
|
67
|
-
);
|
|
65
|
+
expect(readStudioFileChangePath({ filePath: "/nested/file.html" })).toBe("nested/file.html");
|
|
66
|
+
// A still-wrapped delivery is a decoding bug upstream, never a path.
|
|
67
|
+
expect(readStudioFileChangePath({ data: { path: "nested/file.html" } })).toBeNull();
|
|
68
68
|
});
|
|
69
69
|
|
|
70
70
|
it("applies offsets through CSS translate longhand", () => {
|
|
@@ -9,7 +9,7 @@ export {
|
|
|
9
9
|
type StudioPathOffsetSnapshot,
|
|
10
10
|
} from "./manualEditsTypes";
|
|
11
11
|
|
|
12
|
-
export { readStudioFileChangePath } from "./manualEditsParsing";
|
|
12
|
+
export { readFileChangeField, readStudioFileChangePath } from "./manualEditsParsing";
|
|
13
13
|
|
|
14
14
|
export {
|
|
15
15
|
beginStudioManualEditGesture,
|
|
@@ -15,28 +15,19 @@ function normalizeStudioFileChangePath(path: string): string {
|
|
|
15
15
|
.replace(/^\.?\//, "");
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
}
|
|
29
|
-
return normalizeStudioFileChangePath(trimmed);
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
if (!value || typeof value !== "object") return null;
|
|
33
|
-
const record = value as Record<string, unknown>;
|
|
34
|
-
if (typeof record.path === "string") return normalizeStudioFileChangePath(record.path);
|
|
35
|
-
if (typeof record.filePath === "string") return normalizeStudioFileChangePath(record.filePath);
|
|
36
|
-
if ("data" in record) return readStudioFileChangePathFromValue(record.data);
|
|
37
|
-
return null;
|
|
18
|
+
/**
|
|
19
|
+
* Read one string field out of an ALREADY-DECODED file-change payload. Every
|
|
20
|
+
* reader of that payload goes through here, so no reader can disagree with its
|
|
21
|
+
* siblings about the shape. Decoding a raw delivery is the transport's job.
|
|
22
|
+
*/
|
|
23
|
+
export function readFileChangeField(payload: unknown, key: string): string | null {
|
|
24
|
+
if (!payload || typeof payload !== "object") return null;
|
|
25
|
+
const record = payload as Record<string, unknown>;
|
|
26
|
+
const value = record[key];
|
|
27
|
+
return typeof value === "string" ? value : null;
|
|
38
28
|
}
|
|
39
29
|
|
|
40
30
|
export function readStudioFileChangePath(payload: unknown): string | null {
|
|
41
|
-
|
|
31
|
+
const path = readFileChangeField(payload, "path") ?? readFileChangeField(payload, "filePath");
|
|
32
|
+
return path === null ? null : normalizeStudioFileChangePath(path);
|
|
42
33
|
}
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* follow-up (values are per-composition-scope), so no preview control is shown.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
-
import { useCallback, useState
|
|
9
|
+
import { useCallback, useState } from "react";
|
|
10
10
|
import type { Composition, CompositionVariable } from "@hyperframes/sdk";
|
|
11
11
|
import {
|
|
12
12
|
useEditVariablesInFile,
|
|
@@ -92,7 +92,6 @@ export function VariablesOtherCompositions({
|
|
|
92
92
|
writeProjectFile,
|
|
93
93
|
recordEdit,
|
|
94
94
|
reloadPreview,
|
|
95
|
-
domEditSaveTimestampRef,
|
|
96
95
|
}: {
|
|
97
96
|
fileTree: string[];
|
|
98
97
|
excludePath: string;
|
|
@@ -101,7 +100,6 @@ export function VariablesOtherCompositions({
|
|
|
101
100
|
writeProjectFile: (path: string, content: string) => Promise<void>;
|
|
102
101
|
recordEdit: RecordEditFn;
|
|
103
102
|
reloadPreview: () => void;
|
|
104
|
-
domEditSaveTimestampRef: MutableRefObject<number>;
|
|
105
103
|
}) {
|
|
106
104
|
const [selfRefresh, setSelfRefresh] = useState(0);
|
|
107
105
|
const groups = useProjectCompositionVariables(
|
|
@@ -115,7 +113,6 @@ export function VariablesOtherCompositions({
|
|
|
115
113
|
writeProjectFile,
|
|
116
114
|
recordEdit,
|
|
117
115
|
reloadPreview,
|
|
118
|
-
domEditSaveTimestampRef,
|
|
119
116
|
});
|
|
120
117
|
const [editingKey, setEditingKey] = useState<string | null>(null);
|
|
121
118
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { memo, useCallback, useEffect, useMemo, useState
|
|
1
|
+
import { memo, useCallback, useEffect, useMemo, useState } from "react";
|
|
2
2
|
import type {
|
|
3
3
|
Composition,
|
|
4
4
|
CompositionVariable,
|
|
@@ -35,7 +35,6 @@ export interface StudioEditPersistenceProps {
|
|
|
35
35
|
sdkSession: Composition | null;
|
|
36
36
|
publishSdkSession: PublishSdkSession;
|
|
37
37
|
reloadPreview: () => void;
|
|
38
|
-
domEditSaveTimestampRef: MutableRefObject<number>;
|
|
39
38
|
recordEdit: (entry: {
|
|
40
39
|
label: string;
|
|
41
40
|
kind: EditHistoryKind;
|
|
@@ -253,7 +252,6 @@ export const VariablesPanel = memo(function VariablesPanel({
|
|
|
253
252
|
sdkSession,
|
|
254
253
|
publishSdkSession,
|
|
255
254
|
reloadPreview,
|
|
256
|
-
domEditSaveTimestampRef,
|
|
257
255
|
recordEdit,
|
|
258
256
|
}: VariablesPanelProps) {
|
|
259
257
|
const { activeCompPath, showToast } = useStudioShellContext();
|
|
@@ -289,7 +287,6 @@ export const VariablesPanel = memo(function VariablesPanel({
|
|
|
289
287
|
writeProjectFile,
|
|
290
288
|
recordEdit,
|
|
291
289
|
reloadPreview,
|
|
292
|
-
domEditSaveTimestampRef,
|
|
293
290
|
publishSdkSession,
|
|
294
291
|
});
|
|
295
292
|
|
|
@@ -552,7 +549,6 @@ export const VariablesPanel = memo(function VariablesPanel({
|
|
|
552
549
|
writeProjectFile={writeProjectFile}
|
|
553
550
|
recordEdit={recordEdit}
|
|
554
551
|
reloadPreview={reloadPreview}
|
|
555
|
-
domEditSaveTimestampRef={domEditSaveTimestampRef}
|
|
556
552
|
/>
|
|
557
553
|
</div>
|
|
558
554
|
</div>
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import type { MutableRefObject } from "react";
|
|
2
1
|
import type { EditHistoryKind } from "../utils/editHistory";
|
|
3
2
|
import { saveProjectFilesWithHistory } from "../utils/studioFileHistory";
|
|
4
3
|
import { patchMediaColorGradingInHtml } from "./editor/colorGradingScopePatch";
|
|
@@ -27,7 +26,6 @@ interface ApplyColorGradingScopeOptions {
|
|
|
27
26
|
selectedSourceFile: string;
|
|
28
27
|
fileTree: string[];
|
|
29
28
|
projectId: string;
|
|
30
|
-
domEditSaveTimestampRef: MutableRefObject<number>;
|
|
31
29
|
waitForPendingDomEditSaves: () => Promise<void>;
|
|
32
30
|
readProjectFile: ProjectFileReader;
|
|
33
31
|
writeProjectFile: ProjectFileWriter;
|
|
@@ -78,7 +76,6 @@ export async function applyColorGradingScopeUpdate({
|
|
|
78
76
|
selectedSourceFile,
|
|
79
77
|
fileTree,
|
|
80
78
|
projectId,
|
|
81
|
-
domEditSaveTimestampRef,
|
|
82
79
|
waitForPendingDomEditSaves,
|
|
83
80
|
readProjectFile,
|
|
84
81
|
writeProjectFile,
|
|
@@ -105,7 +102,6 @@ export async function applyColorGradingScopeUpdate({
|
|
|
105
102
|
return EMPTY_COLOR_GRADING_SCOPE_RESULT;
|
|
106
103
|
}
|
|
107
104
|
|
|
108
|
-
domEditSaveTimestampRef.current = Date.now();
|
|
109
105
|
const changedPaths = await saveProjectFilesWithHistory({
|
|
110
106
|
projectId,
|
|
111
107
|
label: value ? "Apply color grading" : "Clear color grading",
|
|
@@ -15,7 +15,6 @@ export function makeLifecycleOpsParams(
|
|
|
15
15
|
activeCompPath: "index.html",
|
|
16
16
|
showToast: vi.fn(),
|
|
17
17
|
writeProjectFile: vi.fn(async () => {}),
|
|
18
|
-
domEditSaveTimestampRef: { current: 0 },
|
|
19
18
|
editHistory: { recordEdit: vi.fn(async () => {}) },
|
|
20
19
|
projectIdRef: { current: null },
|
|
21
20
|
reloadPreview: vi.fn(),
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { readFileSync } from "node:fs";
|
|
2
|
-
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { describe, expect, it, vi } from "vitest";
|
|
3
|
+
import { sseFileChangeChannel } from "./useExternalFileChangeCoordinator";
|
|
3
4
|
|
|
4
5
|
describe("external file-change subscription ownership", () => {
|
|
5
6
|
it("has one subscriber instead of independent Preview and SDK listeners", () => {
|
|
@@ -14,4 +15,42 @@ describe("external file-change subscription ownership", () => {
|
|
|
14
15
|
expect(sdk).not.toContain('hot.on("hf:file-change"');
|
|
15
16
|
expect(coordinator.match(/hot\.on\("hf:file-change"/g)).toHaveLength(1);
|
|
16
17
|
});
|
|
18
|
+
|
|
19
|
+
it("selects the SSE channel for production and hands it the shared handler", () => {
|
|
20
|
+
const coordinator = readFileSync(
|
|
21
|
+
new URL("./useExternalFileChangeCoordinator.ts", import.meta.url),
|
|
22
|
+
"utf8",
|
|
23
|
+
);
|
|
24
|
+
// vitest defines `import.meta.hot`, so the selection is the one claim left to
|
|
25
|
+
// the source; the channel's own behaviour is exercised below.
|
|
26
|
+
expect(coordinator.match(/sseFileChangeChannel\(handler\)/g)).toHaveLength(1);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it("delivers the raw file-change event from EventSource to the handler and closes on cleanup", () => {
|
|
30
|
+
const listeners = new Map<string, (event: unknown) => void>();
|
|
31
|
+
const close = vi.fn();
|
|
32
|
+
const opened: string[] = [];
|
|
33
|
+
class FakeEventSource {
|
|
34
|
+
constructor(url: string) {
|
|
35
|
+
opened.push(url);
|
|
36
|
+
}
|
|
37
|
+
addEventListener(type: string, listener: (event: unknown) => void) {
|
|
38
|
+
listeners.set(type, listener);
|
|
39
|
+
}
|
|
40
|
+
close = close;
|
|
41
|
+
}
|
|
42
|
+
vi.stubGlobal("EventSource", FakeEventSource);
|
|
43
|
+
try {
|
|
44
|
+
const onDelivery = vi.fn();
|
|
45
|
+
const stop = sseFileChangeChannel(onDelivery);
|
|
46
|
+
const event = new MessageEvent("file-change", { data: JSON.stringify({ path: "a.html" }) });
|
|
47
|
+
listeners.get("file-change")?.(event);
|
|
48
|
+
expect(opened).toEqual(["/api/events"]);
|
|
49
|
+
expect(onDelivery).toHaveBeenCalledWith(event);
|
|
50
|
+
stop();
|
|
51
|
+
expect(close).toHaveBeenCalledTimes(1);
|
|
52
|
+
} finally {
|
|
53
|
+
vi.unstubAllGlobals();
|
|
54
|
+
}
|
|
55
|
+
});
|
|
17
56
|
});
|
|
@@ -102,7 +102,6 @@ export interface GsapScriptCommitsParams {
|
|
|
102
102
|
files: Record<string, { before: string; after: string }>;
|
|
103
103
|
}) => Promise<void>;
|
|
104
104
|
};
|
|
105
|
-
domEditSaveTimestampRef: React.MutableRefObject<number>;
|
|
106
105
|
reloadPreview: () => void;
|
|
107
106
|
onCacheInvalidate: () => void;
|
|
108
107
|
onFileContentChanged?: (path: string, content: string) => void;
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* external write (notably undo/redo) in the file-change reload-suppression path.
|
|
4
4
|
*
|
|
5
5
|
* The old suppression was purely time-based: any file-change within 2 s of the
|
|
6
|
-
* shared
|
|
6
|
+
* shared save timestamp was swallowed. But BOTH an SDK cutover
|
|
7
7
|
* self-write AND an undo write set that same timestamp, so the window could not
|
|
8
8
|
* tell "the echo of the bytes I just wrote" (suppress) from "the reverted bytes
|
|
9
9
|
* an undo just wrote" (must reload). An undo that landed inside the window was
|
|
@@ -143,7 +143,6 @@ interface CreateAudioGroupAndAssignMembersInput {
|
|
|
143
143
|
previewIframe: HTMLIFrameElement | null;
|
|
144
144
|
writeProjectFile: (path: string, content: string) => Promise<void>;
|
|
145
145
|
recordEdit: (input: RecordEditInput) => Promise<void>;
|
|
146
|
-
domEditSaveTimestampRef: MutableRef<number>;
|
|
147
146
|
pendingTimelineEditPathRef: MutableRef<Set<string>>;
|
|
148
147
|
}
|
|
149
148
|
|
|
@@ -164,7 +163,6 @@ export async function createAudioGroupAndAssignMembers({
|
|
|
164
163
|
previewIframe,
|
|
165
164
|
writeProjectFile,
|
|
166
165
|
recordEdit,
|
|
167
|
-
domEditSaveTimestampRef,
|
|
168
166
|
pendingTimelineEditPathRef,
|
|
169
167
|
}: CreateAudioGroupAndAssignMembersInput): Promise<string[]> {
|
|
170
168
|
// Throws rather than returning empty: the carve's auto-group awaits this and
|
|
@@ -222,7 +220,6 @@ export async function createAudioGroupAndAssignMembers({
|
|
|
222
220
|
pendingTimelineEditPathRef.current.add(groupPath);
|
|
223
221
|
}
|
|
224
222
|
|
|
225
|
-
domEditSaveTimestampRef.current = Date.now();
|
|
226
223
|
const changedPaths = await saveProjectFilesWithHistory({
|
|
227
224
|
projectId,
|
|
228
225
|
label: groupLabel
|
|
@@ -238,7 +235,6 @@ export async function createAudioGroupAndAssignMembers({
|
|
|
238
235
|
writeFile: writeProjectFile,
|
|
239
236
|
recordEdit,
|
|
240
237
|
});
|
|
241
|
-
domEditSaveTimestampRef.current = Date.now();
|
|
242
238
|
for (const element of elements) {
|
|
243
239
|
usePlayerStore.getState().updateElement(element.key ?? element.id, { audioGroup: groupId });
|
|
244
240
|
}
|
|
@@ -268,7 +264,6 @@ export function useAudioGroupCarveAssignment({
|
|
|
268
264
|
showToast,
|
|
269
265
|
writeProjectFile,
|
|
270
266
|
recordEdit,
|
|
271
|
-
domEditSaveTimestampRef,
|
|
272
267
|
previewIframeRef,
|
|
273
268
|
pendingTimelineEditPathRef,
|
|
274
269
|
isRecordingRef,
|
|
@@ -314,7 +309,6 @@ export function useAudioGroupCarveAssignment({
|
|
|
314
309
|
previewIframe: previewIframeRef.current,
|
|
315
310
|
writeProjectFile,
|
|
316
311
|
recordEdit,
|
|
317
|
-
domEditSaveTimestampRef,
|
|
318
312
|
pendingTimelineEditPathRef,
|
|
319
313
|
});
|
|
320
314
|
} catch (error) {
|
|
@@ -335,7 +329,6 @@ export function useAudioGroupCarveAssignment({
|
|
|
335
329
|
previewIframeRef,
|
|
336
330
|
writeProjectFile,
|
|
337
331
|
recordEdit,
|
|
338
|
-
domEditSaveTimestampRef,
|
|
339
332
|
pendingTimelineEditPathRef,
|
|
340
333
|
isRecordingRef,
|
|
341
334
|
showToast,
|
|
@@ -45,7 +45,6 @@ function makeSetter() {
|
|
|
45
45
|
showToast: () => {},
|
|
46
46
|
writeProjectFile: async () => {},
|
|
47
47
|
recordEdit: async () => {},
|
|
48
|
-
domEditSaveTimestampRef: { current: 0 },
|
|
49
48
|
pendingTimelineEditPathRef: { current: new Set<string>() },
|
|
50
49
|
previewIframeRef: { current: null },
|
|
51
50
|
};
|
|
@@ -143,7 +143,6 @@ interface SetAudioGroupAttributeInput {
|
|
|
143
143
|
previewIframe: HTMLIFrameElement | null;
|
|
144
144
|
writeProjectFile: (path: string, content: string) => Promise<void>;
|
|
145
145
|
recordEdit: (input: RecordEditInput) => Promise<void>;
|
|
146
|
-
domEditSaveTimestampRef: MutableRef<number>;
|
|
147
146
|
pendingTimelineEditPathRef: MutableRef<Set<string>>;
|
|
148
147
|
}
|
|
149
148
|
|
|
@@ -163,7 +162,6 @@ async function setAudioGroupAttribute({
|
|
|
163
162
|
previewIframe,
|
|
164
163
|
writeProjectFile,
|
|
165
164
|
recordEdit,
|
|
166
|
-
domEditSaveTimestampRef,
|
|
167
165
|
pendingTimelineEditPathRef,
|
|
168
166
|
}: SetAudioGroupAttributeInput): Promise<string[]> {
|
|
169
167
|
// The file that actually CONTAINS the group element, not just the active
|
|
@@ -188,7 +186,6 @@ async function setAudioGroupAttribute({
|
|
|
188
186
|
label,
|
|
189
187
|
writeProjectFile,
|
|
190
188
|
recordEdit,
|
|
191
|
-
domEditSaveTimestampRef,
|
|
192
189
|
pendingTimelineEditPathRef,
|
|
193
190
|
patchLive: (v) => patchLiveGroupAttribute(previewIframe, groupId, attr, v),
|
|
194
191
|
});
|
|
@@ -207,7 +204,6 @@ export function useSetAudioGroupAttribute({
|
|
|
207
204
|
showToast,
|
|
208
205
|
writeProjectFile,
|
|
209
206
|
recordEdit,
|
|
210
|
-
domEditSaveTimestampRef,
|
|
211
207
|
previewIframeRef,
|
|
212
208
|
pendingTimelineEditPathRef,
|
|
213
209
|
isRecordingRef,
|
|
@@ -244,7 +240,6 @@ export function useSetAudioGroupAttribute({
|
|
|
244
240
|
previewIframe: previewIframeRef.current,
|
|
245
241
|
writeProjectFile,
|
|
246
242
|
recordEdit,
|
|
247
|
-
domEditSaveTimestampRef,
|
|
248
243
|
pendingTimelineEditPathRef,
|
|
249
244
|
});
|
|
250
245
|
syncStoredGroupAttribute(groupId, attr, value);
|
|
@@ -269,7 +264,6 @@ export function useSetAudioGroupAttribute({
|
|
|
269
264
|
previewIframeRef,
|
|
270
265
|
writeProjectFile,
|
|
271
266
|
recordEdit,
|
|
272
|
-
domEditSaveTimestampRef,
|
|
273
267
|
pendingTimelineEditPathRef,
|
|
274
268
|
isRecordingRef,
|
|
275
269
|
showToast,
|
|
@@ -243,7 +243,6 @@ describe("persistTimelineBatchEdit", () => {
|
|
|
243
243
|
writes.push([path, content]);
|
|
244
244
|
},
|
|
245
245
|
recordEdit: async () => {},
|
|
246
|
-
domEditSaveTimestampRef: { current: 0 },
|
|
247
246
|
pendingTimelineEditPathRef: { current: new Set<string>() },
|
|
248
247
|
};
|
|
249
248
|
}
|
|
@@ -452,7 +451,6 @@ describe("persistElementAttribute", () => {
|
|
|
452
451
|
label: "Set volume",
|
|
453
452
|
writeProjectFile,
|
|
454
453
|
recordEdit: vi.fn(),
|
|
455
|
-
domEditSaveTimestampRef: { current: 0 },
|
|
456
454
|
pendingTimelineEditPathRef: { current: new Set() },
|
|
457
455
|
patchLive,
|
|
458
456
|
}),
|
|
@@ -496,7 +494,6 @@ describe("persistElementAttribute — unwind value", () => {
|
|
|
496
494
|
label: "Set volume",
|
|
497
495
|
writeProjectFile,
|
|
498
496
|
recordEdit: vi.fn(),
|
|
499
|
-
domEditSaveTimestampRef: { current: 0 },
|
|
500
497
|
pendingTimelineEditPathRef: { current: new Set() },
|
|
501
498
|
// The live DOM is ALREADY at the new value when the commit runs — that
|
|
502
499
|
// is what `setLive` does on every drag frame.
|
|
@@ -285,7 +285,6 @@ export interface PersistTimelineEditInput {
|
|
|
285
285
|
buildPatches: (original: string, target: PatchTarget) => string;
|
|
286
286
|
writeProjectFile: (path: string, content: string, expectedContent?: string) => Promise<void>;
|
|
287
287
|
recordEdit: (input: RecordEditInput) => Promise<void>;
|
|
288
|
-
domEditSaveTimestampRef: React.MutableRefObject<number>;
|
|
289
288
|
pendingTimelineEditPathRef: React.MutableRefObject<Set<string>>;
|
|
290
289
|
coalesceKey?: string;
|
|
291
290
|
}
|
|
@@ -305,7 +304,6 @@ export async function persistTimelineEdit(input: PersistTimelineEditInput): Prom
|
|
|
305
304
|
}
|
|
306
305
|
|
|
307
306
|
input.pendingTimelineEditPathRef.current.add(targetPath);
|
|
308
|
-
input.domEditSaveTimestampRef.current = Date.now();
|
|
309
307
|
await saveProjectFilesWithHistory({
|
|
310
308
|
projectId: input.projectId,
|
|
311
309
|
label: input.label,
|
|
@@ -316,7 +314,6 @@ export async function persistTimelineEdit(input: PersistTimelineEditInput): Prom
|
|
|
316
314
|
writeFile: input.writeProjectFile,
|
|
317
315
|
recordEdit: input.recordEdit,
|
|
318
316
|
});
|
|
319
|
-
input.domEditSaveTimestampRef.current = Date.now();
|
|
320
317
|
}
|
|
321
318
|
|
|
322
319
|
export interface PersistTimelineBatchChange {
|
|
@@ -331,7 +328,6 @@ export interface PersistTimelineBatchEditInput {
|
|
|
331
328
|
changes: PersistTimelineBatchChange[];
|
|
332
329
|
writeProjectFile: (path: string, content: string, expectedContent?: string) => Promise<void>;
|
|
333
330
|
recordEdit: (input: RecordEditInput) => Promise<void>;
|
|
334
|
-
domEditSaveTimestampRef: React.MutableRefObject<number>;
|
|
335
331
|
pendingTimelineEditPathRef: React.MutableRefObject<Set<string>>;
|
|
336
332
|
coalesceKey?: string;
|
|
337
333
|
/** Per-entry undo coalesce window override (ms) — see EditHistoryEntry.coalesceMs. */
|
|
@@ -378,7 +374,6 @@ export async function persistTimelineBatchEdit(
|
|
|
378
374
|
for (const targetPath of Object.keys(files)) {
|
|
379
375
|
input.pendingTimelineEditPathRef.current.add(targetPath);
|
|
380
376
|
}
|
|
381
|
-
input.domEditSaveTimestampRef.current = Date.now();
|
|
382
377
|
await saveProjectFilesWithHistory({
|
|
383
378
|
projectId: input.projectId,
|
|
384
379
|
label: input.label,
|
|
@@ -390,7 +385,6 @@ export async function persistTimelineBatchEdit(
|
|
|
390
385
|
writeFile: input.writeProjectFile,
|
|
391
386
|
recordEdit: input.recordEdit,
|
|
392
387
|
});
|
|
393
|
-
input.domEditSaveTimestampRef.current = Date.now();
|
|
394
388
|
}
|
|
395
389
|
|
|
396
390
|
export { applyPatchByTarget, formatTimelineAttributeNumber };
|
|
@@ -406,7 +400,6 @@ export interface PersistElementAttributeInput {
|
|
|
406
400
|
label: string;
|
|
407
401
|
writeProjectFile: (path: string, content: string) => Promise<void>;
|
|
408
402
|
recordEdit: (input: RecordEditInput) => Promise<void>;
|
|
409
|
-
domEditSaveTimestampRef: { current: number };
|
|
410
403
|
pendingTimelineEditPathRef: { current: Set<string> };
|
|
411
404
|
/** Write the attribute directly on the live preview DOM node. */
|
|
412
405
|
patchLive: (value: string | null) => void;
|
|
@@ -429,7 +422,6 @@ export async function persistElementAttribute({
|
|
|
429
422
|
label,
|
|
430
423
|
writeProjectFile,
|
|
431
424
|
recordEdit,
|
|
432
|
-
domEditSaveTimestampRef,
|
|
433
425
|
pendingTimelineEditPathRef,
|
|
434
426
|
patchLive,
|
|
435
427
|
}: PersistElementAttributeInput): Promise<string[]> {
|
|
@@ -459,7 +451,6 @@ export async function persistElementAttribute({
|
|
|
459
451
|
const patched = applyPatchByTarget(before, patchTarget, operation);
|
|
460
452
|
|
|
461
453
|
pendingTimelineEditPathRef.current.add(targetPath);
|
|
462
|
-
domEditSaveTimestampRef.current = Date.now();
|
|
463
454
|
try {
|
|
464
455
|
const changedPaths = await saveProjectFilesWithHistory({
|
|
465
456
|
projectId,
|
|
@@ -470,7 +461,6 @@ export async function persistElementAttribute({
|
|
|
470
461
|
writeFile: writeProjectFile,
|
|
471
462
|
recordEdit,
|
|
472
463
|
});
|
|
473
|
-
domEditSaveTimestampRef.current = Date.now();
|
|
474
464
|
return changedPaths;
|
|
475
465
|
} catch (error) {
|
|
476
466
|
// The optimistic live write already ran; unwind it on a save failure so
|