@hyperframes/studio 0.7.58 → 0.7.59
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-Bm07FLkl.js} +1 -1
- package/dist/assets/{index-C47jAC3Q.js → index-B995FG46.js} +1 -1
- package/dist/assets/index-CrkAdJkb.js +426 -0
- package/dist/assets/index-Dj5p8U_A.css +1 -0
- package/dist/assets/{index-DeQPzqwH.js → index-FvzmPhfG.js} +1 -1
- package/dist/index.d.ts +12 -3
- package/dist/index.html +2 -2
- package/dist/index.js +13514 -8528
- 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 +3 -3
- package/src/components/DesignPanelPromoteProvider.tsx +15 -2
- package/src/components/StudioRightPanel.tsx +29 -51
- 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 +101 -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/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/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.ts +29 -175
- package/src/hooks/useDomEditCommitsHelpers.ts +158 -0
- package/src/hooks/useDomEditSession.ts +17 -5
- package/src/hooks/useDomEditTextCommits.ts +14 -9
- package/src/hooks/useDomEditWiring.ts +10 -9
- package/src/hooks/useElementLifecycleOps.ts +8 -3
- package/src/hooks/useFileManager.projectOwnership.test.tsx +98 -0
- package/src/hooks/useFileManager.ts +82 -60
- 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/usePersistentEditHistory.projectOwnership.test.tsx +60 -0
- package/src/hooks/usePersistentEditHistory.test.ts +49 -1
- package/src/hooks/usePersistentEditHistory.ts +71 -31
- 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/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 +16 -10
- package/src/hooks/useTimelineEditingTypes.ts +3 -0
- package/src/hooks/useTimelineGroupEditing.ts +16 -2
- 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/sdkCutover.gate.test.ts +7 -7
- package/src/utils/sdkCutover.test.ts +490 -57
- package/src/utils/sdkCutover.ts +181 -219
- package/src/utils/sdkEditTransaction.ts +292 -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 +30 -27
- package/src/utils/studioFileMutationCoordinator.ts +45 -0
- package/src/utils/studioHelpers.test.ts +45 -0
- package/src/utils/studioHelpers.ts +45 -14
- package/src/utils/studioSaveDiagnostics.ts +17 -0
- package/dist/assets/index-B_UvTX3E.js +0 -423
- package/dist/assets/index-uahwWkgw.css +0 -1
|
@@ -145,15 +145,19 @@ export function useDomEditTextCommits({
|
|
|
145
145
|
const domTextCommitVersionRef = useRef(0);
|
|
146
146
|
const domStyleCommitVersionRef = useRef(new Map<string, number>());
|
|
147
147
|
|
|
148
|
-
const {
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
148
|
+
const {
|
|
149
|
+
handleDomAttributeCommit,
|
|
150
|
+
handleDomAttributeLiveCommit,
|
|
151
|
+
handleDomHtmlAttributeCommit,
|
|
152
|
+
handleDomAttributesCommit,
|
|
153
|
+
} = useDomEditAttributeCommits({
|
|
154
|
+
activeCompPath,
|
|
155
|
+
previewIframeRef,
|
|
156
|
+
showToast,
|
|
157
|
+
domEditSelection,
|
|
158
|
+
refreshDomEditSelectionFromPreview,
|
|
159
|
+
persistDomEditOperations,
|
|
160
|
+
});
|
|
157
161
|
|
|
158
162
|
const handleDomStyleCommit = useCallback(
|
|
159
163
|
async (property: string, value: string) => {
|
|
@@ -471,6 +475,7 @@ export function useDomEditTextCommits({
|
|
|
471
475
|
handleDomAttributeCommit,
|
|
472
476
|
handleDomAttributeLiveCommit,
|
|
473
477
|
handleDomHtmlAttributeCommit,
|
|
478
|
+
handleDomAttributesCommit,
|
|
474
479
|
handleDomTextCommit,
|
|
475
480
|
commitDomTextFields,
|
|
476
481
|
handleDomTextFieldStyleCommit,
|
|
@@ -55,24 +55,24 @@ export interface UseDomEditWiringParams {
|
|
|
55
55
|
sel: DomEditSelection,
|
|
56
56
|
animId: string,
|
|
57
57
|
updates: { duration?: number; ease?: string; position?: number },
|
|
58
|
-
) => void
|
|
59
|
-
deleteGsapAnimation: (sel: DomEditSelection, animId: string) => void
|
|
60
|
-
deleteAllForSelector: (sel: DomEditSelection, targetSelector: string) => void
|
|
58
|
+
) => Promise<void>;
|
|
59
|
+
deleteGsapAnimation: (sel: DomEditSelection, animId: string) => Promise<void>;
|
|
60
|
+
deleteAllForSelector: (sel: DomEditSelection, targetSelector: string) => Promise<void>;
|
|
61
61
|
addGsapAnimation: (
|
|
62
62
|
sel: DomEditSelection,
|
|
63
63
|
method: "to" | "from" | "set" | "fromTo",
|
|
64
64
|
time: number,
|
|
65
65
|
) => Promise<void>;
|
|
66
|
-
addGsapProperty: (sel: DomEditSelection, animId: string, prop: string) => void
|
|
67
|
-
removeGsapProperty: (sel: DomEditSelection, animId: string, prop: string) => void
|
|
66
|
+
addGsapProperty: (sel: DomEditSelection, animId: string, prop: string) => Promise<void>;
|
|
67
|
+
removeGsapProperty: (sel: DomEditSelection, animId: string, prop: string) => Promise<void>;
|
|
68
68
|
updateGsapFromProperty: (
|
|
69
69
|
sel: DomEditSelection,
|
|
70
70
|
animId: string,
|
|
71
71
|
prop: string,
|
|
72
72
|
value: number | string,
|
|
73
|
-
) => void
|
|
74
|
-
addGsapFromProperty: (sel: DomEditSelection, animId: string, prop: string) => void
|
|
75
|
-
removeGsapFromProperty: (sel: DomEditSelection, animId: string, prop: string) => void
|
|
73
|
+
) => Promise<void>;
|
|
74
|
+
addGsapFromProperty: (sel: DomEditSelection, animId: string, prop: string) => Promise<void>;
|
|
75
|
+
removeGsapFromProperty: (sel: DomEditSelection, animId: string, prop: string) => Promise<void>;
|
|
76
76
|
addKeyframe: (
|
|
77
77
|
sel: DomEditSelection,
|
|
78
78
|
animId: string,
|
|
@@ -105,7 +105,7 @@ export interface UseDomEditWiringParams {
|
|
|
105
105
|
animId: string,
|
|
106
106
|
resolvedFromValues?: Record<string, number | string>,
|
|
107
107
|
) => Promise<void>;
|
|
108
|
-
removeAllKeyframes: (sel: DomEditSelection, animId: string) => void
|
|
108
|
+
removeAllKeyframes: (sel: DomEditSelection, animId: string) => Promise<void>;
|
|
109
109
|
handleDomManualEditsReset: (sel: DomEditSelection) => void;
|
|
110
110
|
}
|
|
111
111
|
|
|
@@ -245,6 +245,7 @@ export function useDomEditWiring({
|
|
|
245
245
|
removeAllKeyframes,
|
|
246
246
|
handleDomManualEditsReset,
|
|
247
247
|
selectedGsapAnimations,
|
|
248
|
+
showToast,
|
|
248
249
|
});
|
|
249
250
|
|
|
250
251
|
// ── Preview sync side-effects ──
|
|
@@ -20,10 +20,15 @@ import {
|
|
|
20
20
|
type LayerRevealCommitOwnership,
|
|
21
21
|
} from "../components/editor/useLayerRevealOverride";
|
|
22
22
|
import type { CommitDomEditPatchBatches, DomEditPatchBatch } from "./domEditCommitTypes";
|
|
23
|
+
import { cutoverCommittedOrThrow, type CutoverResult } from "../utils/sdkCutover";
|
|
23
24
|
|
|
24
25
|
interface UseElementLifecycleOpsParams extends DomEditCommitBaseParams {
|
|
25
|
-
/** Route delete through SDK when session resolves the hf-id
|
|
26
|
-
onTrySdkDelete?: (
|
|
26
|
+
/** Route delete through SDK when session resolves the hf-id. */
|
|
27
|
+
onTrySdkDelete?: (
|
|
28
|
+
hfId: string,
|
|
29
|
+
originalContent: string,
|
|
30
|
+
targetPath: string,
|
|
31
|
+
) => Promise<CutoverResult>;
|
|
27
32
|
/** Resolver-shadow tripwire for the reordered targets (telemetry-only, decoupled from cutover). */
|
|
28
33
|
onReorderShadow?: (targets: string[]) => void;
|
|
29
34
|
/** Resync the SDK session after a server-fallback delete. */
|
|
@@ -97,7 +102,7 @@ export function useElementLifecycleOps({
|
|
|
97
102
|
|
|
98
103
|
if (onTrySdkDelete && selection.hfId) {
|
|
99
104
|
const handled = await onTrySdkDelete(selection.hfId, originalContent, targetPath);
|
|
100
|
-
if (handled) {
|
|
105
|
+
if (cutoverCommittedOrThrow(handled)) {
|
|
101
106
|
clearDomSelection();
|
|
102
107
|
usePlayerStore.getState().setSelectedElementId(null);
|
|
103
108
|
showToast(`Deleted ${label}. Use Undo to restore it.`, "info");
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
// @vitest-environment happy-dom
|
|
2
|
+
import { act } from "react";
|
|
3
|
+
import { createRoot } from "react-dom/client";
|
|
4
|
+
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
5
|
+
|
|
6
|
+
vi.mock("./useFileTree", () => ({
|
|
7
|
+
useFileTree: () => ({
|
|
8
|
+
projectDir: "",
|
|
9
|
+
fileTree: [],
|
|
10
|
+
setFileTree: vi.fn(),
|
|
11
|
+
fileTreeLoaded: true,
|
|
12
|
+
refreshFileTree: vi.fn(async () => {}),
|
|
13
|
+
compositions: [],
|
|
14
|
+
assets: [],
|
|
15
|
+
fontAssets: [],
|
|
16
|
+
}),
|
|
17
|
+
}));
|
|
18
|
+
|
|
19
|
+
vi.mock("./useEditorSave", () => ({
|
|
20
|
+
useEditorSave: () => ({
|
|
21
|
+
saveRafRef: { current: null },
|
|
22
|
+
handleContentChange: vi.fn(),
|
|
23
|
+
}),
|
|
24
|
+
}));
|
|
25
|
+
|
|
26
|
+
import { useFileManager } from "./useFileManager";
|
|
27
|
+
|
|
28
|
+
(globalThis as unknown as { IS_REACT_ACT_ENVIRONMENT: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
|
|
29
|
+
|
|
30
|
+
describe("useFileManager project ownership", () => {
|
|
31
|
+
afterEach(() => {
|
|
32
|
+
vi.unstubAllGlobals();
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it("keeps delayed callbacks bound to the project that created them", async () => {
|
|
36
|
+
let resolveProjectARead: ((value: Response) => void) | undefined;
|
|
37
|
+
const projectARead = new Promise<Response>((resolve) => {
|
|
38
|
+
resolveProjectARead = resolve;
|
|
39
|
+
});
|
|
40
|
+
const fetchMock = vi.fn((url: string, init?: RequestInit) => {
|
|
41
|
+
if (url.includes("project-a") && !init?.method) return projectARead;
|
|
42
|
+
if (!init?.method) {
|
|
43
|
+
return Promise.resolve({
|
|
44
|
+
ok: true,
|
|
45
|
+
json: async () => ({ content: "PROJECT_B" }),
|
|
46
|
+
} as Response);
|
|
47
|
+
}
|
|
48
|
+
return Promise.resolve({ ok: true } as Response);
|
|
49
|
+
});
|
|
50
|
+
vi.stubGlobal("fetch", fetchMock);
|
|
51
|
+
|
|
52
|
+
const captured: { manager: ReturnType<typeof useFileManager> | null } = { manager: null };
|
|
53
|
+
function Probe({ projectId }: { projectId: string }) {
|
|
54
|
+
captured.manager = useFileManager({
|
|
55
|
+
projectId,
|
|
56
|
+
showToast: vi.fn(),
|
|
57
|
+
recordEdit: vi.fn(async () => {}),
|
|
58
|
+
domEditSaveTimestampRef: { current: 0 },
|
|
59
|
+
setRefreshKey: vi.fn(),
|
|
60
|
+
});
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const root = createRoot(document.createElement("div"));
|
|
65
|
+
await act(async () => root.render(<Probe projectId="project-a/../other?x=1" />));
|
|
66
|
+
const managerA = captured.manager;
|
|
67
|
+
if (!managerA) throw new Error("project A manager did not render");
|
|
68
|
+
const delayedRead = managerA.readProjectFile("index.html");
|
|
69
|
+
|
|
70
|
+
await act(async () => root.render(<Probe projectId="project-b#fragment" />));
|
|
71
|
+
const managerB = captured.manager;
|
|
72
|
+
if (!managerB) throw new Error("project B manager did not render");
|
|
73
|
+
expect(managerB.writeProjectFile).not.toBe(managerA.writeProjectFile);
|
|
74
|
+
|
|
75
|
+
resolveProjectARead?.({
|
|
76
|
+
ok: true,
|
|
77
|
+
json: async () => ({ content: "PROJECT_A" }),
|
|
78
|
+
} as Response);
|
|
79
|
+
await expect(delayedRead).resolves.toBe("PROJECT_A");
|
|
80
|
+
await managerA.writeProjectFile("index.html", "A_AFTER");
|
|
81
|
+
await expect(managerB.readProjectFile("index.html")).resolves.toBe("PROJECT_B");
|
|
82
|
+
await expect(managerB.readOptionalProjectFile("index.html")).resolves.toBe("PROJECT_B");
|
|
83
|
+
|
|
84
|
+
expect(fetchMock).toHaveBeenCalledWith(
|
|
85
|
+
"/api/projects/project-a%2F..%2Fother%3Fx%3D1/files/index.html",
|
|
86
|
+
);
|
|
87
|
+
expect(fetchMock).toHaveBeenCalledWith(
|
|
88
|
+
"/api/projects/project-a%2F..%2Fother%3Fx%3D1/files/index.html",
|
|
89
|
+
expect.objectContaining({ method: "PUT", body: "A_AFTER" }),
|
|
90
|
+
);
|
|
91
|
+
expect(fetchMock).toHaveBeenCalledWith("/api/projects/project-b%23fragment/files/index.html");
|
|
92
|
+
expect(fetchMock).toHaveBeenCalledWith(
|
|
93
|
+
"/api/projects/project-b%23fragment/files/index.html?optional=1",
|
|
94
|
+
);
|
|
95
|
+
|
|
96
|
+
await act(async () => root.unmount());
|
|
97
|
+
});
|
|
98
|
+
});
|
|
@@ -66,38 +66,48 @@ export function useFileManager({
|
|
|
66
66
|
|
|
67
67
|
// ── Core file I/O ──
|
|
68
68
|
|
|
69
|
-
const readProjectFile = useCallback(
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
69
|
+
const readProjectFile = useCallback(
|
|
70
|
+
async (path: string): Promise<string> => {
|
|
71
|
+
if (!projectId) throw new Error("No active project");
|
|
72
|
+
const response = await fetch(
|
|
73
|
+
`/api/projects/${encodeURIComponent(projectId)}/files/${encodeURIComponent(path)}`,
|
|
74
|
+
);
|
|
75
|
+
if (!response.ok) throw new Error(`Failed to read ${path}`);
|
|
76
|
+
const data = (await response.json()) as { content?: string };
|
|
77
|
+
if (typeof data.content !== "string") throw new Error(`Missing file contents for ${path}`);
|
|
78
|
+
return data.content;
|
|
79
|
+
},
|
|
80
|
+
[projectId],
|
|
81
|
+
);
|
|
78
82
|
|
|
79
|
-
const writeProjectFile = useCallback(
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
83
|
+
const writeProjectFile = useCallback(
|
|
84
|
+
async (path: string, content: string): Promise<void> => {
|
|
85
|
+
if (!projectId) throw new Error("No active project");
|
|
86
|
+
const writeProjectId = projectId;
|
|
87
|
+
await retryStudioSave(async () => {
|
|
88
|
+
let response: Response;
|
|
89
|
+
try {
|
|
90
|
+
response = await fetch(
|
|
91
|
+
`/api/projects/${encodeURIComponent(writeProjectId)}/files/${encodeURIComponent(path)}`,
|
|
92
|
+
{
|
|
93
|
+
method: "PUT",
|
|
94
|
+
headers: { "Content-Type": "text/plain" },
|
|
95
|
+
body: content,
|
|
96
|
+
},
|
|
97
|
+
);
|
|
98
|
+
} catch (error) {
|
|
99
|
+
throw new StudioSaveNetworkError(`Failed to save ${path}: network error`, {
|
|
100
|
+
cause: error,
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
if (!response.ok) throw await createStudioSaveHttpError(response, `Failed to save ${path}`);
|
|
104
|
+
});
|
|
105
|
+
if (projectIdRef.current === writeProjectId && editingPathRef.current === path) {
|
|
106
|
+
setEditingFile({ path, content });
|
|
94
107
|
}
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
setEditingFile({ path, content });
|
|
99
|
-
}
|
|
100
|
-
}, []);
|
|
108
|
+
},
|
|
109
|
+
[projectId],
|
|
110
|
+
);
|
|
101
111
|
|
|
102
112
|
const updateEditingFileContent = useCallback((path: string, content: string) => {
|
|
103
113
|
if (editingPathRef.current === path) {
|
|
@@ -105,16 +115,18 @@ export function useFileManager({
|
|
|
105
115
|
}
|
|
106
116
|
}, []);
|
|
107
117
|
|
|
108
|
-
const readOptionalProjectFile = useCallback(
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
+
const readOptionalProjectFile = useCallback(
|
|
119
|
+
async (path: string): Promise<string> => {
|
|
120
|
+
if (!projectId) throw new Error("No active project");
|
|
121
|
+
const response = await fetch(
|
|
122
|
+
`/api/projects/${encodeURIComponent(projectId)}/files/${encodeURIComponent(path)}?optional=1`,
|
|
123
|
+
);
|
|
124
|
+
if (!response.ok) throw new Error(`Failed to read ${path}`);
|
|
125
|
+
const data = (await response.json()) as { content?: string };
|
|
126
|
+
return typeof data.content === "string" ? data.content : "";
|
|
127
|
+
},
|
|
128
|
+
[projectId],
|
|
129
|
+
);
|
|
118
130
|
|
|
119
131
|
// ── Editor save (debounced content change) ──
|
|
120
132
|
|
|
@@ -146,7 +158,7 @@ export function useFileManager({
|
|
|
146
158
|
setEditingFile({ path, content: null });
|
|
147
159
|
return;
|
|
148
160
|
}
|
|
149
|
-
fetch(`/api/projects/${pid}/files/${encodeURIComponent(path)}`)
|
|
161
|
+
fetch(`/api/projects/${encodeURIComponent(pid)}/files/${encodeURIComponent(path)}`)
|
|
150
162
|
.then((r) => {
|
|
151
163
|
if (!r.ok) throw new Error(`Failed to load ${path} (${r.status})`);
|
|
152
164
|
return r.json();
|
|
@@ -179,7 +191,7 @@ export function useFileManager({
|
|
|
179
191
|
const requestId = ++revealRequestIdRef.current;
|
|
180
192
|
const controller = new AbortController();
|
|
181
193
|
revealAbortRef.current = controller;
|
|
182
|
-
fetch(`/api/projects/${pid}/files/${encodeURIComponent(sourceFile)}`, {
|
|
194
|
+
fetch(`/api/projects/${encodeURIComponent(pid)}/files/${encodeURIComponent(sourceFile)}`, {
|
|
183
195
|
signal: controller.signal,
|
|
184
196
|
})
|
|
185
197
|
.then((r) => r.json())
|
|
@@ -211,7 +223,7 @@ export function useFileManager({
|
|
|
211
223
|
|
|
212
224
|
const qs = dir ? `?dir=${encodeURIComponent(dir)}` : "";
|
|
213
225
|
try {
|
|
214
|
-
const res = await fetch(`/api/projects/${pid}/upload${qs}`, {
|
|
226
|
+
const res = await fetch(`/api/projects/${encodeURIComponent(pid)}/upload${qs}`, {
|
|
215
227
|
method: "POST",
|
|
216
228
|
body: formData,
|
|
217
229
|
});
|
|
@@ -251,11 +263,14 @@ export function useFileManager({
|
|
|
251
263
|
content =
|
|
252
264
|
'<!DOCTYPE html>\n<html>\n<head>\n <meta charset="UTF-8">\n</head>\n<body>\n\n</body>\n</html>\n';
|
|
253
265
|
}
|
|
254
|
-
const res = await fetch(
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
266
|
+
const res = await fetch(
|
|
267
|
+
`/api/projects/${encodeURIComponent(pid)}/files/${encodeURIComponent(path)}`,
|
|
268
|
+
{
|
|
269
|
+
method: "POST",
|
|
270
|
+
headers: { "Content-Type": "text/plain" },
|
|
271
|
+
body: content,
|
|
272
|
+
},
|
|
273
|
+
);
|
|
259
274
|
if (res.ok) {
|
|
260
275
|
await refreshFileTree();
|
|
261
276
|
handleFileSelect(path);
|
|
@@ -273,7 +288,7 @@ export function useFileManager({
|
|
|
273
288
|
const pid = projectIdRef.current;
|
|
274
289
|
if (!pid) return;
|
|
275
290
|
const res = await fetch(
|
|
276
|
-
`/api/projects/${pid}/files/${encodeURIComponent(path + "/.gitkeep")}`,
|
|
291
|
+
`/api/projects/${encodeURIComponent(pid)}/files/${encodeURIComponent(path + "/.gitkeep")}`,
|
|
277
292
|
{
|
|
278
293
|
method: "POST",
|
|
279
294
|
headers: { "Content-Type": "text/plain" },
|
|
@@ -295,9 +310,12 @@ export function useFileManager({
|
|
|
295
310
|
async (path: string) => {
|
|
296
311
|
const pid = projectIdRef.current;
|
|
297
312
|
if (!pid) return;
|
|
298
|
-
const res = await fetch(
|
|
299
|
-
|
|
300
|
-
|
|
313
|
+
const res = await fetch(
|
|
314
|
+
`/api/projects/${encodeURIComponent(pid)}/files/${encodeURIComponent(path)}`,
|
|
315
|
+
{
|
|
316
|
+
method: "DELETE",
|
|
317
|
+
},
|
|
318
|
+
);
|
|
301
319
|
if (res.ok) {
|
|
302
320
|
if (editingPathRef.current === path) setEditingFile(null);
|
|
303
321
|
await refreshFileTree();
|
|
@@ -314,11 +332,14 @@ export function useFileManager({
|
|
|
314
332
|
async (oldPath: string, newPath: string) => {
|
|
315
333
|
const pid = projectIdRef.current;
|
|
316
334
|
if (!pid) return;
|
|
317
|
-
const res = await fetch(
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
335
|
+
const res = await fetch(
|
|
336
|
+
`/api/projects/${encodeURIComponent(pid)}/files/${encodeURIComponent(oldPath)}`,
|
|
337
|
+
{
|
|
338
|
+
method: "PATCH",
|
|
339
|
+
headers: { "Content-Type": "application/json" },
|
|
340
|
+
body: JSON.stringify({ newPath }),
|
|
341
|
+
},
|
|
342
|
+
);
|
|
322
343
|
if (res.ok) {
|
|
323
344
|
if (editingPathRef.current === oldPath) {
|
|
324
345
|
handleFileSelect(newPath);
|
|
@@ -338,7 +359,7 @@ export function useFileManager({
|
|
|
338
359
|
async (path: string) => {
|
|
339
360
|
const pid = projectIdRef.current;
|
|
340
361
|
if (!pid) return;
|
|
341
|
-
const res = await fetch(`/api/projects/${pid}/duplicate-file`, {
|
|
362
|
+
const res = await fetch(`/api/projects/${encodeURIComponent(pid)}/duplicate-file`, {
|
|
342
363
|
method: "POST",
|
|
343
364
|
headers: { "Content-Type": "application/json" },
|
|
344
365
|
body: JSON.stringify({ path }),
|
|
@@ -367,17 +388,18 @@ export function useFileManager({
|
|
|
367
388
|
|
|
368
389
|
const handleImportFonts = useCallback(
|
|
369
390
|
async (files: FileList | File[]): Promise<ImportedFontAsset[]> => {
|
|
391
|
+
const pid = projectIdRef.current;
|
|
392
|
+
if (!pid) return [];
|
|
370
393
|
const uploaded = await uploadProjectFiles(
|
|
371
394
|
Array.from(files).filter((file) => FONT_EXT.test(file.name)),
|
|
372
395
|
"assets/fonts",
|
|
373
396
|
);
|
|
374
|
-
const pid = projectIdRef.current;
|
|
375
397
|
const imported = uploaded
|
|
376
398
|
.filter((asset) => FONT_EXT.test(asset))
|
|
377
399
|
.map((asset) => ({
|
|
378
400
|
family: fontFamilyFromAssetPath(asset),
|
|
379
401
|
path: asset,
|
|
380
|
-
url: `/api/projects/${pid}/preview/${asset}`,
|
|
402
|
+
url: `/api/projects/${encodeURIComponent(pid)}/preview/${asset}`,
|
|
381
403
|
}));
|
|
382
404
|
importedFontAssetsRef.current = [
|
|
383
405
|
...imported,
|
|
@@ -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,
|