@hyperframes/studio 0.7.55 → 0.7.57
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 +459 -0
- package/dist/assets/{index-CPetwHFV.js → index-Bf1x1y8H.js} +1 -1
- package/dist/assets/index-DfmYkU44.js +423 -0
- package/dist/assets/{index-BXaqaVKt.js → index-KsfE1bUu.js} +1 -1
- package/dist/assets/index-_pqzyxB1.css +1 -0
- package/dist/{chunk-SOTCF4DF.js → chunk-5QSIMBEJ.js} +2 -1
- package/dist/chunk-5QSIMBEJ.js.map +1 -0
- package/dist/{domEditingLayers-2ECJK24D.js → domEditingLayers-6LQGKPOI.js} +2 -2
- package/dist/index.d.ts +172 -144
- package/dist/index.html +2 -2
- package/dist/index.js +43613 -40346
- package/dist/index.js.map +1 -1
- package/package.json +7 -7
- package/src/App.tsx +106 -96
- package/src/components/EditorShell.tsx +253 -0
- package/src/components/StudioGlobalDragOverlay.tsx +1 -3
- package/src/components/StudioLeftSidebar.tsx +13 -3
- package/src/components/StudioRightPanel.tsx +8 -3
- package/src/components/TimelineToolbar.test.tsx +2 -2
- package/src/components/TimelineToolbar.tsx +179 -121
- package/src/components/editor/CanvasContextMenu.test.tsx +152 -1
- package/src/components/editor/CanvasContextMenu.tsx +33 -18
- package/src/components/editor/DomEditCropHandles.test.tsx +74 -3
- package/src/components/editor/DomEditCropHandles.tsx +64 -29
- package/src/components/editor/DomEditOverlay.test.ts +245 -279
- package/src/components/editor/DomEditOverlay.tsx +126 -125
- package/src/components/editor/DomEditOverlayHover.test.tsx +90 -0
- package/src/components/editor/DomEditRotateHandle.tsx +38 -18
- package/src/components/editor/DomEditSelectionChrome.test.tsx +80 -0
- package/src/components/editor/DomEditSelectionChrome.tsx +257 -0
- package/src/components/editor/LayersPanel.test.ts +66 -2
- package/src/components/editor/LayersPanel.tsx +53 -2
- package/src/components/editor/OffCanvasIndicators.test.tsx +34 -0
- package/src/components/editor/OffCanvasIndicators.tsx +49 -10
- package/src/components/editor/SnapGuideOverlay.tsx +28 -23
- package/src/components/editor/SnapToolbar.test.tsx +0 -1
- package/src/components/editor/anchoredResizeCommitFeedsOffset.test.ts +214 -0
- package/src/components/editor/anchoredResizeReleaseShift.test.ts +106 -10
- package/src/components/editor/canvasContextMenuZOrder.test.ts +186 -2
- package/src/components/editor/canvasContextMenuZOrder.ts +144 -22
- package/src/components/editor/domEditNudge.test.ts +80 -0
- package/src/components/editor/domEditNudge.ts +44 -0
- package/src/components/editor/domEditOverlayGeometry.test.ts +167 -1
- package/src/components/editor/domEditOverlayGeometry.ts +280 -20
- package/src/components/editor/domEditOverlayGestures.ts +44 -41
- package/src/components/editor/domEditOverlayStartGesture.ts +62 -2
- package/src/components/editor/domEditResizeLocal.test.ts +131 -0
- package/src/components/editor/domEditResizeLocal.ts +125 -0
- package/src/components/editor/domEditingDom.ts +1 -1
- package/src/components/editor/manualEditsDomPatches.test.ts +47 -0
- package/src/components/editor/manualOffsetDrag.ts +55 -12
- package/src/components/editor/offCanvasIndicatorGeometry.ts +31 -10
- package/src/components/editor/offCanvasIndicatorRefresh.test.tsx +25 -1
- package/src/components/editor/propertyPanelColorGradingSection.tsx +15 -21
- package/src/components/editor/resizeDraft.ts +108 -0
- package/src/components/editor/snapEngine.test.ts +12 -78
- package/src/components/editor/snapEngine.ts +13 -53
- package/src/components/editor/useCanvasContextMenuState.ts +92 -0
- package/src/components/editor/useDomEditNudge.test.tsx +228 -0
- package/src/components/editor/useDomEditNudge.ts +254 -0
- package/src/components/editor/useDomEditOverlayGestures.ts +115 -116
- package/src/components/editor/useDomEditOverlayRects.ts +9 -2
- package/src/components/editor/useZOrderCrossedFlash.tsx +68 -0
- package/src/components/nle/AssetPreviewOverlay.test.tsx +106 -0
- package/src/components/nle/AssetPreviewOverlay.tsx +171 -0
- package/src/components/nle/NLEContext.test.ts +144 -0
- package/src/components/nle/NLEContext.tsx +328 -0
- package/src/components/nle/NLEPreview.tsx +1 -1
- package/src/components/nle/PreviewOverlays.tsx +260 -0
- package/src/components/nle/PreviewPane.tsx +163 -0
- package/src/components/nle/TimelinePane.test.ts +60 -0
- package/src/components/nle/TimelinePane.tsx +293 -0
- package/src/components/nle/TimelineResizeDivider.tsx +8 -2
- package/src/components/nle/useCompositionStack.test.tsx +44 -0
- package/src/components/nle/useCompositionStack.ts +17 -9
- package/src/components/nle/useTimelineEditCallbacks.ts +217 -0
- package/src/components/sidebar/AssetCard.test.tsx +106 -0
- package/src/components/sidebar/AssetCard.tsx +343 -0
- package/src/components/sidebar/AssetContextMenu.tsx +15 -30
- package/src/components/sidebar/AssetsTab.test.ts +96 -0
- package/src/components/sidebar/AssetsTab.tsx +71 -206
- package/src/components/sidebar/AudioRow.tsx +56 -2
- package/src/components/sidebar/BlocksTab.tsx +8 -1
- package/src/components/sidebar/LeftSidebar.tsx +4 -1
- package/src/components/sidebar/assetHelpers.ts +29 -0
- package/src/contexts/DomEditContext.tsx +9 -0
- package/src/contexts/StudioContext.tsx +9 -8
- package/src/contexts/TimelineEditContext.tsx +3 -7
- package/src/hooks/deleteSelectedKeyframes.ts +35 -0
- package/src/hooks/domEditCommitTypes.ts +23 -1
- package/src/hooks/domSelectionTestHarness.ts +14 -0
- package/src/hooks/gestureTransaction.test.ts +311 -0
- package/src/hooks/gestureTransaction.ts +199 -0
- package/src/hooks/gsapDragCommit.test.ts +141 -41
- package/src/hooks/gsapDragCommit.ts +64 -74
- package/src/hooks/gsapDragStaticSetHelpers.ts +9 -21
- package/src/hooks/gsapResizeIntercept.test.ts +69 -0
- package/src/hooks/gsapResizeIntercept.ts +38 -10
- package/src/hooks/gsapRuntimeBridge.test.ts +49 -13
- package/src/hooks/gsapRuntimeBridge.ts +4 -4
- package/src/hooks/gsapScriptCommitTypes.ts +16 -5
- package/src/hooks/gsapShared.test.ts +19 -1
- package/src/hooks/gsapShared.ts +14 -0
- package/src/hooks/patchDocumentRootDuration.test.ts +71 -0
- package/src/hooks/timelineEditingGsap.ts +14 -0
- package/src/hooks/timelineEditingHelpers.test.ts +125 -1
- package/src/hooks/timelineEditingHelpers.ts +53 -292
- package/src/hooks/timelineMoveAdapter.test.ts +81 -0
- package/src/hooks/timelineMoveAdapter.ts +39 -0
- package/src/hooks/timelineTimingSync.test.ts +150 -0
- package/src/hooks/timelineTimingSync.ts +412 -0
- package/src/hooks/useAnimatedPropertyCommit.test.tsx +36 -8
- package/src/hooks/useAnimatedPropertyCommit.ts +31 -26
- package/src/hooks/useAppHotkeys.ts +16 -25
- package/src/hooks/useBlockHandlers.ts +55 -27
- package/src/hooks/useCaptionDetection.ts +2 -0
- package/src/hooks/useCompositionDimensions.ts +32 -5
- package/src/hooks/useContextMenuDismiss.ts +31 -7
- package/src/hooks/useDomEditCommits.test.tsx +245 -1
- package/src/hooks/useDomEditCommits.ts +162 -3
- package/src/hooks/useDomEditPositionPatchCommit.ts +2 -0
- package/src/hooks/useDomEditSession.ts +2 -2
- package/src/hooks/useDomEditTextCommits.test.tsx +135 -0
- package/src/hooks/useDomEditTextCommits.ts +8 -3
- package/src/hooks/useDomEditWiring.ts +7 -9
- package/src/hooks/useDomGeometryCommits.ts +17 -2
- package/src/hooks/useDomSelection.test.ts +2 -36
- package/src/hooks/useDomSelection.ts +81 -51
- package/src/hooks/useDomSelectionSelectionGuards.test.ts +276 -0
- package/src/hooks/useElementLifecycleOps.test.tsx +423 -0
- package/src/hooks/useElementLifecycleOps.ts +53 -71
- package/src/hooks/useElementPicker.ts +7 -8
- package/src/hooks/useEnableKeyframes.test.ts +142 -1
- package/src/hooks/useEnableKeyframes.ts +73 -29
- package/src/hooks/useGestureCommit.test.tsx +130 -0
- package/src/hooks/useGestureCommit.ts +32 -8
- package/src/hooks/useGroupCommits.ts +7 -28
- package/src/hooks/useGsapAwareEditing.test.tsx +239 -0
- package/src/hooks/useGsapAwareEditing.ts +115 -20
- package/src/hooks/useGsapKeyframeOps.test.tsx +59 -0
- package/src/hooks/useGsapKeyframeOps.ts +38 -10
- package/src/hooks/useGsapScriptCommits.test.tsx +40 -1
- package/src/hooks/useGsapScriptCommits.ts +134 -50
- package/src/hooks/useGsapSelectionHandlers.ts +49 -16
- package/src/hooks/useMusicBeatAnalysis.ts +72 -36
- package/src/hooks/usePersistentEditHistory.test.ts +33 -0
- package/src/hooks/usePersistentEditHistory.ts +92 -58
- package/src/hooks/usePreviewPersistence.ts +25 -7
- package/src/hooks/useRazorSplit.history.test.tsx +148 -18
- package/src/hooks/useRazorSplit.test.ts +269 -0
- package/src/hooks/useRazorSplit.testHelpers.ts +64 -0
- package/src/hooks/useRazorSplit.ts +107 -36
- package/src/hooks/useRenderClipContent.ts +24 -6
- package/src/hooks/useStudioContextValue.ts +15 -5
- package/src/hooks/useStudioUrlState.ts +2 -4
- package/src/hooks/useTimelineAssetDropOps.ts +175 -0
- package/src/hooks/useTimelineEditing.test.tsx +398 -241
- package/src/hooks/useTimelineEditing.ts +181 -265
- package/src/hooks/useTimelineEditingTypes.ts +6 -0
- package/src/hooks/useTimelineGroupEditing.ts +167 -132
- package/src/index.ts +1 -1
- package/src/player/components/ImageThumbnail.test.tsx +173 -0
- package/src/player/components/ImageThumbnail.tsx +160 -0
- package/src/player/components/PlayerControls.tsx +5 -5
- package/src/player/components/PlayheadIndicator.tsx +55 -5
- package/src/player/components/Timeline.test.ts +183 -23
- package/src/player/components/Timeline.tsx +194 -194
- package/src/player/components/TimelineCanvas.tsx +218 -523
- package/src/player/components/TimelineClip.tsx +4 -1
- package/src/player/components/TimelineClipDiamonds.tsx +9 -40
- package/src/player/components/TimelineEmptyState.tsx +3 -1
- package/src/player/components/TimelineLanes.tsx +487 -0
- package/src/player/components/TimelineOverlays.tsx +122 -0
- package/src/player/components/TimelineRuler.tsx +58 -48
- package/src/player/components/VideoThumbnail.test.tsx +152 -0
- package/src/player/components/VideoThumbnail.tsx +28 -7
- package/src/player/components/thumbnailUtils.test.ts +127 -0
- package/src/player/components/thumbnailUtils.ts +54 -0
- package/src/player/components/timelineCallbacks.ts +18 -18
- package/src/player/components/timelineClipChildren.tsx +39 -0
- package/src/player/components/timelineClipDragCommit.test.ts +1026 -0
- package/src/player/components/timelineClipDragCommit.ts +498 -0
- package/src/player/components/timelineClipDragPreview.test.ts +144 -0
- package/src/player/components/timelineClipDragPreview.ts +323 -38
- package/src/player/components/timelineClipDragTypes.ts +65 -0
- package/src/player/components/timelineCollision.test.ts +42 -2
- package/src/player/components/timelineDragDrop.ts +69 -60
- package/src/player/components/timelineEditCapabilities.ts +52 -0
- package/src/player/components/timelineEditing.test.ts +1 -1
- package/src/player/components/timelineEditing.ts +76 -59
- package/src/player/components/timelineGroupEditing.test.ts +126 -0
- package/src/player/components/timelineGroupEditing.ts +133 -0
- package/src/player/components/timelineGroupResizeCommit.ts +54 -0
- package/src/player/components/timelineLayout.test.ts +95 -23
- package/src/player/components/timelineLayout.ts +207 -75
- package/src/player/components/timelineMarquee.test.ts +197 -0
- package/src/player/components/timelineMarquee.ts +112 -0
- package/src/player/components/timelineOptimisticRevision.ts +42 -0
- package/src/player/components/timelineRevealScroll.test.ts +133 -0
- package/src/player/components/timelineRevealScroll.ts +91 -0
- package/src/player/components/timelineStackingSync.test.ts +60 -0
- package/src/player/components/timelineStackingSync.ts +27 -2
- package/src/player/components/timelineTheme.ts +6 -1
- package/src/player/components/timelineTrackPersistPipeline.test.ts +168 -0
- package/src/player/components/timelineZones.test.ts +122 -310
- package/src/player/components/timelineZones.ts +82 -135
- package/src/player/components/useResolvedTimelineEditCallbacks.ts +25 -3
- package/src/player/components/useTimelineClipDrag.resize.test.tsx +241 -0
- package/src/player/components/useTimelineClipDrag.ts +380 -386
- package/src/player/components/useTimelineEditPinning.ts +121 -0
- package/src/player/components/useTimelineGeometry.ts +129 -0
- package/src/player/components/useTimelinePlayhead.ts +4 -16
- package/src/player/components/useTimelineRangeSelection.ts +341 -50
- package/src/player/components/useTimelineRevealClip.ts +56 -0
- package/src/player/components/useTimelineStackingSync.test.tsx +77 -0
- package/src/player/components/useTimelineStackingSync.ts +94 -0
- package/src/player/hooks/useExpandedTimelineElements.ts +7 -3
- package/src/player/hooks/usePlaybackKeyboard.ts +5 -0
- package/src/player/hooks/useTimelinePlayer.seek.test.ts +55 -0
- package/src/player/hooks/useTimelinePlayer.test.ts +36 -3
- package/src/player/hooks/useTimelinePlayer.ts +52 -53
- package/src/player/hooks/useTimelinePlayerLoop.ts +85 -0
- package/src/player/hooks/useTimelineSyncCallbacks.ts +9 -1
- package/src/player/index.ts +5 -2
- package/src/player/lib/runtimeProtocol.test.ts +48 -0
- package/src/player/lib/runtimeProtocol.ts +65 -0
- package/src/player/lib/time.test.ts +19 -19
- package/src/player/lib/time.ts +6 -2
- package/src/player/lib/timelineDOM.test.ts +25 -98
- package/src/player/lib/timelineDOM.ts +21 -81
- package/src/player/lib/timelineElementHelpers.ts +73 -5
- package/src/player/lib/timelineIframeHelpers.ts +4 -4
- package/src/player/store/playerStore.test.ts +38 -13
- package/src/player/store/playerStore.ts +99 -3
- package/src/styles/studio.css +27 -0
- package/src/utils/assetPreviewDismiss.test.ts +27 -0
- package/src/utils/assetPreviewDismiss.ts +29 -0
- package/src/utils/assetPreviewStore.ts +33 -0
- package/src/utils/blockInstaller.ts +10 -2
- package/src/utils/editHistory.test.ts +35 -0
- package/src/utils/gsapSoftReload.test.ts +119 -1
- package/src/utils/gsapSoftReload.ts +153 -0
- package/src/utils/mediaTypes.ts +3 -2
- package/src/utils/resizeDebug.ts +55 -0
- package/src/utils/rootDuration.test.ts +90 -1
- package/src/utils/rootDuration.ts +76 -13
- package/src/utils/sdkCutover.ts +4 -0
- package/src/utils/studioFileHistory.ts +37 -7
- package/src/utils/studioHelpers.ts +62 -0
- package/src/utils/studioPreviewHelpers.test.ts +65 -8
- package/src/utils/studioPreviewHelpers.ts +10 -0
- package/src/utils/studioTelemetry.ts +4 -1
- package/src/utils/studioUrlState.test.ts +0 -1
- package/src/utils/timelineAssetDrop.test.ts +144 -52
- package/src/utils/timelineAssetDrop.ts +67 -29
- package/src/utils/timelineDiscovery.ts +0 -17
- package/dist/assets/hyperframes-player-BBrKzTGd.js +0 -459
- package/dist/assets/index-BRwkMj0w.js +0 -423
- package/dist/assets/index-Dq7FEg0K.css +0 -1
- package/dist/chunk-SOTCF4DF.js.map +0 -1
- package/src/components/StudioPreviewArea.tsx +0 -500
- package/src/components/nle/NLELayout.test.ts +0 -12
- package/src/components/nle/NLELayout.tsx +0 -591
- package/src/player/components/TimelineLayerGutter.tsx +0 -61
- package/src/player/components/TimelineSelectionOverlays.tsx +0 -55
- package/src/player/components/timelineMarqueeSelection.test.ts +0 -87
- package/src/player/components/timelineSnapTargets.test.ts +0 -144
- package/src/player/components/timelineSnapTargets.ts +0 -164
- package/src/player/components/useTimelineClipDrag.test.tsx +0 -506
- package/src/player/components/useTimelineClipGroupDrag.ts +0 -417
- package/src/player/components/useTimelineMarqueeSelection.test.tsx +0 -231
- package/src/player/components/useTimelineMarqueeSelection.ts +0 -276
- package/src/utils/timelineDiscovery.test.ts +0 -90
- /package/dist/{domEditingLayers-2ECJK24D.js.map → domEditingLayers-6LQGKPOI.js.map} +0 -0
|
@@ -0,0 +1,423 @@
|
|
|
1
|
+
// @vitest-environment happy-dom
|
|
2
|
+
|
|
3
|
+
import React, { act } from "react";
|
|
4
|
+
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
5
|
+
import { usePlayerStore } from "../player";
|
|
6
|
+
import type { DomEditPatchBatch } from "./domEditCommitTypes";
|
|
7
|
+
import { useElementLifecycleOps } from "./useElementLifecycleOps";
|
|
8
|
+
import { mountReactHarness } from "./domSelectionTestHarness";
|
|
9
|
+
|
|
10
|
+
(globalThis as unknown as { IS_REACT_ACT_ENVIRONMENT: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
|
|
11
|
+
|
|
12
|
+
afterEach(() => {
|
|
13
|
+
document.body.innerHTML = "";
|
|
14
|
+
usePlayerStore.getState().setElements([]);
|
|
15
|
+
vi.unstubAllGlobals();
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
interface BatchOptions {
|
|
19
|
+
label: string;
|
|
20
|
+
coalesceKey: string;
|
|
21
|
+
skipReload?: boolean;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
interface CapturedBatchCall {
|
|
25
|
+
batches: DomEditPatchBatch[];
|
|
26
|
+
options: BatchOptions;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
type ReorderCommit = (
|
|
30
|
+
entries: Array<{
|
|
31
|
+
element: HTMLElement;
|
|
32
|
+
zIndex: number;
|
|
33
|
+
id?: string;
|
|
34
|
+
selector?: string;
|
|
35
|
+
selectorIndex?: number;
|
|
36
|
+
sourceFile: string;
|
|
37
|
+
key?: string;
|
|
38
|
+
}>,
|
|
39
|
+
coalesceKeyOverride?: string,
|
|
40
|
+
actionKind?: string,
|
|
41
|
+
) => Promise<void>;
|
|
42
|
+
|
|
43
|
+
function renderReorderHook(
|
|
44
|
+
capturedCalls: CapturedBatchCall[],
|
|
45
|
+
onReady: (commit: ReorderCommit) => void,
|
|
46
|
+
) {
|
|
47
|
+
function Harness() {
|
|
48
|
+
const { handleDomZIndexReorderCommit } = useElementLifecycleOps({
|
|
49
|
+
activeCompPath: "index.html",
|
|
50
|
+
showToast: vi.fn(),
|
|
51
|
+
writeProjectFile: vi.fn(async () => {}),
|
|
52
|
+
domEditSaveTimestampRef: { current: 0 },
|
|
53
|
+
editHistory: { recordEdit: vi.fn(async () => {}) },
|
|
54
|
+
projectIdRef: { current: null },
|
|
55
|
+
reloadPreview: vi.fn(),
|
|
56
|
+
clearDomSelection: vi.fn(),
|
|
57
|
+
commitDomEditPatchBatches: async (batches, options) => {
|
|
58
|
+
capturedCalls.push({ batches, options });
|
|
59
|
+
},
|
|
60
|
+
});
|
|
61
|
+
onReady(handleDomZIndexReorderCommit);
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
return mountReactHarness(<Harness />);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** Append the element, mount the reorder hook, and run one commit through act. */
|
|
68
|
+
async function runReorderCommit(el: HTMLElement, entries: Parameters<ReorderCommit>[0]) {
|
|
69
|
+
document.body.appendChild(el);
|
|
70
|
+
|
|
71
|
+
const captured: CapturedBatchCall[] = [];
|
|
72
|
+
let commit: ReorderCommit | undefined;
|
|
73
|
+
const root = renderReorderHook(captured, (fn) => (commit = fn));
|
|
74
|
+
|
|
75
|
+
await act(async () => {
|
|
76
|
+
commit!(entries);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
return { captured, root };
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
describe("useElementLifecycleOps — z-index reorder payload", () => {
|
|
83
|
+
// Regression: an id-less canvas element (e.g. a caption `.sub` div, which
|
|
84
|
+
// carries only data-hf-id + class) once had its absent id coerced to `null`
|
|
85
|
+
// (`entry.id ?? null`). The DOM-patch guard rejects a null `body.target.id`,
|
|
86
|
+
// so "move to back" toasted "unsafe values" and nothing persisted. The target
|
|
87
|
+
// id must be `undefined` (dropped on the wire), letting hfId / selector match.
|
|
88
|
+
it("never sends a null target id for an id-less element", async () => {
|
|
89
|
+
const el = document.createElement("div");
|
|
90
|
+
el.className = "sub clip";
|
|
91
|
+
el.setAttribute("data-hf-id", "hf-card");
|
|
92
|
+
|
|
93
|
+
const { captured, root } = await runReorderCommit(el, [
|
|
94
|
+
{
|
|
95
|
+
element: el,
|
|
96
|
+
zIndex: 0,
|
|
97
|
+
// id intentionally absent — the id-less element case.
|
|
98
|
+
selector: ".sub.clip",
|
|
99
|
+
selectorIndex: 3,
|
|
100
|
+
sourceFile: "index.html",
|
|
101
|
+
},
|
|
102
|
+
]);
|
|
103
|
+
|
|
104
|
+
const target = captured[0]?.batches[0]?.patches[0]?.target;
|
|
105
|
+
expect(captured).toHaveLength(1);
|
|
106
|
+
expect(target?.id).toBeUndefined();
|
|
107
|
+
expect(target?.id).not.toBeNull();
|
|
108
|
+
// The element stays addressable via hfId (and selector) instead.
|
|
109
|
+
expect(target?.hfId).toBe("hf-card");
|
|
110
|
+
|
|
111
|
+
act(() => root.unmount());
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it("requests skipReload on every z-reorder persist (live DOM already final)", async () => {
|
|
115
|
+
// The commit applies the z-index (and any injected position) to the live
|
|
116
|
+
// iframe DOM and the store synchronously, so the persisted style-only patch
|
|
117
|
+
// adds nothing the preview doesn't already show — the batch commit is asked
|
|
118
|
+
// to skip the iframe remount. commitDomEditPatchBatches still falls back to
|
|
119
|
+
// reloading when the server can't confirm every patch target matched.
|
|
120
|
+
const el = document.createElement("div");
|
|
121
|
+
el.id = "clip-z";
|
|
122
|
+
|
|
123
|
+
const { captured, root } = await runReorderCommit(el, [
|
|
124
|
+
{ element: el, zIndex: 4, id: "clip-z", sourceFile: "index.html" },
|
|
125
|
+
]);
|
|
126
|
+
|
|
127
|
+
expect(captured).toHaveLength(1);
|
|
128
|
+
expect(captured[0]?.options.skipReload).toBe(true);
|
|
129
|
+
|
|
130
|
+
act(() => root.unmount());
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it("preserves a real id when the element has one", async () => {
|
|
134
|
+
const el = document.createElement("video");
|
|
135
|
+
el.id = "v-hero";
|
|
136
|
+
el.setAttribute("data-hf-id", "hf-ezl2");
|
|
137
|
+
|
|
138
|
+
const { captured, root } = await runReorderCommit(el, [
|
|
139
|
+
{ element: el, zIndex: 2, id: "v-hero", selector: "#v-hero", sourceFile: "index.html" },
|
|
140
|
+
]);
|
|
141
|
+
|
|
142
|
+
expect(captured).toHaveLength(1);
|
|
143
|
+
expect(captured[0]?.batches[0]?.patches[0]?.target.id).toBe("v-hero");
|
|
144
|
+
|
|
145
|
+
act(() => root.unmount());
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
it("threads the lane gesture key into z-index persistence", async () => {
|
|
149
|
+
const el = document.createElement("div");
|
|
150
|
+
el.id = "clip-a";
|
|
151
|
+
document.body.appendChild(el);
|
|
152
|
+
const captured: CapturedBatchCall[] = [];
|
|
153
|
+
let commit: ReorderCommit | undefined;
|
|
154
|
+
const root = renderReorderHook(captured, (fn) => (commit = fn));
|
|
155
|
+
|
|
156
|
+
await act(async () => {
|
|
157
|
+
await commit!(
|
|
158
|
+
[{ element: el, zIndex: 4, id: "clip-a", sourceFile: "index.html" }],
|
|
159
|
+
"clip-lane-move:7",
|
|
160
|
+
);
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
expect(captured).toHaveLength(1);
|
|
164
|
+
expect(captured[0]?.options.coalesceKey).toBe("clip-lane-move:7");
|
|
165
|
+
act(() => root.unmount());
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
it("creates one batch per source file in a multi-file reorder", async () => {
|
|
169
|
+
const elements = ["clip-a", "clip-b", "clip-c"].map((id) => {
|
|
170
|
+
const element = document.createElement("div");
|
|
171
|
+
element.id = id;
|
|
172
|
+
document.body.appendChild(element);
|
|
173
|
+
return element;
|
|
174
|
+
});
|
|
175
|
+
const captured: CapturedBatchCall[] = [];
|
|
176
|
+
let commit: ReorderCommit | undefined;
|
|
177
|
+
const root = renderReorderHook(captured, (fn) => (commit = fn));
|
|
178
|
+
|
|
179
|
+
await act(async () => {
|
|
180
|
+
await commit!(
|
|
181
|
+
elements.map((element, index) => ({
|
|
182
|
+
element,
|
|
183
|
+
zIndex: index + 1,
|
|
184
|
+
id: element.id,
|
|
185
|
+
sourceFile: index < 2 ? "index.html" : "compositions/scene.html",
|
|
186
|
+
})),
|
|
187
|
+
);
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
expect(captured).toHaveLength(1);
|
|
191
|
+
expect(
|
|
192
|
+
captured[0]?.batches.map(({ sourceFile, patches }) => [sourceFile, patches.length]),
|
|
193
|
+
).toEqual([
|
|
194
|
+
["index.html", 2],
|
|
195
|
+
["compositions/scene.html", 1],
|
|
196
|
+
]);
|
|
197
|
+
act(() => root.unmount());
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
it("keeps distinct actions in distinct default coalesce keys", async () => {
|
|
201
|
+
const el = document.createElement("div");
|
|
202
|
+
el.id = "clip-a";
|
|
203
|
+
document.body.appendChild(el);
|
|
204
|
+
const captured: CapturedBatchCall[] = [];
|
|
205
|
+
let commit: ReorderCommit | undefined;
|
|
206
|
+
const root = renderReorderHook(captured, (fn) => (commit = fn));
|
|
207
|
+
|
|
208
|
+
await act(async () => {
|
|
209
|
+
await commit!(
|
|
210
|
+
[{ element: el, zIndex: 1, id: "clip-a", sourceFile: "index.html" }],
|
|
211
|
+
undefined,
|
|
212
|
+
"bring-forward",
|
|
213
|
+
);
|
|
214
|
+
await commit!(
|
|
215
|
+
[{ element: el, zIndex: 0, id: "clip-a", sourceFile: "index.html" }],
|
|
216
|
+
undefined,
|
|
217
|
+
"send-backward",
|
|
218
|
+
);
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
// Same element set, different actions — the keys must differ so the two
|
|
222
|
+
// edits never coalesce into one undo step within the coalesce window.
|
|
223
|
+
expect(captured).toHaveLength(2);
|
|
224
|
+
expect(captured[0]?.options.coalesceKey).toBe("z-reorder:bring-forward:clip-a");
|
|
225
|
+
expect(captured[1]?.options.coalesceKey).toBe("z-reorder:send-backward:clip-a");
|
|
226
|
+
act(() => root.unmount());
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
it("updates the store zIndex synchronously for entries that carry a store key", async () => {
|
|
230
|
+
const el = document.createElement("div");
|
|
231
|
+
el.id = "clip-a";
|
|
232
|
+
document.body.appendChild(el);
|
|
233
|
+
usePlayerStore.getState().setElements([
|
|
234
|
+
{
|
|
235
|
+
id: "clip-a",
|
|
236
|
+
key: "index.html#clip-a",
|
|
237
|
+
tag: "div",
|
|
238
|
+
start: 0,
|
|
239
|
+
duration: 1,
|
|
240
|
+
track: 0,
|
|
241
|
+
zIndex: 0,
|
|
242
|
+
hasExplicitZIndex: false,
|
|
243
|
+
},
|
|
244
|
+
]);
|
|
245
|
+
|
|
246
|
+
let commit: ReorderCommit | undefined;
|
|
247
|
+
let resolveBatch: (() => void) | undefined;
|
|
248
|
+
function Harness() {
|
|
249
|
+
const { handleDomZIndexReorderCommit } = useElementLifecycleOps({
|
|
250
|
+
activeCompPath: "index.html",
|
|
251
|
+
showToast: vi.fn(),
|
|
252
|
+
writeProjectFile: vi.fn(async () => {}),
|
|
253
|
+
domEditSaveTimestampRef: { current: 0 },
|
|
254
|
+
editHistory: { recordEdit: vi.fn(async () => {}) },
|
|
255
|
+
projectIdRef: { current: null },
|
|
256
|
+
reloadPreview: vi.fn(),
|
|
257
|
+
clearDomSelection: vi.fn(),
|
|
258
|
+
// Persist stays pending so the assertion below can only be satisfied
|
|
259
|
+
// by the SYNCHRONOUS store update (the lane-sync path's requirement).
|
|
260
|
+
commitDomEditPatchBatches: () => new Promise((resolve) => (resolveBatch = resolve)),
|
|
261
|
+
});
|
|
262
|
+
commit = handleDomZIndexReorderCommit;
|
|
263
|
+
return null;
|
|
264
|
+
}
|
|
265
|
+
const root = mountReactHarness(<Harness />);
|
|
266
|
+
|
|
267
|
+
let pending: Promise<void> | undefined;
|
|
268
|
+
act(() => {
|
|
269
|
+
pending = commit!([
|
|
270
|
+
{
|
|
271
|
+
element: el,
|
|
272
|
+
zIndex: 5,
|
|
273
|
+
id: "clip-a",
|
|
274
|
+
sourceFile: "index.html",
|
|
275
|
+
key: "index.html#clip-a",
|
|
276
|
+
},
|
|
277
|
+
]);
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
expect(usePlayerStore.getState().elements[0]).toMatchObject({
|
|
281
|
+
zIndex: 5,
|
|
282
|
+
hasExplicitZIndex: true,
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
resolveBatch?.();
|
|
286
|
+
await act(async () => pending);
|
|
287
|
+
act(() => root.unmount());
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
// The canvas context-menu path: the menu no longer pre-applies styles, so the
|
|
291
|
+
// hook sees the PRISTINE element — prior styles are captured before any
|
|
292
|
+
// mutation and a failed persist restores them exactly (previously the menu's
|
|
293
|
+
// optimistic write made the "rollback" restore the already-mutated values,
|
|
294
|
+
// and the never-persisted position patch silently reverted on reload).
|
|
295
|
+
it("rolls back a static, inline-style-free element to pristine styles on failure", async () => {
|
|
296
|
+
const el = document.createElement("div");
|
|
297
|
+
el.id = "clip-a";
|
|
298
|
+
el.style.position = "static"; // happy-dom computes "" for unset position
|
|
299
|
+
document.body.appendChild(el);
|
|
300
|
+
const failure = new Error("persist failed");
|
|
301
|
+
|
|
302
|
+
let commit: ReorderCommit | undefined;
|
|
303
|
+
function Harness() {
|
|
304
|
+
const { handleDomZIndexReorderCommit } = useElementLifecycleOps({
|
|
305
|
+
activeCompPath: "index.html",
|
|
306
|
+
showToast: vi.fn(),
|
|
307
|
+
writeProjectFile: vi.fn(async () => {}),
|
|
308
|
+
domEditSaveTimestampRef: { current: 0 },
|
|
309
|
+
editHistory: { recordEdit: vi.fn(async () => {}) },
|
|
310
|
+
projectIdRef: { current: null },
|
|
311
|
+
reloadPreview: vi.fn(),
|
|
312
|
+
clearDomSelection: vi.fn(),
|
|
313
|
+
commitDomEditPatchBatches: vi.fn(async () => {
|
|
314
|
+
// The live styles were applied by the hook before persist ran.
|
|
315
|
+
expect(el.style.zIndex).toBe("2");
|
|
316
|
+
expect(el.style.position).toBe("relative");
|
|
317
|
+
throw failure;
|
|
318
|
+
}),
|
|
319
|
+
});
|
|
320
|
+
commit = handleDomZIndexReorderCommit;
|
|
321
|
+
return null;
|
|
322
|
+
}
|
|
323
|
+
const root = mountReactHarness(<Harness />);
|
|
324
|
+
|
|
325
|
+
let rejection: unknown;
|
|
326
|
+
await act(async () => {
|
|
327
|
+
try {
|
|
328
|
+
await commit!(
|
|
329
|
+
[{ element: el, zIndex: 2, id: "clip-a", sourceFile: "index.html" }],
|
|
330
|
+
undefined,
|
|
331
|
+
"bring-forward",
|
|
332
|
+
);
|
|
333
|
+
} catch (error) {
|
|
334
|
+
rejection = error;
|
|
335
|
+
}
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
expect(rejection).toBe(failure);
|
|
339
|
+
expect(el.style.zIndex).toBe("");
|
|
340
|
+
expect(el.style.position).toBe("static");
|
|
341
|
+
act(() => root.unmount());
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
it("rolls back only live and store state after an atomic reorder failure", async () => {
|
|
345
|
+
const writeProjectFile = vi.fn(async () => {});
|
|
346
|
+
const recordEdit = vi.fn(async () => {});
|
|
347
|
+
const forceReloadSdkSession = vi.fn();
|
|
348
|
+
const originalError = new Error("second patch failed");
|
|
349
|
+
const elements = ["clip-a", "clip-b", "clip-c"].map((id, index) => {
|
|
350
|
+
const element = document.createElement("div");
|
|
351
|
+
element.id = id;
|
|
352
|
+
element.style.zIndex = String(index + 10);
|
|
353
|
+
document.body.appendChild(element);
|
|
354
|
+
return element;
|
|
355
|
+
});
|
|
356
|
+
usePlayerStore.getState().setElements(
|
|
357
|
+
elements.map((element, index) => ({
|
|
358
|
+
id: element.id,
|
|
359
|
+
tag: "div",
|
|
360
|
+
start: 0,
|
|
361
|
+
duration: 1,
|
|
362
|
+
track: index,
|
|
363
|
+
zIndex: index + 10,
|
|
364
|
+
hasExplicitZIndex: false,
|
|
365
|
+
})),
|
|
366
|
+
);
|
|
367
|
+
|
|
368
|
+
let commit: ReorderCommit | undefined;
|
|
369
|
+
function Harness() {
|
|
370
|
+
const { handleDomZIndexReorderCommit } = useElementLifecycleOps({
|
|
371
|
+
activeCompPath: "index.html",
|
|
372
|
+
showToast: vi.fn(),
|
|
373
|
+
writeProjectFile,
|
|
374
|
+
domEditSaveTimestampRef: { current: 0 },
|
|
375
|
+
editHistory: { recordEdit },
|
|
376
|
+
projectIdRef: { current: "demo" },
|
|
377
|
+
reloadPreview: vi.fn(),
|
|
378
|
+
clearDomSelection: vi.fn(),
|
|
379
|
+
forceReloadSdkSession,
|
|
380
|
+
commitDomEditPatchBatches: vi.fn(async () => {
|
|
381
|
+
throw originalError;
|
|
382
|
+
}),
|
|
383
|
+
});
|
|
384
|
+
commit = handleDomZIndexReorderCommit;
|
|
385
|
+
return null;
|
|
386
|
+
}
|
|
387
|
+
const root = mountReactHarness(<Harness />);
|
|
388
|
+
|
|
389
|
+
let rejection: unknown;
|
|
390
|
+
await act(async () => {
|
|
391
|
+
try {
|
|
392
|
+
await commit!(
|
|
393
|
+
elements.map((element, index) => ({
|
|
394
|
+
element,
|
|
395
|
+
zIndex: 3 - index,
|
|
396
|
+
id: element.id,
|
|
397
|
+
sourceFile: "index.html",
|
|
398
|
+
key: element.id,
|
|
399
|
+
})),
|
|
400
|
+
"clip-lane-move:failure",
|
|
401
|
+
);
|
|
402
|
+
} catch (error) {
|
|
403
|
+
rejection = error;
|
|
404
|
+
}
|
|
405
|
+
});
|
|
406
|
+
|
|
407
|
+
expect(rejection).toBe(originalError);
|
|
408
|
+
expect(elements.map((element) => element.style.zIndex)).toEqual(["10", "11", "12"]);
|
|
409
|
+
expect(
|
|
410
|
+
usePlayerStore
|
|
411
|
+
.getState()
|
|
412
|
+
.elements.map(({ zIndex, hasExplicitZIndex }) => ({ zIndex, hasExplicitZIndex })),
|
|
413
|
+
).toEqual([
|
|
414
|
+
{ zIndex: 10, hasExplicitZIndex: false },
|
|
415
|
+
{ zIndex: 11, hasExplicitZIndex: false },
|
|
416
|
+
{ zIndex: 12, hasExplicitZIndex: false },
|
|
417
|
+
]);
|
|
418
|
+
expect(writeProjectFile).not.toHaveBeenCalled();
|
|
419
|
+
expect(recordEdit).not.toHaveBeenCalled();
|
|
420
|
+
expect(forceReloadSdkSession).not.toHaveBeenCalled();
|
|
421
|
+
act(() => root.unmount());
|
|
422
|
+
});
|
|
423
|
+
});
|
|
@@ -1,42 +1,26 @@
|
|
|
1
1
|
import { useCallback } from "react";
|
|
2
2
|
import { usePlayerStore } from "../player";
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
readProjectFileContent,
|
|
5
|
+
saveProjectFilesWithHistory,
|
|
6
|
+
type DomEditCommitBaseParams,
|
|
7
|
+
} from "../utils/studioFileHistory";
|
|
4
8
|
import { createStudioSaveHttpError } from "../utils/studioSaveDiagnostics";
|
|
5
9
|
import {
|
|
6
10
|
buildDomEditPatchTarget,
|
|
7
11
|
readHfId,
|
|
8
12
|
type DomEditSelection,
|
|
9
13
|
} from "../components/editor/domEditing";
|
|
10
|
-
import type {
|
|
11
|
-
import type { EditHistoryKind } from "../utils/editHistory";
|
|
12
|
-
|
|
13
|
-
interface RecordEditInput {
|
|
14
|
-
label: string;
|
|
15
|
-
kind: EditHistoryKind;
|
|
16
|
-
coalesceKey?: string;
|
|
17
|
-
files: Record<string, { before: string; after: string }>;
|
|
18
|
-
}
|
|
14
|
+
import type { CommitDomEditPatchBatches, DomEditPatchBatch } from "./domEditCommitTypes";
|
|
19
15
|
|
|
20
|
-
interface UseElementLifecycleOpsParams {
|
|
21
|
-
activeCompPath: string | null;
|
|
22
|
-
showToast: (message: string, tone?: "error" | "info") => void;
|
|
23
|
-
writeProjectFile: (path: string, content: string) => Promise<void>;
|
|
24
|
-
domEditSaveTimestampRef: React.MutableRefObject<number>;
|
|
25
|
-
editHistory: { recordEdit: (entry: RecordEditInput) => Promise<void> };
|
|
26
|
-
projectIdRef: React.MutableRefObject<string | null>;
|
|
27
|
-
reloadPreview: () => void;
|
|
28
|
-
clearDomSelection: () => void;
|
|
16
|
+
interface UseElementLifecycleOpsParams extends DomEditCommitBaseParams {
|
|
29
17
|
/** Route delete through SDK when session resolves the hf-id; returns true if handled. */
|
|
30
18
|
onTrySdkDelete?: (hfId: string, originalContent: string, targetPath: string) => Promise<boolean>;
|
|
31
19
|
/** Resolver-shadow tripwire for the reordered targets (telemetry-only, decoupled from cutover). */
|
|
32
20
|
onReorderShadow?: (targets: string[]) => void;
|
|
33
21
|
/** Resync the SDK session after a server-fallback delete. */
|
|
34
22
|
forceReloadSdkSession?: () => void;
|
|
35
|
-
|
|
36
|
-
selection: DomEditSelection,
|
|
37
|
-
patches: PatchOperation[],
|
|
38
|
-
options: { label: string; coalesceKey: string; skipRefresh?: boolean },
|
|
39
|
-
) => Promise<void>;
|
|
23
|
+
commitDomEditPatchBatches: CommitDomEditPatchBatches;
|
|
40
24
|
/** Stage 7 Step 3b: called after a successful server-side element delete (shadow). */
|
|
41
25
|
onElementDeleted?: (selection: DomEditSelection) => void;
|
|
42
26
|
}
|
|
@@ -53,7 +37,7 @@ export function useElementLifecycleOps({
|
|
|
53
37
|
onTrySdkDelete,
|
|
54
38
|
onReorderShadow,
|
|
55
39
|
forceReloadSdkSession,
|
|
56
|
-
|
|
40
|
+
commitDomEditPatchBatches,
|
|
57
41
|
onElementDeleted,
|
|
58
42
|
}: UseElementLifecycleOpsParams) {
|
|
59
43
|
// fallow-ignore-next-line complexity
|
|
@@ -66,17 +50,7 @@ export function useElementLifecycleOps({
|
|
|
66
50
|
|
|
67
51
|
const targetPath = selection.sourceFile || activeCompPath || "index.html";
|
|
68
52
|
try {
|
|
69
|
-
const
|
|
70
|
-
`/api/projects/${pid}/files/${encodeURIComponent(targetPath)}`,
|
|
71
|
-
);
|
|
72
|
-
if (!response.ok) {
|
|
73
|
-
throw await createStudioSaveHttpError(response, `Failed to read ${targetPath}`);
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
const data = (await response.json()) as { content?: string };
|
|
77
|
-
const originalContent = data.content;
|
|
78
|
-
if (typeof originalContent !== "string")
|
|
79
|
-
throw new Error(`Missing file contents for ${targetPath}`);
|
|
53
|
+
const originalContent = await readProjectFileContent(pid, targetPath);
|
|
80
54
|
|
|
81
55
|
const patchTarget = buildDomEditPatchTarget(selection);
|
|
82
56
|
if (!patchTarget.id && !patchTarget.selector && !patchTarget.hfId) {
|
|
@@ -156,8 +130,8 @@ export function useElementLifecycleOps({
|
|
|
156
130
|
],
|
|
157
131
|
);
|
|
158
132
|
|
|
159
|
-
// ponytail: z-index reorder
|
|
160
|
-
//
|
|
133
|
+
// ponytail: z-index reorder folds every element patch for a source file in memory and sends
|
|
134
|
+
// one patch-elements-batch request, so each file is persisted with one atomic disk write.
|
|
161
135
|
// No SDK reorder/reparent op exists; DOM sibling order stays server-authoritative if ever needed.
|
|
162
136
|
const handleDomZIndexReorderCommit = useCallback(
|
|
163
137
|
// fallow-ignore-next-line complexity
|
|
@@ -171,6 +145,8 @@ export function useElementLifecycleOps({
|
|
|
171
145
|
sourceFile: string;
|
|
172
146
|
key?: string;
|
|
173
147
|
}>,
|
|
148
|
+
gestureCoalesceKey?: string,
|
|
149
|
+
actionKind?: string,
|
|
174
150
|
) => {
|
|
175
151
|
if (entries.length === 0) return Promise.resolve();
|
|
176
152
|
// Resolver shadow (telemetry-only, decoupled from cutover): record whether
|
|
@@ -178,11 +154,16 @@ export function useElementLifecycleOps({
|
|
|
178
154
|
onReorderShadow?.(
|
|
179
155
|
entries.map((e) => readHfId(e.element)).filter((id): id is string => id != null),
|
|
180
156
|
);
|
|
181
|
-
|
|
182
|
-
|
|
157
|
+
// The default key carries the action kind so two DIFFERENT actions on the
|
|
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"]>();
|
|
183
165
|
const rollbacks: Array<() => void> = [];
|
|
184
|
-
for (
|
|
185
|
-
const entry = entries[i];
|
|
166
|
+
for (const entry of entries) {
|
|
186
167
|
const priorZIndex = entry.element.style.zIndex;
|
|
187
168
|
const priorPosition = entry.element.style.position;
|
|
188
169
|
const priorStoreEntry = entry.key
|
|
@@ -218,39 +199,40 @@ export function useElementLifecycleOps({
|
|
|
218
199
|
});
|
|
219
200
|
}
|
|
220
201
|
});
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
{
|
|
233
|
-
label: "Reorder layers",
|
|
234
|
-
coalesceKey,
|
|
235
|
-
skipRefresh: i < entries.length - 1,
|
|
236
|
-
},
|
|
237
|
-
),
|
|
238
|
-
);
|
|
202
|
+
const filePatches = patchesBySourceFile.get(entry.sourceFile) ?? [];
|
|
203
|
+
filePatches.push({
|
|
204
|
+
target: buildDomEditPatchTarget({
|
|
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);
|
|
239
213
|
}
|
|
240
|
-
|
|
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
|
|
241
219
|
// can be ordered after it (see applyTimelineStackingReorder callers).
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
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;
|
|
251
233
|
});
|
|
252
234
|
},
|
|
253
|
-
[
|
|
235
|
+
[commitDomEditPatchBatches, onReorderShadow],
|
|
254
236
|
);
|
|
255
237
|
|
|
256
238
|
return {
|
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import { useState, useCallback, useRef } from "react";
|
|
2
2
|
import { useMountEffect } from "./useMountEffect";
|
|
3
3
|
import { resolveSourceFile, applyPatch } from "../utils/sourcePatcher";
|
|
4
|
+
import {
|
|
5
|
+
acceptStudioRuntimeMessage,
|
|
6
|
+
postRuntimeControlMessage,
|
|
7
|
+
} from "../player/lib/runtimeProtocol";
|
|
4
8
|
|
|
5
9
|
export interface PickedElement {
|
|
6
10
|
id: string | null;
|
|
@@ -65,10 +69,7 @@ export function useElementPicker(
|
|
|
65
69
|
|
|
66
70
|
const enablePick = useCallback(() => {
|
|
67
71
|
try {
|
|
68
|
-
getActiveIframe()?.contentWindow
|
|
69
|
-
{ source: "hf-parent", type: "control", action: "enable-pick-mode" },
|
|
70
|
-
"*",
|
|
71
|
-
);
|
|
72
|
+
postRuntimeControlMessage(getActiveIframe()?.contentWindow, "enable-pick-mode");
|
|
72
73
|
setIsPickMode(true);
|
|
73
74
|
} catch {
|
|
74
75
|
/* cross-origin */
|
|
@@ -77,10 +78,7 @@ export function useElementPicker(
|
|
|
77
78
|
|
|
78
79
|
const disablePick = useCallback(() => {
|
|
79
80
|
try {
|
|
80
|
-
getActiveIframe()?.contentWindow
|
|
81
|
-
{ source: "hf-parent", type: "control", action: "disable-pick-mode" },
|
|
82
|
-
"*",
|
|
83
|
-
);
|
|
81
|
+
postRuntimeControlMessage(getActiveIframe()?.contentWindow, "disable-pick-mode");
|
|
84
82
|
} catch {
|
|
85
83
|
/* cross-origin */
|
|
86
84
|
}
|
|
@@ -96,6 +94,7 @@ export function useElementPicker(
|
|
|
96
94
|
const handleMessage = (e: MessageEvent) => {
|
|
97
95
|
const data = e.data;
|
|
98
96
|
if (data?.source !== "hf-preview") return;
|
|
97
|
+
if (!acceptStudioRuntimeMessage(data)) return;
|
|
99
98
|
// Accept events from either the primary iframe or the active override
|
|
100
99
|
const activeIframe = getActiveIframe();
|
|
101
100
|
if (!activeIframe) return;
|