@hyperframes/studio 0.7.58 → 0.7.60
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-CtTDO63S.js → hyperframes-player-3XTTaVNf.js} +1 -1
- package/dist/assets/{index-C47jAC3Q.js → index-D6etaey-.js} +1 -1
- package/dist/assets/index-DXbu6IPT.css +1 -0
- package/dist/assets/{index-DeQPzqwH.js → index-Dh_WhagG.js} +1 -1
- package/dist/assets/index-cH6NfVV_.js +426 -0
- package/dist/index.d.ts +12 -3
- package/dist/index.html +2 -2
- package/dist/index.js +13755 -8627
- package/dist/index.js.map +1 -1
- package/dist/styles/tailwind-preset.d.ts +5 -0
- package/dist/styles/tailwind-preset.js +8 -1
- package/dist/styles/tailwind-preset.js.map +1 -1
- package/package.json +7 -7
- package/src/App.tsx +4 -4
- package/src/components/DesignPanelPromoteProvider.tsx +15 -2
- package/src/components/StudioRightPanel.tsx +42 -53
- package/src/components/editor/AnimationCard.test.tsx +134 -0
- package/src/components/editor/AnimationCard.tsx +19 -4
- package/src/components/editor/ArcPathControls.tsx +1 -0
- package/src/components/editor/GestureRecordControl.tsx +7 -1
- package/src/components/editor/GsapAnimationSection.tsx +109 -14
- package/src/components/editor/InspectorHeaderActions.tsx +42 -4
- package/src/components/editor/PropertyPanel.test.tsx +948 -0
- package/src/components/editor/PropertyPanel.tsx +144 -148
- package/src/components/editor/PropertyPanelEmptyState.test.tsx +74 -0
- package/src/components/editor/PropertyPanelEmptyState.tsx +179 -1
- package/src/components/editor/PropertyPanelFlat.tsx +580 -0
- package/src/components/editor/PropertyPanelFlatFooter.test.tsx +83 -0
- package/src/components/editor/PropertyPanelFlatFooter.tsx +73 -0
- package/src/components/editor/PropertyPanelFlatHeader.test.tsx +79 -0
- package/src/components/editor/PropertyPanelFlatHeader.tsx +112 -0
- package/src/components/editor/gsapAnimationCallbacks.ts +27 -0
- package/src/components/editor/gsapLivePreview.ts +34 -0
- package/src/components/editor/manualEditingAvailability.test.ts +10 -0
- package/src/components/editor/manualEditingAvailability.ts +9 -0
- package/src/components/editor/propertyPanel3dTransform.tsx +5 -0
- package/src/components/editor/propertyPanelColor.test.tsx +28 -0
- package/src/components/editor/propertyPanelColor.tsx +31 -1
- package/src/components/editor/propertyPanelColorGradingControls.tsx +11 -2
- package/src/components/editor/propertyPanelColorGradingSection.tsx +45 -365
- package/src/components/editor/propertyPanelColorGradingSlider.tsx +19 -5
- package/src/components/editor/propertyPanelFill.tsx +22 -5
- package/src/components/editor/propertyPanelFlatColorGradingSection.test.tsx +622 -0
- package/src/components/editor/propertyPanelFlatColorGradingSection.tsx +570 -0
- package/src/components/editor/propertyPanelFlatLayoutSection.test.tsx +293 -0
- package/src/components/editor/propertyPanelFlatLayoutSection.tsx +383 -0
- package/src/components/editor/propertyPanelFlatMaskInsetRows.tsx +102 -0
- package/src/components/editor/propertyPanelFlatMediaSection.test.tsx +436 -0
- package/src/components/editor/propertyPanelFlatMediaSection.tsx +286 -0
- package/src/components/editor/propertyPanelFlatMotionSection.test.tsx +272 -0
- package/src/components/editor/propertyPanelFlatMotionSection.tsx +315 -0
- package/src/components/editor/propertyPanelFlatPrimitives.test.tsx +1174 -0
- package/src/components/editor/propertyPanelFlatPrimitives.tsx +563 -0
- package/src/components/editor/propertyPanelFlatSelectRow.tsx +107 -0
- package/src/components/editor/propertyPanelFlatStyleHelpers.test.ts +23 -0
- package/src/components/editor/propertyPanelFlatStyleHelpers.ts +27 -0
- package/src/components/editor/propertyPanelFlatStyleSections.test.tsx +598 -0
- package/src/components/editor/propertyPanelFlatStyleSections.tsx +516 -0
- package/src/components/editor/propertyPanelFlatTextSection.test.tsx +465 -0
- package/src/components/editor/propertyPanelFlatTextSection.tsx +407 -0
- package/src/components/editor/propertyPanelFlatTimingDerivation.test.ts +71 -0
- package/src/components/editor/propertyPanelFlatTimingDerivation.ts +59 -0
- package/src/components/editor/propertyPanelFlatToggle.test.tsx +63 -0
- package/src/components/editor/propertyPanelFlatToggle.tsx +53 -0
- package/src/components/editor/propertyPanelFont.test.tsx +30 -0
- package/src/components/editor/propertyPanelFont.tsx +136 -95
- package/src/components/editor/propertyPanelHelpers.ts +73 -0
- package/src/components/editor/propertyPanelInputCoverage.test.tsx +587 -0
- package/src/components/editor/propertyPanelMediaSection.tsx +10 -0
- package/src/components/editor/propertyPanelPrimitives.tsx +60 -29
- package/src/components/editor/propertyPanelSections.test.tsx +161 -0
- package/src/components/editor/propertyPanelSections.tsx +154 -90
- package/src/components/editor/propertyPanelStyleSections.tsx +12 -1
- package/src/components/editor/propertyPanelTimingSection.tsx +1 -1
- package/src/components/editor/propertyPanelTypes.ts +14 -1
- package/src/components/editor/propertyPanelValueTier.test.ts +32 -0
- package/src/components/editor/propertyPanelValueTier.ts +28 -0
- package/src/components/editor/useColorGradingController.test.ts +418 -0
- package/src/components/editor/useColorGradingController.ts +596 -0
- package/src/components/panels/VariablesPanel.tsx +4 -0
- package/src/components/sidebar/CompositionsTab.test.ts +28 -2
- package/src/components/sidebar/CompositionsTab.tsx +3 -1
- package/src/components/storyboard/FramePoster.tsx +24 -4
- package/src/components/storyboard/StoryboardFrameFocus.tsx +4 -0
- package/src/components/storyboard/StoryboardFrameTile.tsx +30 -1
- package/src/components/storyboard/StoryboardGrid.tsx +23 -1
- package/src/components/storyboard/StoryboardLoaded.tsx +58 -2
- package/src/components/storyboard/StoryboardView.tsx +9 -2
- package/src/components/storyboard/frameComments.test.ts +95 -0
- package/src/components/storyboard/frameComments.ts +115 -0
- package/src/components/storyboard/useFrameComments.ts +82 -0
- package/src/contexts/DesignPanelInputContext.test.tsx +81 -0
- package/src/contexts/DesignPanelInputContext.tsx +48 -0
- package/src/contexts/DomEditContext.tsx +4 -0
- package/src/contexts/FileManagerContext.tsx +3 -0
- package/src/contexts/PanelLayoutContext.tsx +3 -0
- package/src/contexts/VariablePromoteContext.test.tsx +67 -0
- package/src/contexts/VariablePromoteContext.tsx +10 -5
- package/src/hooks/domEditCommitRunner.ts +11 -0
- package/src/hooks/gsapScriptCommitTypes.ts +6 -4
- package/src/hooks/timelineEditingHelpers.ts +2 -2
- package/src/hooks/timelineTrackVisibility.test.ts +52 -0
- package/src/hooks/timelineTrackVisibility.ts +27 -10
- package/src/hooks/useAppHotkeys.ts +9 -0
- package/src/hooks/useDomEditAttributeCommits.ts +123 -1
- package/src/hooks/useDomEditCommits.test.tsx +41 -1
- package/src/hooks/useDomEditCommits.ts +31 -177
- package/src/hooks/useDomEditCommitsHelpers.ts +158 -0
- package/src/hooks/useDomEditSession.ts +18 -6
- package/src/hooks/useDomEditTextCommits.ts +14 -9
- package/src/hooks/useDomEditWiring.ts +10 -9
- package/src/hooks/useEditorSave.ts +1 -1
- package/src/hooks/useElementLifecycleOps.ts +8 -3
- package/src/hooks/useFileManager.projectOwnership.test.tsx +109 -0
- package/src/hooks/useFileManager.ts +151 -65
- package/src/hooks/useGsapAnimationOps.ts +7 -6
- package/src/hooks/useGsapKeyframeOps.ts +6 -5
- package/src/hooks/useGsapPropertyDebounce.ts +43 -28
- package/src/hooks/useGsapPropertyDebounceFlush.test.ts +35 -0
- package/src/hooks/useGsapScriptCommits.test.tsx +48 -2
- package/src/hooks/useGsapScriptCommits.ts +150 -50
- package/src/hooks/useGsapSelectionHandlers.test.tsx +116 -0
- package/src/hooks/useGsapSelectionHandlers.ts +94 -31
- package/src/hooks/useInspectorSplitResize.ts +51 -0
- package/src/hooks/usePanelLayout.test.ts +118 -0
- package/src/hooks/usePanelLayout.ts +24 -1
- package/src/hooks/usePersistentEditHistory.projectOwnership.test.tsx +60 -0
- package/src/hooks/usePersistentEditHistory.test.ts +49 -1
- package/src/hooks/usePersistentEditHistory.ts +71 -31
- package/src/hooks/usePreviewPersistence.ts +4 -1
- package/src/hooks/useProjectCompositionVariables.ts +10 -10
- package/src/hooks/useProjectSignaturePoll.test.tsx +135 -0
- package/src/hooks/useProjectSignaturePoll.ts +68 -0
- package/src/hooks/useRazorSplit.history.test.tsx +12 -4
- package/src/hooks/useRazorSplit.test.tsx +88 -0
- package/src/hooks/useRazorSplit.testHelpers.ts +3 -2
- package/src/hooks/useRazorSplit.ts +50 -16
- package/src/hooks/useSdkSession.lifecycle.test.tsx +137 -0
- package/src/hooks/useSdkSession.ts +155 -14
- package/src/hooks/useSlideshowPersist.ts +7 -1
- package/src/hooks/useStoryboard.ts +23 -4
- package/src/hooks/useTimelineEditing.test.tsx +4 -1
- package/src/hooks/useTimelineEditing.ts +18 -10
- package/src/hooks/useTimelineEditingTypes.ts +5 -1
- package/src/hooks/useTimelineGroupEditing.ts +17 -3
- package/src/hooks/useVariablesPersist.ts +9 -7
- package/src/icons/SystemIcons.tsx +2 -0
- package/src/styles/studio.css +22 -0
- package/src/styles/tailwind-preset.shared.js +7 -0
- package/src/utils/designInputTracking.test.ts +97 -0
- package/src/utils/designInputTracking.ts +80 -0
- package/src/utils/domEditSaveQueue.test.ts +19 -0
- package/src/utils/domEditSaveQueue.ts +2 -1
- package/src/utils/sdkCutover.gate.test.ts +7 -7
- package/src/utils/sdkCutover.test.ts +511 -62
- package/src/utils/sdkCutover.ts +181 -219
- package/src/utils/sdkEditTransaction.ts +293 -0
- package/src/utils/sdkResolverAttempts.ts +94 -0
- package/src/utils/sdkResolverShadow.test.ts +148 -0
- package/src/utils/sdkResolverShadow.ts +139 -106
- package/src/utils/setSlideshowManifest.test.ts +0 -9
- package/src/utils/setSlideshowManifest.ts +20 -27
- package/src/utils/studioFileHistory.test.ts +41 -0
- package/src/utils/studioFileHistory.ts +34 -29
- package/src/utils/studioFileMutationCoordinator.ts +45 -0
- package/src/utils/studioFileVersion.test.ts +36 -0
- package/src/utils/studioFileVersion.ts +23 -0
- package/src/utils/studioHelpers.test.ts +45 -0
- package/src/utils/studioHelpers.ts +45 -14
- package/src/utils/studioSaveDiagnostics.test.ts +18 -0
- package/src/utils/studioSaveDiagnostics.ts +38 -0
- package/dist/assets/index-B_UvTX3E.js +0 -423
- package/dist/assets/index-uahwWkgw.css +0 -1
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useState, useCallback, useRef } from "react";
|
|
1
|
+
import { useState, useCallback, useMemo, useRef } from "react";
|
|
2
2
|
import type { EditingFile } from "../utils/studioHelpers";
|
|
3
3
|
import { FONT_EXT, isMediaFile } from "../utils/mediaTypes";
|
|
4
4
|
import { fontFamilyFromAssetPath, type ImportedFontAsset } from "../components/editor/fontAssets";
|
|
@@ -7,8 +7,10 @@ import { findTagByTarget, type PatchTarget } from "../utils/sourcePatcher";
|
|
|
7
7
|
import {
|
|
8
8
|
createStudioSaveHttpError,
|
|
9
9
|
retryStudioSave,
|
|
10
|
+
StudioFileConflictError,
|
|
10
11
|
StudioSaveNetworkError,
|
|
11
12
|
} from "../utils/studioSaveDiagnostics";
|
|
13
|
+
import { createStudioWriteToken, studioExpectedFileVersion } from "../utils/studioFileVersion";
|
|
12
14
|
import { useFileTree } from "./useFileTree";
|
|
13
15
|
import { useEditorSave } from "./useEditorSave";
|
|
14
16
|
|
|
@@ -50,6 +52,17 @@ export function useFileManager({
|
|
|
50
52
|
projectIdRef.current = projectId;
|
|
51
53
|
|
|
52
54
|
const importedFontAssetsRef = useRef<ImportedFontAsset[]>([]);
|
|
55
|
+
const fileVersionScope = useMemo(
|
|
56
|
+
() => ({ projectId, versions: new Map<string, string | null>() }),
|
|
57
|
+
[projectId],
|
|
58
|
+
);
|
|
59
|
+
const fileVersions = fileVersionScope.versions;
|
|
60
|
+
const observeProjectFileVersion = useCallback(
|
|
61
|
+
(path: string, version: string | null) => {
|
|
62
|
+
fileVersions.set(path, version);
|
|
63
|
+
},
|
|
64
|
+
[fileVersions],
|
|
65
|
+
);
|
|
53
66
|
|
|
54
67
|
// ── File tree ──
|
|
55
68
|
|
|
@@ -66,38 +79,95 @@ export function useFileManager({
|
|
|
66
79
|
|
|
67
80
|
// ── Core file I/O ──
|
|
68
81
|
|
|
69
|
-
const readProjectFile = useCallback(
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
82
|
+
const readProjectFile = useCallback(
|
|
83
|
+
async (path: string): Promise<string> => {
|
|
84
|
+
if (!projectId) throw new Error("No active project");
|
|
85
|
+
const response = await fetch(
|
|
86
|
+
`/api/projects/${encodeURIComponent(projectId)}/files/${encodeURIComponent(path)}`,
|
|
87
|
+
);
|
|
88
|
+
if (!response.ok) throw new Error(`Failed to read ${path}`);
|
|
89
|
+
const data = (await response.json()) as { content?: string; version?: string };
|
|
90
|
+
if (typeof data.content !== "string") throw new Error(`Missing file contents for ${path}`);
|
|
91
|
+
fileVersions.set(path, data.version ?? response.headers.get("etag"));
|
|
92
|
+
return data.content;
|
|
93
|
+
},
|
|
94
|
+
[fileVersions, projectId],
|
|
95
|
+
);
|
|
78
96
|
|
|
79
|
-
const writeProjectFile = useCallback(
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
let
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
97
|
+
const writeProjectFile = useCallback(
|
|
98
|
+
async (path: string, content: string, expectedContent?: string): Promise<void> => {
|
|
99
|
+
if (!projectId) throw new Error("No active project");
|
|
100
|
+
const writeProjectId = projectId;
|
|
101
|
+
let expectedVersion = await studioExpectedFileVersion(fileVersions, path, expectedContent);
|
|
102
|
+
if (expectedVersion === undefined) {
|
|
103
|
+
const preflight = await fetch(
|
|
104
|
+
`/api/projects/${encodeURIComponent(writeProjectId)}/files/${encodeURIComponent(path)}`,
|
|
105
|
+
);
|
|
106
|
+
if (preflight.ok) {
|
|
107
|
+
const data = (await preflight.json()) as { content?: string; version?: string };
|
|
108
|
+
throw new StudioFileConflictError({
|
|
109
|
+
filePath: path,
|
|
110
|
+
currentVersion: data.version ?? preflight.headers.get("etag"),
|
|
111
|
+
currentContent: data.content ?? null,
|
|
112
|
+
attemptedContent: content,
|
|
113
|
+
});
|
|
114
|
+
} else if (preflight.status === 404) {
|
|
115
|
+
expectedVersion = null;
|
|
116
|
+
} else {
|
|
117
|
+
throw await createStudioSaveHttpError(preflight, `Failed to read ${path} before save`);
|
|
118
|
+
}
|
|
94
119
|
}
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
120
|
+
const writeToken = createStudioWriteToken();
|
|
121
|
+
await retryStudioSave(async () => {
|
|
122
|
+
let response: Response;
|
|
123
|
+
try {
|
|
124
|
+
response = await fetch(
|
|
125
|
+
`/api/projects/${encodeURIComponent(writeProjectId)}/files/${encodeURIComponent(path)}`,
|
|
126
|
+
{
|
|
127
|
+
method: "PUT",
|
|
128
|
+
headers: {
|
|
129
|
+
"Content-Type": "text/plain",
|
|
130
|
+
"X-Hyperframes-Write-Token": writeToken,
|
|
131
|
+
...(expectedVersion ? { "If-Match": expectedVersion } : { "If-None-Match": "*" }),
|
|
132
|
+
},
|
|
133
|
+
body: content,
|
|
134
|
+
},
|
|
135
|
+
);
|
|
136
|
+
} catch (error) {
|
|
137
|
+
throw new StudioSaveNetworkError(`Failed to save ${path}: network error`, {
|
|
138
|
+
cause: error,
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
if (response.status === 409) {
|
|
142
|
+
const conflict = (await response.json().catch(() => null)) as {
|
|
143
|
+
currentVersion?: string | null;
|
|
144
|
+
currentContent?: string | null;
|
|
145
|
+
} | null;
|
|
146
|
+
const currentVersion = conflict?.currentVersion ?? null;
|
|
147
|
+
if (currentVersion && conflict?.currentContent === content) {
|
|
148
|
+
fileVersions.set(path, currentVersion);
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
throw new StudioFileConflictError({
|
|
152
|
+
filePath: path,
|
|
153
|
+
currentVersion,
|
|
154
|
+
currentContent: conflict?.currentContent ?? null,
|
|
155
|
+
attemptedContent: content,
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
if (!response.ok) throw await createStudioSaveHttpError(response, `Failed to save ${path}`);
|
|
159
|
+
const result = (await response.json()) as { version?: string };
|
|
160
|
+
const version = result.version ?? response.headers.get("etag");
|
|
161
|
+
if (!version)
|
|
162
|
+
throw new Error(`Save response for ${path} did not include a content version`);
|
|
163
|
+
fileVersions.set(path, version);
|
|
164
|
+
});
|
|
165
|
+
if (projectIdRef.current === writeProjectId && editingPathRef.current === path) {
|
|
166
|
+
setEditingFile({ path, content });
|
|
167
|
+
}
|
|
168
|
+
},
|
|
169
|
+
[fileVersions, projectId],
|
|
170
|
+
);
|
|
101
171
|
|
|
102
172
|
const updateEditingFileContent = useCallback((path: string, content: string) => {
|
|
103
173
|
if (editingPathRef.current === path) {
|
|
@@ -105,16 +175,19 @@ export function useFileManager({
|
|
|
105
175
|
}
|
|
106
176
|
}, []);
|
|
107
177
|
|
|
108
|
-
const readOptionalProjectFile = useCallback(
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
178
|
+
const readOptionalProjectFile = useCallback(
|
|
179
|
+
async (path: string): Promise<string> => {
|
|
180
|
+
if (!projectId) throw new Error("No active project");
|
|
181
|
+
const response = await fetch(
|
|
182
|
+
`/api/projects/${encodeURIComponent(projectId)}/files/${encodeURIComponent(path)}?optional=1`,
|
|
183
|
+
);
|
|
184
|
+
if (!response.ok) throw new Error(`Failed to read ${path}`);
|
|
185
|
+
const data = (await response.json()) as { content?: string; version?: string };
|
|
186
|
+
fileVersions.set(path, data.version ?? response.headers.get("etag"));
|
|
187
|
+
return typeof data.content === "string" ? data.content : "";
|
|
188
|
+
},
|
|
189
|
+
[fileVersions, projectId],
|
|
190
|
+
);
|
|
118
191
|
|
|
119
192
|
// ── Editor save (debounced content change) ──
|
|
120
193
|
|
|
@@ -146,13 +219,14 @@ export function useFileManager({
|
|
|
146
219
|
setEditingFile({ path, content: null });
|
|
147
220
|
return;
|
|
148
221
|
}
|
|
149
|
-
fetch(`/api/projects/${pid}/files/${encodeURIComponent(path)}`)
|
|
222
|
+
fetch(`/api/projects/${encodeURIComponent(pid)}/files/${encodeURIComponent(path)}`)
|
|
150
223
|
.then((r) => {
|
|
151
224
|
if (!r.ok) throw new Error(`Failed to load ${path} (${r.status})`);
|
|
152
225
|
return r.json();
|
|
153
226
|
})
|
|
154
|
-
.then((data: { content?: string }) => {
|
|
227
|
+
.then((data: { content?: string; version?: string }) => {
|
|
155
228
|
if (data.content != null) {
|
|
229
|
+
fileVersions.set(path, data.version ?? null);
|
|
156
230
|
setEditingFile({ path, content: data.content });
|
|
157
231
|
}
|
|
158
232
|
})
|
|
@@ -160,7 +234,7 @@ export function useFileManager({
|
|
|
160
234
|
showToast(err instanceof Error ? err.message : `Failed to load ${path}`, "error");
|
|
161
235
|
});
|
|
162
236
|
},
|
|
163
|
-
[showToast],
|
|
237
|
+
[fileVersions, showToast],
|
|
164
238
|
);
|
|
165
239
|
|
|
166
240
|
// ── Click-to-source ──
|
|
@@ -179,13 +253,14 @@ export function useFileManager({
|
|
|
179
253
|
const requestId = ++revealRequestIdRef.current;
|
|
180
254
|
const controller = new AbortController();
|
|
181
255
|
revealAbortRef.current = controller;
|
|
182
|
-
fetch(`/api/projects/${pid}/files/${encodeURIComponent(sourceFile)}`, {
|
|
256
|
+
fetch(`/api/projects/${encodeURIComponent(pid)}/files/${encodeURIComponent(sourceFile)}`, {
|
|
183
257
|
signal: controller.signal,
|
|
184
258
|
})
|
|
185
259
|
.then((r) => r.json())
|
|
186
|
-
.then((data: { content?: string }) => {
|
|
260
|
+
.then((data: { content?: string; version?: string }) => {
|
|
187
261
|
if (requestId !== revealRequestIdRef.current) return;
|
|
188
262
|
if (data.content != null) {
|
|
263
|
+
fileVersions.set(sourceFile, data.version ?? null);
|
|
189
264
|
setEditingFile({ path: sourceFile, content: data.content });
|
|
190
265
|
const match = findTagByTarget(data.content, target);
|
|
191
266
|
setRevealSourceOffset(match ? match.start : null);
|
|
@@ -193,7 +268,7 @@ export function useFileManager({
|
|
|
193
268
|
})
|
|
194
269
|
.catch(() => {});
|
|
195
270
|
},
|
|
196
|
-
[editingFile?.content],
|
|
271
|
+
[editingFile?.content, fileVersions],
|
|
197
272
|
);
|
|
198
273
|
|
|
199
274
|
// ── Upload ──
|
|
@@ -211,7 +286,7 @@ export function useFileManager({
|
|
|
211
286
|
|
|
212
287
|
const qs = dir ? `?dir=${encodeURIComponent(dir)}` : "";
|
|
213
288
|
try {
|
|
214
|
-
const res = await fetch(`/api/projects/${pid}/upload${qs}`, {
|
|
289
|
+
const res = await fetch(`/api/projects/${encodeURIComponent(pid)}/upload${qs}`, {
|
|
215
290
|
method: "POST",
|
|
216
291
|
body: formData,
|
|
217
292
|
});
|
|
@@ -251,11 +326,14 @@ export function useFileManager({
|
|
|
251
326
|
content =
|
|
252
327
|
'<!DOCTYPE html>\n<html>\n<head>\n <meta charset="UTF-8">\n</head>\n<body>\n\n</body>\n</html>\n';
|
|
253
328
|
}
|
|
254
|
-
const res = await fetch(
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
329
|
+
const res = await fetch(
|
|
330
|
+
`/api/projects/${encodeURIComponent(pid)}/files/${encodeURIComponent(path)}`,
|
|
331
|
+
{
|
|
332
|
+
method: "POST",
|
|
333
|
+
headers: { "Content-Type": "text/plain" },
|
|
334
|
+
body: content,
|
|
335
|
+
},
|
|
336
|
+
);
|
|
259
337
|
if (res.ok) {
|
|
260
338
|
await refreshFileTree();
|
|
261
339
|
handleFileSelect(path);
|
|
@@ -273,7 +351,7 @@ export function useFileManager({
|
|
|
273
351
|
const pid = projectIdRef.current;
|
|
274
352
|
if (!pid) return;
|
|
275
353
|
const res = await fetch(
|
|
276
|
-
`/api/projects/${pid}/files/${encodeURIComponent(path + "/.gitkeep")}`,
|
|
354
|
+
`/api/projects/${encodeURIComponent(pid)}/files/${encodeURIComponent(path + "/.gitkeep")}`,
|
|
277
355
|
{
|
|
278
356
|
method: "POST",
|
|
279
357
|
headers: { "Content-Type": "text/plain" },
|
|
@@ -295,9 +373,12 @@ export function useFileManager({
|
|
|
295
373
|
async (path: string) => {
|
|
296
374
|
const pid = projectIdRef.current;
|
|
297
375
|
if (!pid) return;
|
|
298
|
-
const res = await fetch(
|
|
299
|
-
|
|
300
|
-
|
|
376
|
+
const res = await fetch(
|
|
377
|
+
`/api/projects/${encodeURIComponent(pid)}/files/${encodeURIComponent(path)}`,
|
|
378
|
+
{
|
|
379
|
+
method: "DELETE",
|
|
380
|
+
},
|
|
381
|
+
);
|
|
301
382
|
if (res.ok) {
|
|
302
383
|
if (editingPathRef.current === path) setEditingFile(null);
|
|
303
384
|
await refreshFileTree();
|
|
@@ -314,11 +395,14 @@ export function useFileManager({
|
|
|
314
395
|
async (oldPath: string, newPath: string) => {
|
|
315
396
|
const pid = projectIdRef.current;
|
|
316
397
|
if (!pid) return;
|
|
317
|
-
const res = await fetch(
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
398
|
+
const res = await fetch(
|
|
399
|
+
`/api/projects/${encodeURIComponent(pid)}/files/${encodeURIComponent(oldPath)}`,
|
|
400
|
+
{
|
|
401
|
+
method: "PATCH",
|
|
402
|
+
headers: { "Content-Type": "application/json" },
|
|
403
|
+
body: JSON.stringify({ newPath }),
|
|
404
|
+
},
|
|
405
|
+
);
|
|
322
406
|
if (res.ok) {
|
|
323
407
|
if (editingPathRef.current === oldPath) {
|
|
324
408
|
handleFileSelect(newPath);
|
|
@@ -338,7 +422,7 @@ export function useFileManager({
|
|
|
338
422
|
async (path: string) => {
|
|
339
423
|
const pid = projectIdRef.current;
|
|
340
424
|
if (!pid) return;
|
|
341
|
-
const res = await fetch(`/api/projects/${pid}/duplicate-file`, {
|
|
425
|
+
const res = await fetch(`/api/projects/${encodeURIComponent(pid)}/duplicate-file`, {
|
|
342
426
|
method: "POST",
|
|
343
427
|
headers: { "Content-Type": "application/json" },
|
|
344
428
|
body: JSON.stringify({ path }),
|
|
@@ -367,17 +451,18 @@ export function useFileManager({
|
|
|
367
451
|
|
|
368
452
|
const handleImportFonts = useCallback(
|
|
369
453
|
async (files: FileList | File[]): Promise<ImportedFontAsset[]> => {
|
|
454
|
+
const pid = projectIdRef.current;
|
|
455
|
+
if (!pid) return [];
|
|
370
456
|
const uploaded = await uploadProjectFiles(
|
|
371
457
|
Array.from(files).filter((file) => FONT_EXT.test(file.name)),
|
|
372
458
|
"assets/fonts",
|
|
373
459
|
);
|
|
374
|
-
const pid = projectIdRef.current;
|
|
375
460
|
const imported = uploaded
|
|
376
461
|
.filter((asset) => FONT_EXT.test(asset))
|
|
377
462
|
.map((asset) => ({
|
|
378
463
|
family: fontFamilyFromAssetPath(asset),
|
|
379
464
|
path: asset,
|
|
380
|
-
url: `/api/projects/${pid}/preview/${asset}`,
|
|
465
|
+
url: `/api/projects/${encodeURIComponent(pid)}/preview/${asset}`,
|
|
381
466
|
}));
|
|
382
467
|
importedFontAssetsRef.current = [
|
|
383
468
|
...imported,
|
|
@@ -412,6 +497,7 @@ export function useFileManager({
|
|
|
412
497
|
readProjectFile,
|
|
413
498
|
writeProjectFile,
|
|
414
499
|
readOptionalProjectFile,
|
|
500
|
+
observeProjectFileVersion,
|
|
415
501
|
updateEditingFileContent,
|
|
416
502
|
|
|
417
503
|
// Click-to-source
|
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
sdkGsapDeleteAllForSelectorPersist,
|
|
8
8
|
sdkAddWithKeyframesPersist,
|
|
9
9
|
sdkReplaceWithKeyframesPersist,
|
|
10
|
+
cutoverCommittedOrThrow,
|
|
10
11
|
type CutoverDeps,
|
|
11
12
|
} from "../utils/sdkCutover";
|
|
12
13
|
import {
|
|
@@ -52,7 +53,7 @@ export function useGsapAnimationOps({
|
|
|
52
53
|
sdkDeps,
|
|
53
54
|
{ label: "Edit GSAP animation", coalesceKey: `gsap:${animationId}:meta` },
|
|
54
55
|
);
|
|
55
|
-
if (handled) return;
|
|
56
|
+
if (cutoverCommittedOrThrow(handled)) return;
|
|
56
57
|
}
|
|
57
58
|
commitMutationSafely(
|
|
58
59
|
selection,
|
|
@@ -74,7 +75,7 @@ export function useGsapAnimationOps({
|
|
|
74
75
|
sdkDeps,
|
|
75
76
|
{ label: "Delete GSAP animation" },
|
|
76
77
|
);
|
|
77
|
-
if (handled) return;
|
|
78
|
+
if (cutoverCommittedOrThrow(handled)) return;
|
|
78
79
|
}
|
|
79
80
|
commitMutationSafely(
|
|
80
81
|
selection,
|
|
@@ -96,7 +97,7 @@ export function useGsapAnimationOps({
|
|
|
96
97
|
sdkDeps,
|
|
97
98
|
{ label: "Delete all animations for element" },
|
|
98
99
|
);
|
|
99
|
-
if (handled) return;
|
|
100
|
+
if (cutoverCommittedOrThrow(handled)) return;
|
|
100
101
|
}
|
|
101
102
|
void commitMutation(
|
|
102
103
|
selection,
|
|
@@ -162,7 +163,7 @@ export function useGsapAnimationOps({
|
|
|
162
163
|
sdkDeps,
|
|
163
164
|
{ label: `Add GSAP ${method} animation` },
|
|
164
165
|
);
|
|
165
|
-
if (handled) return;
|
|
166
|
+
if (cutoverCommittedOrThrow(handled)) return;
|
|
166
167
|
}
|
|
167
168
|
|
|
168
169
|
await commitMutation(
|
|
@@ -213,7 +214,7 @@ export function useGsapAnimationOps({
|
|
|
213
214
|
sdkDeps,
|
|
214
215
|
{ label },
|
|
215
216
|
);
|
|
216
|
-
if (handled) return;
|
|
217
|
+
if (cutoverCommittedOrThrow(handled)) return;
|
|
217
218
|
}
|
|
218
219
|
void commitMutation(
|
|
219
220
|
selection,
|
|
@@ -256,7 +257,7 @@ export function useGsapAnimationOps({
|
|
|
256
257
|
sdkDeps,
|
|
257
258
|
{ label },
|
|
258
259
|
);
|
|
259
|
-
if (handled) return;
|
|
260
|
+
if (cutoverCommittedOrThrow(handled)) return;
|
|
260
261
|
}
|
|
261
262
|
void commitMutation(
|
|
262
263
|
selection,
|
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
sdkGsapRemoveKeyframePersist,
|
|
9
9
|
sdkGsapRemoveAllKeyframesPersist,
|
|
10
10
|
sdkGsapConvertToKeyframesPersist,
|
|
11
|
+
cutoverCommittedOrThrow,
|
|
11
12
|
type CutoverDeps,
|
|
12
13
|
} from "../utils/sdkCutover";
|
|
13
14
|
import type { KeyframeCacheEntry } from "../player/store/playerStore";
|
|
@@ -136,7 +137,7 @@ export function useGsapKeyframeOps({
|
|
|
136
137
|
coalesceKey: `gsap:${animationId}:kf:${percentage}`,
|
|
137
138
|
},
|
|
138
139
|
);
|
|
139
|
-
if (handled) return;
|
|
140
|
+
if (cutoverCommittedOrThrow(handled)) return;
|
|
140
141
|
}
|
|
141
142
|
await commitMutation(selection, mutation, {
|
|
142
143
|
label: `Add keyframe at ${percentage}%`,
|
|
@@ -169,7 +170,7 @@ export function useGsapKeyframeOps({
|
|
|
169
170
|
sdkDeps,
|
|
170
171
|
toSdkPersistOptions(`Add keyframe at ${percentage}%`, commitOverrides),
|
|
171
172
|
);
|
|
172
|
-
if (handled) return;
|
|
173
|
+
if (cutoverCommittedOrThrow(handled)) return;
|
|
173
174
|
}
|
|
174
175
|
return commitMutation(
|
|
175
176
|
selection,
|
|
@@ -218,7 +219,7 @@ export function useGsapKeyframeOps({
|
|
|
218
219
|
sdkDeps,
|
|
219
220
|
toSdkPersistOptions(label, commitOverrides),
|
|
220
221
|
);
|
|
221
|
-
if (handled) return;
|
|
222
|
+
if (cutoverCommittedOrThrow(handled)) return;
|
|
222
223
|
}
|
|
223
224
|
const commitOptions = commitOverrides?.skipReload
|
|
224
225
|
? { label, ...commitOverrides }
|
|
@@ -300,7 +301,7 @@ export function useGsapKeyframeOps({
|
|
|
300
301
|
sdkDeps,
|
|
301
302
|
toSdkPersistOptions("Convert to keyframes", commitOverrides),
|
|
302
303
|
);
|
|
303
|
-
if (handled) return;
|
|
304
|
+
if (cutoverCommittedOrThrow(handled)) return;
|
|
304
305
|
}
|
|
305
306
|
return commitMutation(
|
|
306
307
|
selection,
|
|
@@ -329,7 +330,7 @@ export function useGsapKeyframeOps({
|
|
|
329
330
|
sdkDeps,
|
|
330
331
|
{ label: "Remove all keyframes" },
|
|
331
332
|
);
|
|
332
|
-
if (handled) return;
|
|
333
|
+
if (cutoverCommittedOrThrow(handled)) return;
|
|
333
334
|
}
|
|
334
335
|
commitMutationSafely(
|
|
335
336
|
selection,
|
|
@@ -5,6 +5,7 @@ import type { DomEditSelection } from "../components/editor/domEditingTypes";
|
|
|
5
5
|
import {
|
|
6
6
|
sdkGsapTweenPersist,
|
|
7
7
|
sdkGsapRemovePropertyPersist,
|
|
8
|
+
cutoverCommittedOrThrow,
|
|
8
9
|
type CutoverDeps,
|
|
9
10
|
} from "../utils/sdkCutover";
|
|
10
11
|
import { extractGsapScriptText } from "../utils/gsapSoftReload";
|
|
@@ -45,6 +46,12 @@ interface SdkPropertyDeps {
|
|
|
45
46
|
sdkSession?: Composition | null;
|
|
46
47
|
sdkDeps?: CutoverDeps | null;
|
|
47
48
|
activeCompPath?: string | null;
|
|
49
|
+
onFlushError?: (
|
|
50
|
+
error: unknown,
|
|
51
|
+
selection: DomEditSelection,
|
|
52
|
+
mutation: Record<string, unknown>,
|
|
53
|
+
label: string,
|
|
54
|
+
) => void;
|
|
48
55
|
}
|
|
49
56
|
|
|
50
57
|
export function useGsapPropertyDebounce(
|
|
@@ -68,38 +75,46 @@ export function useGsapPropertyDebounce(
|
|
|
68
75
|
const sdkRef = useRef(sdk);
|
|
69
76
|
sdkRef.current = sdk;
|
|
70
77
|
|
|
78
|
+
// fallow-ignore-next-line complexity
|
|
71
79
|
const flushPendingPropertyEdit = useCallback(async () => {
|
|
72
80
|
const pending = pendingPropertyEditRef.current;
|
|
73
81
|
if (!pending) return;
|
|
74
82
|
pendingPropertyEditRef.current = null;
|
|
75
83
|
const { selection, animationId, property, value } = pending;
|
|
76
|
-
const {
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
const
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
84
|
+
const mutation = { type: "update-property", animationId, property, value };
|
|
85
|
+
const label = `Edit GSAP ${property}`;
|
|
86
|
+
try {
|
|
87
|
+
const { sdkSession, sdkDeps, activeCompPath } = sdkRef.current ?? {};
|
|
88
|
+
if (sdkSession && sdkDeps) {
|
|
89
|
+
const targetPath = selection.sourceFile || activeCompPath || "index.html";
|
|
90
|
+
const handled = await sdkGsapTweenPersist(
|
|
91
|
+
targetPath,
|
|
92
|
+
{
|
|
93
|
+
kind: "set",
|
|
94
|
+
animationId,
|
|
95
|
+
properties: {
|
|
96
|
+
properties: mergeTweenProperties(
|
|
97
|
+
sdkSession,
|
|
98
|
+
animationId,
|
|
99
|
+
{ [property]: value },
|
|
100
|
+
"to",
|
|
101
|
+
),
|
|
102
|
+
},
|
|
86
103
|
},
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
selection,
|
|
96
|
-
{ type: "update-property", animationId, property, value },
|
|
97
|
-
{
|
|
98
|
-
label: `Edit GSAP ${property}`,
|
|
104
|
+
sdkSession,
|
|
105
|
+
sdkDeps,
|
|
106
|
+
{ label, coalesceKey: `gsap:${animationId}:${property}` },
|
|
107
|
+
);
|
|
108
|
+
if (cutoverCommittedOrThrow(handled)) return;
|
|
109
|
+
}
|
|
110
|
+
await commitMutationSafely(selection, mutation, {
|
|
111
|
+
label,
|
|
99
112
|
coalesceKey: `gsap:${animationId}:${property}`,
|
|
100
113
|
softReload: true,
|
|
101
|
-
}
|
|
102
|
-
)
|
|
114
|
+
});
|
|
115
|
+
} catch (error) {
|
|
116
|
+
sdkRef.current?.onFlushError?.(error, selection, mutation, label);
|
|
117
|
+
}
|
|
103
118
|
}, [commitMutationSafely]);
|
|
104
119
|
|
|
105
120
|
const updateGsapProperty = useCallback(
|
|
@@ -162,7 +177,7 @@ export function useGsapPropertyDebounce(
|
|
|
162
177
|
sdkDeps,
|
|
163
178
|
{ label: `Add GSAP ${property}` },
|
|
164
179
|
);
|
|
165
|
-
if (handled) return;
|
|
180
|
+
if (cutoverCommittedOrThrow(handled)) return;
|
|
166
181
|
}
|
|
167
182
|
commitMutationSafely(
|
|
168
183
|
selection,
|
|
@@ -187,7 +202,7 @@ export function useGsapPropertyDebounce(
|
|
|
187
202
|
sdkDeps,
|
|
188
203
|
{ label: `Remove GSAP ${from ? `from-${property}` : property}` },
|
|
189
204
|
);
|
|
190
|
-
if (handled) return;
|
|
205
|
+
if (cutoverCommittedOrThrow(handled)) return;
|
|
191
206
|
}
|
|
192
207
|
if (from) {
|
|
193
208
|
commitMutationSafely(
|
|
@@ -247,7 +262,7 @@ export function useGsapPropertyDebounce(
|
|
|
247
262
|
coalesceKey: `gsap:${animationId}:from:${property}`,
|
|
248
263
|
},
|
|
249
264
|
);
|
|
250
|
-
if (handled) return;
|
|
265
|
+
if (cutoverCommittedOrThrow(handled)) return;
|
|
251
266
|
}
|
|
252
267
|
commitMutationSafely(
|
|
253
268
|
selection,
|
|
@@ -285,7 +300,7 @@ export function useGsapPropertyDebounce(
|
|
|
285
300
|
sdkDeps,
|
|
286
301
|
{ label: `Add GSAP from-${property}` },
|
|
287
302
|
);
|
|
288
|
-
if (handled) return;
|
|
303
|
+
if (cutoverCommittedOrThrow(handled)) return;
|
|
289
304
|
}
|
|
290
305
|
commitMutationSafely(
|
|
291
306
|
selection,
|
|
@@ -82,4 +82,39 @@ describe("useGsapPropertyDebounce flush stability (finding #7)", () => {
|
|
|
82
82
|
root.unmount();
|
|
83
83
|
});
|
|
84
84
|
});
|
|
85
|
+
|
|
86
|
+
it("reports a rejected debounced flush instead of leaking an unhandled rejection", async () => {
|
|
87
|
+
const error = new Error("save failed");
|
|
88
|
+
const commitMutationSafely = vi.fn().mockRejectedValue(error);
|
|
89
|
+
const onFlushError = vi.fn();
|
|
90
|
+
let queueEdit: (() => void) | null = null;
|
|
91
|
+
|
|
92
|
+
function Harness() {
|
|
93
|
+
const ops = useGsapPropertyDebounce(commitMutationSafely, {
|
|
94
|
+
sdkSession: null,
|
|
95
|
+
sdkDeps: null,
|
|
96
|
+
activeCompPath: "index.html",
|
|
97
|
+
onFlushError,
|
|
98
|
+
});
|
|
99
|
+
queueEdit = () => ops.updateGsapProperty(selection, "tw-1", "x", 42);
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const root = createRoot(container);
|
|
104
|
+
await act(async () => {
|
|
105
|
+
root.render(React.createElement(Harness));
|
|
106
|
+
});
|
|
107
|
+
act(() => queueEdit?.());
|
|
108
|
+
await act(async () => {
|
|
109
|
+
await vi.advanceTimersByTimeAsync(200);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
expect(onFlushError).toHaveBeenCalledWith(
|
|
113
|
+
error,
|
|
114
|
+
selection,
|
|
115
|
+
{ type: "update-property", animationId: "tw-1", property: "x", value: 42 },
|
|
116
|
+
"Edit GSAP x",
|
|
117
|
+
);
|
|
118
|
+
act(() => root.unmount());
|
|
119
|
+
});
|
|
85
120
|
});
|