@hyperframes/studio 0.7.57 → 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--Z69cEkE.js → hyperframes-player-Bm07FLkl.js} +1 -1
- package/dist/assets/{index-KsfE1bUu.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-Bf1x1y8H.js → index-FvzmPhfG.js} +1 -1
- package/dist/{chunk-5QSIMBEJ.js → chunk-OBAG3GWK.js} +33 -21
- package/dist/chunk-OBAG3GWK.js.map +1 -0
- package/dist/{domEditingLayers-6LQGKPOI.js → domEditingLayers-2CKWGEHS.js} +2 -2
- package/dist/index.d.ts +27 -7
- package/dist/index.html +2 -2
- package/dist/index.js +18029 -11563
- 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 +6 -6
- package/src/components/DesignPanelPromoteProvider.tsx +15 -2
- package/src/components/StudioFeedbackBar.tsx +2 -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/CanvasContextMenu.test.tsx +27 -14
- package/src/components/editor/CanvasContextMenu.tsx +71 -6
- package/src/components/editor/DomEditOverlay.tsx +5 -2
- 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/LayersPanel.tsx +110 -99
- 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/canvasContextMenuZOrder.test.ts +57 -0
- package/src/components/editor/canvasContextMenuZOrder.ts +40 -1
- package/src/components/editor/domEditingDom.ts +5 -6
- package/src/components/editor/domEditingElement.ts +15 -32
- package/src/components/editor/gsapAnimationCallbacks.ts +27 -0
- package/src/components/editor/gsapLivePreview.ts +34 -0
- package/src/components/editor/layersPanelSort.ts +80 -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/editor/useLayerRevealOverride.test.ts +169 -0
- package/src/components/editor/useLayerRevealOverride.ts +389 -0
- package/src/components/nle/PreviewOverlays.tsx +27 -2
- package/src/components/nle/TimelinePane.test.ts +3 -0
- package/src/components/nle/TimelinePane.tsx +11 -1
- package/src/components/nle/useCanvasZOrderTimelineMirror.test.tsx +440 -0
- package/src/components/nle/useCanvasZOrderTimelineMirror.ts +191 -0
- package/src/components/nle/useTimelineEditCallbacks.ts +1 -0
- package/src/components/nle/zLaneGesture.test.ts +176 -0
- package/src/components/nle/zLaneGesture.ts +77 -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/domEditCommitTypes.ts +17 -1
- package/src/hooks/elementLifecycleOpsTestUtils.ts +25 -0
- package/src/hooks/fetchStubTestUtils.ts +16 -0
- package/src/hooks/gsapScriptCommitTypes.ts +6 -4
- package/src/hooks/timelineEditingHelpers.test.ts +92 -1
- package/src/hooks/timelineEditingHelpers.ts +32 -15
- package/src/hooks/timelineMoveAdapter.ts +2 -1
- package/src/hooks/timelineTimingSync.test.ts +703 -33
- package/src/hooks/timelineTimingSync.ts +282 -97
- package/src/hooks/timelineTrackVisibility.test.ts +55 -1
- package/src/hooks/timelineTrackVisibility.ts +31 -13
- package/src/hooks/useAppHotkeys.ts +9 -0
- package/src/hooks/useClipboard.ts +19 -17
- package/src/hooks/useDomEditAttributeCommits.ts +123 -1
- package/src/hooks/useDomEditCommits.test.tsx +316 -38
- package/src/hooks/useDomEditCommits.ts +81 -162
- package/src/hooks/useDomEditCommitsHelpers.ts +158 -0
- package/src/hooks/useDomEditSession.test.tsx +3 -1
- package/src/hooks/useDomEditSession.ts +18 -6
- package/src/hooks/useDomEditTextCommits.ts +14 -9
- package/src/hooks/useDomEditWiring.ts +10 -9
- package/src/hooks/useElementLifecycleOps.test.tsx +426 -60
- package/src/hooks/useElementLifecycleOps.ts +182 -84
- 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/usePreviewPersistence.ts +2 -2
- 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 +306 -155
- package/src/hooks/useTimelineEditing.ts +111 -101
- package/src/hooks/useTimelineEditingTypes.ts +4 -1
- package/src/hooks/useTimelineGroupEditing.ts +51 -5
- package/src/hooks/useVariablesPersist.ts +9 -7
- package/src/icons/SystemIcons.tsx +2 -0
- package/src/player/components/Timeline.test.ts +31 -21
- package/src/player/components/Timeline.tsx +62 -91
- package/src/player/components/TimelineCanvas.tsx +36 -5
- package/src/player/components/TimelineLanes.tsx +32 -10
- package/src/player/components/TimelineOverlays.tsx +36 -0
- package/src/player/components/TimelineRuler.tsx +18 -5
- package/src/player/components/TrackGapContextMenu.tsx +116 -0
- package/src/player/components/timelineCallbacks.ts +4 -1
- package/src/player/components/timelineClipDragCommit.test.ts +279 -3
- package/src/player/components/timelineClipDragCommit.ts +173 -71
- package/src/player/components/timelineGapCommit.test.ts +163 -0
- package/src/player/components/timelineGapCommit.ts +103 -0
- package/src/player/components/timelineGaps.test.ts +221 -0
- package/src/player/components/timelineGaps.ts +182 -0
- package/src/player/components/timelineLayout.test.ts +2 -1
- package/src/player/components/timelineLayout.ts +22 -6
- package/src/player/components/timelineMarquee.test.ts +20 -10
- package/src/player/components/timelineMarquee.ts +9 -2
- package/src/player/components/timelineStackingSync.test.ts +42 -1
- package/src/player/components/timelineStackingSync.ts +31 -16
- package/src/player/components/timelineZMirror.test.ts +667 -0
- package/src/player/components/timelineZMirror.ts +393 -0
- package/src/player/components/timelineZoom.test.ts +2 -2
- package/src/player/components/timelineZoom.ts +7 -3
- package/src/player/components/useTimelineClipDrag.ts +1 -1
- package/src/player/components/useTimelineGapHighlights.test.ts +119 -0
- package/src/player/components/useTimelineGapHighlights.ts +110 -0
- package/src/player/components/useTimelinePlayhead.ts +4 -3
- package/src/player/components/useTimelineRangeSelection.ts +6 -4
- package/src/player/components/useTimelineScrollViewport.ts +73 -0
- package/src/player/components/useTimelineStackingSync.test.tsx +0 -1
- package/src/player/components/useTimelineStackingSync.ts +5 -3
- package/src/player/components/useTimelineTrackDerivations.ts +38 -0
- package/src/player/components/useTrackGapMenu.ts +155 -0
- package/src/player/hooks/useExpandedTimelineElements.test.ts +67 -4
- package/src/player/hooks/useExpandedTimelineElements.ts +12 -1
- package/src/player/hooks/useTimelineSyncCallbacks.ts +2 -0
- package/src/player/lib/layerOrdering.test.ts +1 -15
- package/src/player/lib/layerOrdering.ts +5 -28
- package/src/player/lib/timelineDOM.test.ts +72 -0
- package/src/player/lib/timelineDOM.ts +3 -0
- package/src/player/lib/timelineElementHelpers.ts +35 -11
- package/src/player/store/playerStore.ts +12 -1
- package/src/styles/studio.css +22 -0
- package/src/styles/tailwind-preset.shared.js +7 -0
- package/src/telemetry/events.test.ts +10 -0
- package/src/telemetry/events.ts +1 -0
- package/src/utils/designInputTracking.test.ts +97 -0
- package/src/utils/designInputTracking.ts +80 -0
- package/src/utils/domEditSaveQueue.ts +4 -3
- package/src/utils/editHistory.test.ts +82 -0
- package/src/utils/editHistory.ts +14 -2
- package/src/utils/gsapSoftReload.test.ts +54 -115
- package/src/utils/gsapSoftReload.ts +42 -152
- package/src/utils/gsapUndoRestore.test.ts +269 -0
- package/src/utils/gsapUndoRestore.ts +269 -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/sourceScopedSelectorIndex.ts +29 -0
- 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-DfmYkU44.js +0 -423
- package/dist/assets/index-_pqzyxB1.css +0 -1
- package/dist/chunk-5QSIMBEJ.js.map +0 -1
- /package/dist/{domEditingLayers-6LQGKPOI.js.map → domEditingLayers-2CKWGEHS.js.map} +0 -0
|
@@ -11,11 +11,24 @@ import {
|
|
|
11
11
|
readHfId,
|
|
12
12
|
type DomEditSelection,
|
|
13
13
|
} from "../components/editor/domEditing";
|
|
14
|
+
import { LAYER_REVEAL_PRIOR_POSITION_ATTR } from "../player/lib/timelineElementHelpers";
|
|
15
|
+
import {
|
|
16
|
+
beginLayerRevealCommit,
|
|
17
|
+
beginLayerZPersist,
|
|
18
|
+
completeLayerRevealCommit,
|
|
19
|
+
rollbackLayerRevealCommit,
|
|
20
|
+
type LayerRevealCommitOwnership,
|
|
21
|
+
} from "../components/editor/useLayerRevealOverride";
|
|
14
22
|
import type { CommitDomEditPatchBatches, DomEditPatchBatch } from "./domEditCommitTypes";
|
|
23
|
+
import { cutoverCommittedOrThrow, type CutoverResult } from "../utils/sdkCutover";
|
|
15
24
|
|
|
16
25
|
interface UseElementLifecycleOpsParams extends DomEditCommitBaseParams {
|
|
17
|
-
/** Route delete through SDK when session resolves the hf-id
|
|
18
|
-
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>;
|
|
19
32
|
/** Resolver-shadow tripwire for the reordered targets (telemetry-only, decoupled from cutover). */
|
|
20
33
|
onReorderShadow?: (targets: string[]) => void;
|
|
21
34
|
/** Resync the SDK session after a server-fallback delete. */
|
|
@@ -25,6 +38,36 @@ interface UseElementLifecycleOpsParams extends DomEditCommitBaseParams {
|
|
|
25
38
|
onElementDeleted?: (selection: DomEditSelection) => void;
|
|
26
39
|
}
|
|
27
40
|
|
|
41
|
+
// One coalesce key per z-reorder gesture. A monotonic counter — NOT Date.now()
|
|
42
|
+
// / Math.random(), which the determinism rules forbid — matches the
|
|
43
|
+
// laneChangeGestureSeq precedent in timelineClipDragCommit.ts: the key only has
|
|
44
|
+
// to be unique per gesture and identical across the gesture's records.
|
|
45
|
+
let zReorderGestureSeq = 0;
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Undo coalesce key for ONE z-reorder gesture — unique per call. The key
|
|
49
|
+
* carries the action kind + element ids for debuggability, plus a gesture
|
|
50
|
+
* sequence so two SEPARATE user actions (even the same action on the same
|
|
51
|
+
* selection) never share a key. That uniqueness is what makes the unbounded
|
|
52
|
+
* per-gesture coalesce window (see handleDomZIndexReorderCommit) safe: the
|
|
53
|
+
* fold can only ever merge records of the SAME gesture.
|
|
54
|
+
*
|
|
55
|
+
* Exported as THE single implementation of the key: the canvas z-order wiring
|
|
56
|
+
* (PreviewOverlays) mints it once per gesture and passes the same instance to
|
|
57
|
+
* both the z persist and the timeline lane mirror (useCanvasZOrderTimelineMirror)
|
|
58
|
+
* so editHistory folds the z write and the track write into one undo entry —
|
|
59
|
+
* recomputing the key per record would silently split the undo.
|
|
60
|
+
*/
|
|
61
|
+
export function zReorderCoalesceKey(
|
|
62
|
+
entries: ReadonlyArray<{ element: HTMLElement; id?: string; selector?: string }>,
|
|
63
|
+
actionKind?: string,
|
|
64
|
+
): string {
|
|
65
|
+
const ids = entries
|
|
66
|
+
.map((e) => e.id ?? e.selector ?? e.element.getAttribute("data-hf-id") ?? "el")
|
|
67
|
+
.join(":");
|
|
68
|
+
return `z-reorder:${actionKind ?? "reorder"}:${ids}:g${zReorderGestureSeq++}`;
|
|
69
|
+
}
|
|
70
|
+
|
|
28
71
|
export function useElementLifecycleOps({
|
|
29
72
|
activeCompPath,
|
|
30
73
|
showToast,
|
|
@@ -59,7 +102,7 @@ export function useElementLifecycleOps({
|
|
|
59
102
|
|
|
60
103
|
if (onTrySdkDelete && selection.hfId) {
|
|
61
104
|
const handled = await onTrySdkDelete(selection.hfId, originalContent, targetPath);
|
|
62
|
-
if (handled) {
|
|
105
|
+
if (cutoverCommittedOrThrow(handled)) {
|
|
63
106
|
clearDomSelection();
|
|
64
107
|
usePlayerStore.getState().setSelectedElementId(null);
|
|
65
108
|
showToast(`Deleted ${label}. Use Undo to restore it.`, "info");
|
|
@@ -130,11 +173,11 @@ export function useElementLifecycleOps({
|
|
|
130
173
|
],
|
|
131
174
|
);
|
|
132
175
|
|
|
133
|
-
//
|
|
134
|
-
//
|
|
176
|
+
// Z-index reorder folds patches by source file, then sends one aggregate cross-file
|
|
177
|
+
// patch-element-batches request. The server refuses the whole gesture on any unmatched
|
|
178
|
+
// target and rolls back earlier file writes if a later write fails.
|
|
135
179
|
// No SDK reorder/reparent op exists; DOM sibling order stays server-authoritative if ever needed.
|
|
136
180
|
const handleDomZIndexReorderCommit = useCallback(
|
|
137
|
-
// fallow-ignore-next-line complexity
|
|
138
181
|
(
|
|
139
182
|
entries: Array<{
|
|
140
183
|
element: HTMLElement;
|
|
@@ -149,88 +192,143 @@ export function useElementLifecycleOps({
|
|
|
149
192
|
actionKind?: string,
|
|
150
193
|
) => {
|
|
151
194
|
if (entries.length === 0) return Promise.resolve();
|
|
152
|
-
//
|
|
153
|
-
//
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
)
|
|
157
|
-
|
|
158
|
-
// same element set (e.g. "bring-forward" then "send-backward" within the
|
|
159
|
-
// coalesce window) never merge into one undo step. Callers that share a
|
|
160
|
-
// gesture (lane moves) pass an explicit gestureCoalesceKey instead.
|
|
161
|
-
const coalesceKey =
|
|
162
|
-
gestureCoalesceKey ??
|
|
163
|
-
`z-reorder:${actionKind ?? "reorder"}:${entries.map((e) => e.id ?? e.selector ?? e.element.getAttribute("data-hf-id") ?? "el").join(":")}`;
|
|
164
|
-
const patchesBySourceFile = new Map<string, DomEditPatchBatch["patches"]>();
|
|
165
|
-
const rollbacks: Array<() => void> = [];
|
|
166
|
-
for (const entry of entries) {
|
|
167
|
-
const priorZIndex = entry.element.style.zIndex;
|
|
168
|
-
const priorPosition = entry.element.style.position;
|
|
169
|
-
const priorStoreEntry = entry.key
|
|
170
|
-
? usePlayerStore.getState().elements.find((el) => (el.key ?? el.id) === entry.key)
|
|
171
|
-
: undefined;
|
|
172
|
-
let positionChanged = false;
|
|
173
|
-
entry.element.style.zIndex = String(entry.zIndex);
|
|
174
|
-
const patches: Array<{ type: "inline-style"; property: string; value: string }> = [
|
|
175
|
-
{ type: "inline-style", property: "z-index", value: String(entry.zIndex) },
|
|
176
|
-
];
|
|
195
|
+
// One async owner must bracket reveal tokens, optimistic DOM/store state,
|
|
196
|
+
// atomic persistence, rollback, and the final persistence-count release.
|
|
197
|
+
// Splitting those phases would make transaction ownership less explicit.
|
|
198
|
+
// fallow-ignore-next-line complexity
|
|
199
|
+
return (async () => {
|
|
200
|
+
const releaseZPersists = entries.map((entry) => beginLayerZPersist(entry.element));
|
|
177
201
|
try {
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
202
|
+
// Resolver shadow (telemetry-only, decoupled from cutover): record whether
|
|
203
|
+
// the SDK resolves each reordered element — the reorderElements op's targets.
|
|
204
|
+
onReorderShadow?.(
|
|
205
|
+
entries.map((e) => readHfId(e.element)).filter((id): id is string => id != null),
|
|
206
|
+
);
|
|
207
|
+
// The default key carries the action kind so two DIFFERENT actions on the
|
|
208
|
+
// same element set (e.g. "bring-forward" then "send-backward" within the
|
|
209
|
+
// coalesce window) never merge into one undo step. Callers that share a
|
|
210
|
+
// gesture (lane moves) pass an explicit gestureCoalesceKey instead.
|
|
211
|
+
const coalesceKey = gestureCoalesceKey ?? zReorderCoalesceKey(entries, actionKind);
|
|
212
|
+
const patchesBySourceFile = new Map<string, DomEditPatchBatch["patches"]>();
|
|
213
|
+
const rollbacks: Array<() => void> = [];
|
|
214
|
+
const revealCommits: Array<{
|
|
215
|
+
element: HTMLElement;
|
|
216
|
+
ownership: LayerRevealCommitOwnership;
|
|
217
|
+
}> = [];
|
|
218
|
+
for (const entry of entries) {
|
|
219
|
+
const priorZIndex = entry.element.style.zIndex;
|
|
220
|
+
const priorPosition = entry.element.style.position;
|
|
221
|
+
const priorStoreEntry = entry.key
|
|
222
|
+
? usePlayerStore.getState().elements.find((el) => (el.key ?? el.id) === entry.key)
|
|
223
|
+
: undefined;
|
|
224
|
+
let positionChanged = false;
|
|
225
|
+
// An active Layers-panel reveal lift on this element is consumed by
|
|
226
|
+
// this commit: the new z is the truth. Read the parked TRUE position
|
|
227
|
+
// for the static check below (the lift set a temporary
|
|
228
|
+
// position:relative that would otherwise mask the need to persist
|
|
229
|
+
// one), then drop the lift attributes so z readers stop reporting the
|
|
230
|
+
// stale prior (see useLayerRevealOverride / readLayerRevealPriorZ).
|
|
231
|
+
const liftPriorPosition = entry.element.getAttribute(LAYER_REVEAL_PRIOR_POSITION_ATTR);
|
|
232
|
+
const revealOwnership = beginLayerRevealCommit(entry.element);
|
|
233
|
+
if (revealOwnership) {
|
|
234
|
+
revealCommits.push({ element: entry.element, ownership: revealOwnership });
|
|
235
|
+
}
|
|
236
|
+
entry.element.style.zIndex = String(entry.zIndex);
|
|
237
|
+
const patches: Array<{ type: "inline-style"; property: string; value: string }> = [
|
|
238
|
+
{ type: "inline-style", property: "z-index", value: String(entry.zIndex) },
|
|
239
|
+
];
|
|
240
|
+
try {
|
|
241
|
+
const win = entry.element.ownerDocument?.defaultView;
|
|
242
|
+
const effectivePosition =
|
|
243
|
+
liftPriorPosition ??
|
|
244
|
+
(win ? win.getComputedStyle(entry.element).position : undefined);
|
|
245
|
+
if (effectivePosition === "static") {
|
|
246
|
+
entry.element.style.position = "relative";
|
|
247
|
+
positionChanged = true;
|
|
248
|
+
patches.push({ type: "inline-style", property: "position", value: "relative" });
|
|
249
|
+
}
|
|
250
|
+
} catch {
|
|
251
|
+
/* cross-origin or detached — skip */
|
|
252
|
+
}
|
|
253
|
+
if (entry.key) {
|
|
254
|
+
usePlayerStore
|
|
255
|
+
.getState()
|
|
256
|
+
.updateElement(entry.key, { zIndex: entry.zIndex, hasExplicitZIndex: true });
|
|
257
|
+
}
|
|
258
|
+
rollbacks.push(() => {
|
|
259
|
+
if (revealOwnership) {
|
|
260
|
+
rollbackLayerRevealCommit(entry.element, revealOwnership);
|
|
261
|
+
} else {
|
|
262
|
+
entry.element.style.zIndex = priorZIndex;
|
|
263
|
+
if (positionChanged) entry.element.style.position = priorPosition;
|
|
264
|
+
}
|
|
265
|
+
if (entry.key && priorStoreEntry) {
|
|
266
|
+
usePlayerStore.getState().updateElement(entry.key, {
|
|
267
|
+
zIndex: priorStoreEntry.zIndex,
|
|
268
|
+
hasExplicitZIndex: priorStoreEntry.hasExplicitZIndex,
|
|
269
|
+
});
|
|
270
|
+
}
|
|
271
|
+
});
|
|
272
|
+
const filePatches = patchesBySourceFile.get(entry.sourceFile) ?? [];
|
|
273
|
+
filePatches.push({
|
|
274
|
+
target: buildDomEditPatchTarget({
|
|
275
|
+
id: entry.id,
|
|
276
|
+
hfId: readHfId(entry.element),
|
|
277
|
+
selector: entry.selector,
|
|
278
|
+
selectorIndex: entry.selectorIndex,
|
|
279
|
+
}),
|
|
280
|
+
operations: patches,
|
|
281
|
+
});
|
|
282
|
+
patchesBySourceFile.set(entry.sourceFile, filePatches);
|
|
183
283
|
}
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
284
|
+
const batches = [...patchesBySourceFile].map(([sourceFile, patches]) => ({
|
|
285
|
+
sourceFile,
|
|
286
|
+
patches,
|
|
287
|
+
}));
|
|
288
|
+
// Live z state changed with NO reload coming (skipReload below) — nudge
|
|
289
|
+
// DOM-derived views (Layers panel z-sort) to re-read the iframe.
|
|
290
|
+
usePlayerStore.getState().bumpZEditVersion();
|
|
291
|
+
const rollbackOptimisticState = () => {
|
|
292
|
+
for (const rollback of rollbacks) rollback();
|
|
293
|
+
usePlayerStore.getState().bumpZEditVersion();
|
|
294
|
+
};
|
|
295
|
+
// Resolves once every source-file batch is persisted so a same-file timing write
|
|
296
|
+
// can be ordered after it (see applyTimelineStackingReorder callers).
|
|
297
|
+
//
|
|
298
|
+
// skipReload: the live iframe DOM and the player store already hold the
|
|
299
|
+
// final z state (applied synchronously above), and the persisted patch is
|
|
300
|
+
// inline-style-only — a full iframe remount would only blink the preview.
|
|
301
|
+
// commitDomEditPatchBatches still falls back to reloading whenever the
|
|
302
|
+
// server reports an unmatched patch target (live DOM ≠ disk).
|
|
303
|
+
try {
|
|
304
|
+
const result = await commitDomEditPatchBatches(batches, {
|
|
305
|
+
label: "Reorder layers",
|
|
306
|
+
coalesceKey,
|
|
307
|
+
// Unbounded window: every key this commit records under is unique per
|
|
308
|
+
// gesture (zReorderCoalesceKey's gesture seq, or the lane drag's
|
|
309
|
+
// clip-lane-move:<seq>), so the fold can only merge records of the SAME
|
|
310
|
+
// gesture — and those records are separated by a server round-trip
|
|
311
|
+
// (move→z on a lane drag, z→lane-mirror on a canvas action), which
|
|
312
|
+
// under real network latency exceeds the 300ms default window.
|
|
313
|
+
coalesceMs: Number.POSITIVE_INFINITY,
|
|
314
|
+
skipReload: true,
|
|
199
315
|
});
|
|
316
|
+
if (!result.durable) {
|
|
317
|
+
rollbackOptimisticState();
|
|
318
|
+
return result;
|
|
319
|
+
}
|
|
320
|
+
for (const { element, ownership } of revealCommits) {
|
|
321
|
+
completeLayerRevealCommit(element, ownership);
|
|
322
|
+
}
|
|
323
|
+
return result;
|
|
324
|
+
} catch (error) {
|
|
325
|
+
rollbackOptimisticState();
|
|
326
|
+
throw error;
|
|
200
327
|
}
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
id: entry.id,
|
|
206
|
-
hfId: readHfId(entry.element),
|
|
207
|
-
selector: entry.selector,
|
|
208
|
-
selectorIndex: entry.selectorIndex,
|
|
209
|
-
}),
|
|
210
|
-
operations: patches,
|
|
211
|
-
});
|
|
212
|
-
patchesBySourceFile.set(entry.sourceFile, filePatches);
|
|
213
|
-
}
|
|
214
|
-
const batches = [...patchesBySourceFile].map(([sourceFile, patches]) => ({
|
|
215
|
-
sourceFile,
|
|
216
|
-
patches,
|
|
217
|
-
}));
|
|
218
|
-
// Resolves once every source-file batch is persisted so a same-file timing write
|
|
219
|
-
// can be ordered after it (see applyTimelineStackingReorder callers).
|
|
220
|
-
//
|
|
221
|
-
// skipReload: the live iframe DOM and the player store already hold the
|
|
222
|
-
// final z state (applied synchronously above), and the persisted patch is
|
|
223
|
-
// inline-style-only — a full iframe remount would only blink the preview.
|
|
224
|
-
// commitDomEditPatchBatches still falls back to reloading whenever the
|
|
225
|
-
// server reports an unmatched patch target (live DOM ≠ disk).
|
|
226
|
-
return commitDomEditPatchBatches(batches, {
|
|
227
|
-
label: "Reorder layers",
|
|
228
|
-
coalesceKey,
|
|
229
|
-
skipReload: true,
|
|
230
|
-
}).catch((error) => {
|
|
231
|
-
for (const rollback of rollbacks) rollback();
|
|
232
|
-
throw error;
|
|
233
|
-
});
|
|
328
|
+
} finally {
|
|
329
|
+
for (const release of releaseZPersists) release();
|
|
330
|
+
}
|
|
331
|
+
})();
|
|
234
332
|
},
|
|
235
333
|
[commitDomEditPatchBatches, onReorderShadow],
|
|
236
334
|
);
|
|
@@ -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,
|