@hyperframes/studio 0.8.115 → 0.8.117
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-C-9N8nk0.js → hyperframes-player-5fcgSstJ.js} +1 -1
- package/dist/assets/{index-CAvZ0AA-.js → index-C5qKdpgl.js} +205 -203
- package/dist/assets/{index-BoUM6Xqb.js → index-XJSawwYe.js} +1 -1
- package/dist/assets/{index-CVuorIMv.js → index-tg2LXUeu.js} +1 -1
- package/dist/{chunk-TXZMMOMA.js → chunk-3F47EFCT.js} +100 -2
- package/dist/chunk-3F47EFCT.js.map +1 -0
- package/dist/{domEditingLayers-D7VYFJJD.js → domEditingLayers-OL7GLSS4.js} +2 -2
- package/dist/index.d.ts +132 -41
- package/dist/index.html +1 -1
- package/dist/index.js +1184 -1084
- package/dist/index.js.map +1 -1
- package/package.json +8 -8
- package/src/captions/hooks/useCaptionSync.ts +10 -6
- package/src/components/ProjectUnreachableBanner.tsx +3 -3
- package/src/components/editor/DomEditOverlay.tsx +1 -0
- package/src/components/editor/domEditOverlayGestures.ts +1 -0
- package/src/components/editor/domEditOverlayStartGesture.ts +2 -2
- package/src/components/editor/domGroupMoveBy.test.tsx +252 -0
- package/src/components/editor/domGroupMoveBy.ts +95 -0
- package/src/components/editor/dragUndoPaint.test.ts +5 -5
- package/src/components/editor/gestureHoldsPreviewReload.test.tsx +33 -0
- package/src/components/editor/manualEditingAvailability.ts +8 -0
- package/src/components/editor/manualOffsetDrag.ts +8 -9
- package/src/components/editor/motionPathSelection.test.ts +6 -31
- package/src/components/editor/plainTranslate.ts +5 -3
- package/src/components/editor/probeSourceElement.ts +2 -1
- package/src/components/editor/propertyPanelFont.tsx +3 -2
- package/src/components/editor/propertyPanelHelpers.ts +2 -2
- package/src/components/editor/propertyPanelTransformCommit.ts +2 -2
- package/src/components/editor/rotationDraft.ts +2 -1
- package/src/components/editor/useColorGradingController.ts +2 -1
- package/src/components/editor/useDomEditOverlayGestures.ts +2 -2
- package/src/components/editor/useFxCarveNodes.ts +2 -1
- package/src/components/editor/useFxLevelling.ts +2 -1
- package/src/components/feedback/projectProvenance.ts +2 -1
- package/src/components/nle/NLEContext.test.ts +1 -0
- package/src/components/nle/NLEContext.tsx +2 -1
- package/src/components/nle/NLEPreview.tsx +12 -10
- package/src/components/nle/PreviewPoster.tsx +2 -1
- package/src/components/renders/useFfmpegStatus.ts +2 -1
- package/src/components/renders/useRenderQueue.ts +5 -4
- package/src/components/sidebar/AssetsTab.tsx +2 -1
- package/src/components/sidebar/GlobalAssetsView.tsx +2 -1
- package/src/contexts/DomEditContext.tsx +4 -0
- package/src/hooks/domSelectionTestHarness.ts +23 -0
- package/src/hooks/domStyleCommit.propertyNames.test.ts +73 -0
- package/src/hooks/domStyleCommit.ts +8 -1
- package/src/hooks/draggedGsapPosition.ts +10 -0
- package/src/hooks/gsapDragCommit.test.ts +75 -164
- package/src/hooks/gsapDragCommit.ts +2 -105
- package/src/hooks/gsapDragPositionCommit.ts +30 -393
- package/src/hooks/gsapEditOutcome.ts +22 -2
- package/src/hooks/gsapGroupPreflight.ts +34 -0
- package/src/hooks/gsapMutationClient.ts +4 -3
- package/src/hooks/gsapParsedTween.test-helpers.ts +75 -0
- package/src/hooks/gsapParsedTween.test.ts +62 -0
- package/src/hooks/gsapParsedTween.ts +243 -0
- package/src/hooks/gsapPositionDetection.test.ts +78 -0
- package/src/hooks/gsapPositionDetection.ts +52 -33
- package/src/hooks/gsapResizeDropPoint.test.ts +4 -3
- package/src/hooks/gsapResizeElementSize.test.tsx +1 -0
- package/src/hooks/gsapResizeGeometrySweep.test.ts +13 -25
- package/src/hooks/gsapResizeIntercept.test.ts +63 -11
- package/src/hooks/gsapResizeIntercept.ts +79 -169
- package/src/hooks/gsapResizeSweep.test.ts +16 -33
- package/src/hooks/gsapRuntimeBridge.sharedTween.test.ts +6 -8
- package/src/hooks/gsapRuntimeBridge.test.ts +41 -6
- package/src/hooks/gsapRuntimeBridge.ts +105 -131
- package/src/hooks/gsapRuntimeKeyframes.ts +16 -3
- package/src/hooks/gsapScriptCommitHelpers.ts +2 -1
- package/src/hooks/gsapScriptCommitTypes.ts +9 -1
- package/src/hooks/gsapShared.ts +7 -0
- package/src/hooks/gsapShared.writeTarget.test.ts +26 -58
- package/src/hooks/gsapValueAtPlayhead.browser.test.ts +83 -0
- package/src/hooks/gsapValueAtPlayhead.realGsap.test.ts +179 -0
- package/src/hooks/gsapValueAtPlayhead.test.ts +552 -0
- package/src/hooks/gsapValueAtPlayhead.ts +292 -0
- package/src/hooks/gsapWholePropertyOffsetCommit.test.ts +43 -0
- package/src/hooks/gsapWholePropertyOffsetCommit.ts +2 -3
- package/src/hooks/keyframeCacheAstLoad.ts +2 -1
- package/src/hooks/newTweenTarget.test.ts +41 -76
- package/src/hooks/plainRotation.ts +5 -1
- package/src/hooks/timelineTimingSync.test.ts +1 -0
- package/src/hooks/timelineTimingSync.ts +2 -1
- package/src/hooks/useAskAgentModal.ts +2 -1
- package/src/hooks/useBlockCatalog.ts +2 -1
- package/src/hooks/useCaptionDetection.ts +2 -1
- package/src/hooks/useCompositionContentLoader.ts +2 -1
- package/src/hooks/useDomEditCommitsHelpers.ts +3 -2
- package/src/hooks/useDomEditPersist.test.tsx +335 -0
- package/src/hooks/useDomEditSession.test.tsx +30 -1
- package/src/hooks/useDomEditSession.ts +10 -0
- package/src/hooks/useDomEditTextCommits.test.tsx +23 -0
- package/src/hooks/useDomEditTextCommits.ts +8 -2
- package/src/hooks/useDomGeometryCommit.test.tsx +5 -1
- package/src/hooks/useDomGeometryCommit.ts +7 -9
- package/src/hooks/useEditHistoryActions.paint.test.tsx +82 -20
- package/src/hooks/useEditHistoryActions.test.tsx +15 -0
- package/src/hooks/useEditHistoryActions.ts +16 -5
- package/src/hooks/useElementLifecycleOps.ts +2 -1
- package/src/hooks/useEnableKeyframes.test.ts +32 -1
- package/src/hooks/useEnableKeyframes.ts +2 -1
- package/src/hooks/useExternalFileChangeCoordinator.test.tsx +19 -0
- package/src/hooks/useExternalFileChangeCoordinator.ts +4 -2
- package/src/hooks/useFileManager.projectOwnership.test.tsx +6 -1
- package/src/hooks/useFileManager.ts +14 -10
- package/src/hooks/useFileTree.ts +3 -2
- package/src/hooks/useFrameCapture.ts +5 -1
- package/src/hooks/useFreezeFrame.test.ts +39 -3
- package/src/hooks/useFreezeFrame.ts +7 -3
- package/src/hooks/useGroupCommits.ts +2 -1
- package/src/hooks/useGsapAwareEditing.groupPlan.test.tsx +160 -0
- package/src/hooks/useGsapAwareEditing.test.tsx +42 -29
- package/src/hooks/useGsapAwareEditing.testHelpers.tsx +42 -0
- package/src/hooks/useGsapAwareEditing.ts +17 -8
- package/src/hooks/useGsapAwareGroupMove.ts +18 -18
- package/src/hooks/useGsapInteractionFailureTelemetry.ts +10 -4
- package/src/hooks/useGsapScriptCommits.test.tsx +72 -2
- package/src/hooks/useGsapScriptCommits.ts +21 -17
- package/src/hooks/useLintModal.ts +2 -1
- package/src/hooks/usePersistentEditHistory.test.ts +302 -3
- package/src/hooks/usePersistentEditHistory.ts +50 -17
- package/src/hooks/usePreviewPersistence.history.test.tsx +14 -3
- package/src/hooks/usePreviewPersistence.ts +13 -11
- package/src/hooks/useProjectFileWriter.ts +20 -19
- package/src/hooks/useRemoveBackground.ts +2 -1
- package/src/hooks/useSdkSession.ts +2 -1
- package/src/hooks/useServerConnection.ts +4 -3
- package/src/hooks/useStudioSelectionPublisher.ts +2 -1
- package/src/hooks/useThumbnailStill.test.ts +100 -0
- package/src/hooks/useThumbnailStill.ts +37 -0
- package/src/hooks/useTimelineDeleteOps.ts +2 -1
- package/src/index.ts +4 -0
- package/src/player/components/AudioWaveform.tsx +3 -2
- package/src/player/components/CompositionThumbnail.tsx +2 -1
- package/src/player/components/clipMenuNormalize.ts +2 -1
- package/src/player/components/clipPeakMap.ts +2 -1
- package/src/player/hooks/useTimelinePlayer.ts +5 -1
- package/src/player/lib/thumbnailScheduler.test.ts +16 -0
- package/src/player/lib/thumbnailScheduler.ts +10 -2
- package/src/player/sceneSwap.ts +2 -1
- package/src/telemetry/client.ts +2 -1
- package/src/utils/blockInstaller.ts +2 -1
- package/src/utils/elementGsap.test.ts +9 -0
- package/src/utils/elementGsap.ts +7 -0
- package/src/utils/gsapSoftReload.test.ts +39 -0
- package/src/utils/gsapSoftReload.ts +9 -14
- package/src/utils/gsapUndoRestore.browser.test.ts +94 -41
- package/src/utils/gsapUndoRestore.ts +3 -6
- package/src/utils/previewPrefetch.ts +2 -1
- package/src/utils/razorSplitTransaction.ts +12 -8
- package/src/utils/softReloadTargets.ts +5 -6
- package/src/utils/studioApiFetch.test.ts +75 -0
- package/src/utils/studioApiFetch.ts +13 -0
- package/src/utils/studioFileHistory.ts +5 -1
- package/src/utils/studioHelpers.ts +7 -1
- package/src/utils/studioPendingEdits.test.ts +23 -6
- package/src/utils/studioPendingEdits.ts +32 -8
- package/src/utils/studioTelemetry.ts +2 -1
- package/src/utils/timelineCompositionInsert.ts +3 -2
- package/src/webmcp/StudioAgentTools.tsx +2 -1
- package/dist/chunk-TXZMMOMA.js.map +0 -1
- /package/dist/{domEditingLayers-D7VYFJJD.js.map → domEditingLayers-OL7GLSS4.js.map} +0 -0
|
@@ -7,6 +7,7 @@ import { toProjectAbsolutePath, type AgentModalAnchorPoint } from "../utils/stud
|
|
|
7
7
|
import { liveMarkupWithoutPreviewMarks } from "../utils/authoredSource";
|
|
8
8
|
import { buildElementAgentPrompt, type DomEditSelection } from "../components/editor/domEditing";
|
|
9
9
|
import { usePlayerStore } from "../player";
|
|
10
|
+
import { studioApiFetch } from "../utils/studioApiFetch";
|
|
10
11
|
|
|
11
12
|
// ── Types ──
|
|
12
13
|
|
|
@@ -55,7 +56,7 @@ export function useAskAgentModal({
|
|
|
55
56
|
|
|
56
57
|
const targetPath = selection.sourceFile || activeCompPath || "index.html";
|
|
57
58
|
try {
|
|
58
|
-
const response = await
|
|
59
|
+
const response = await studioApiFetch(
|
|
59
60
|
buildProjectApiPath(pid, `/files/${encodeURIComponent(targetPath)}`),
|
|
60
61
|
);
|
|
61
62
|
if (!response.ok) return;
|
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
type BlockCategory,
|
|
6
6
|
resolveBlockCategory,
|
|
7
7
|
} from "../utils/blockCategories";
|
|
8
|
+
import { studioApiFetch } from "../utils/studioApiFetch";
|
|
8
9
|
|
|
9
10
|
type CatalogItem = RegistryItem & {
|
|
10
11
|
category: BlockCategory;
|
|
@@ -14,7 +15,7 @@ let catalogCache: CatalogItem[] | null = null;
|
|
|
14
15
|
let catalogRequest: Promise<CatalogItem[]> | null = null;
|
|
15
16
|
|
|
16
17
|
function loadCatalog(): Promise<CatalogItem[]> {
|
|
17
|
-
catalogRequest ??=
|
|
18
|
+
catalogRequest ??= studioApiFetch("/api/registry/blocks")
|
|
18
19
|
.then((response) => {
|
|
19
20
|
if (!response.ok) throw new Error("Failed to load catalog");
|
|
20
21
|
return response.json() as Promise<RegistryItem[]>;
|
|
@@ -5,6 +5,7 @@ import { useCaptionStore } from "../captions/store";
|
|
|
5
5
|
import { acceptStudioRuntimeMessage } from "../player/lib/runtimeProtocol";
|
|
6
6
|
import { useCaptionSync } from "../captions/hooks/useCaptionSync";
|
|
7
7
|
import { parseCaptionComposition } from "../captions/parser";
|
|
8
|
+
import { studioApiFetch } from "../utils/studioApiFetch";
|
|
8
9
|
|
|
9
10
|
interface UseCaptionDetectionParams {
|
|
10
11
|
projectId: string | null;
|
|
@@ -110,7 +111,7 @@ export function useCaptionDetection({
|
|
|
110
111
|
|
|
111
112
|
activating = true;
|
|
112
113
|
const srcPath = captionSrcPath;
|
|
113
|
-
|
|
114
|
+
studioApiFetch(buildProjectApiPath(projectId, `/files/${encodeURIComponent(srcPath)}`))
|
|
114
115
|
.then((r) => r.json())
|
|
115
116
|
.then((data: { content?: string }) => {
|
|
116
117
|
if (!data.content || !doc || !win || useCaptionStore.getState().isEditMode) return;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { buildProjectApiPath } from "../utils/projectRouting";
|
|
2
2
|
import { useCallback } from "react";
|
|
3
|
+
import { studioApiFetch } from "../utils/studioApiFetch";
|
|
3
4
|
|
|
4
5
|
/**
|
|
5
6
|
* Loads a composition file's content for the source editor when a composition
|
|
@@ -24,7 +25,7 @@ export function useCompositionContentLoader({
|
|
|
24
25
|
if (!projectId) return;
|
|
25
26
|
setActiveCompPath(comp.endsWith(".html") ? comp : null);
|
|
26
27
|
setEditingFile({ path: comp, content: null });
|
|
27
|
-
|
|
28
|
+
studioApiFetch(buildProjectApiPath(projectId, `/files/${encodeURIComponent(comp)}`))
|
|
28
29
|
.then(async (r) => {
|
|
29
30
|
if (!r.ok) throw new Error(`Failed to load ${comp} (${r.status})`);
|
|
30
31
|
return r.json();
|
|
@@ -7,6 +7,7 @@ import { buildProjectApiPath } from "../utils/projectRouting";
|
|
|
7
7
|
import type { DomEditPatchBatch } from "./domEditCommitTypes";
|
|
8
8
|
import { formatFieldsSuffix } from "./gsapScriptCommitHelpers";
|
|
9
9
|
import { studioWriteHeaders } from "../utils/studioFileVersion";
|
|
10
|
+
import { studioApiFetch } from "../utils/studioApiFetch";
|
|
10
11
|
|
|
11
12
|
export function formatUnsafeFieldList(fields: Array<{ path: string }>): string {
|
|
12
13
|
return fields.map((field) => field.path).join(", ");
|
|
@@ -99,7 +100,7 @@ function isAtomicElementPatchFile(value: unknown): value is AtomicElementPatchFi
|
|
|
99
100
|
export async function patchElementBatches(projectId: string, batches: DomEditPatchBatch[]) {
|
|
100
101
|
const body = JSON.stringify({ batches });
|
|
101
102
|
try {
|
|
102
|
-
const response = await
|
|
103
|
+
const response = await studioApiFetch(
|
|
103
104
|
`/api/projects/${encodeURIComponent(projectId)}/file-mutations/patch-element-batches`,
|
|
104
105
|
{
|
|
105
106
|
method: "POST",
|
|
@@ -178,7 +179,7 @@ export async function postPatchElement(
|
|
|
178
179
|
body: unknown,
|
|
179
180
|
showToast: ShowToast,
|
|
180
181
|
): Promise<PatchElementResponse> {
|
|
181
|
-
const response = await
|
|
182
|
+
const response = await studioApiFetch(
|
|
182
183
|
buildProjectApiPath(
|
|
183
184
|
projectId,
|
|
184
185
|
`/file-mutations/patch-element/${encodeURIComponent(targetPath)}`,
|
|
@@ -0,0 +1,335 @@
|
|
|
1
|
+
// @vitest-environment happy-dom
|
|
2
|
+
import { act, createElement } from "react";
|
|
3
|
+
import { createRoot } from "react-dom/client";
|
|
4
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
5
|
+
import type { DomEditSelection } from "../components/editor/domEditing";
|
|
6
|
+
import type { PatchOperation } from "../utils/sourcePatcher";
|
|
7
|
+
import type { CutoverResult } from "../utils/sdkCutover";
|
|
8
|
+
import { studioManualEditSavesIn } from "../components/editor/manualEditsDom";
|
|
9
|
+
import {
|
|
10
|
+
DomEditPersistPreparedWriteError,
|
|
11
|
+
DomEditPersistUnresolvableError,
|
|
12
|
+
DomEditPersistUnsafeValueError,
|
|
13
|
+
} from "./domEditPersistFailure";
|
|
14
|
+
import type { PersistDomEditOperations } from "./domEditCommitTypes";
|
|
15
|
+
import { jsonResponse, requestUrl } from "./fetchStubTestUtils";
|
|
16
|
+
import { useDomEditPersist, type UseDomEditPersistParams } from "./useDomEditPersist";
|
|
17
|
+
|
|
18
|
+
const trackStudioEvent = vi.fn();
|
|
19
|
+
const reseekPreviewRuntime = vi.fn();
|
|
20
|
+
vi.mock("../utils/studioTelemetry", () => ({
|
|
21
|
+
trackStudioEvent: (...args: unknown[]) => trackStudioEvent(...args),
|
|
22
|
+
}));
|
|
23
|
+
vi.mock("./timelineTrackVisibility", () => ({
|
|
24
|
+
reseekPreviewRuntime: (...args: unknown[]) => reseekPreviewRuntime(...args),
|
|
25
|
+
}));
|
|
26
|
+
|
|
27
|
+
Reflect.set(globalThis, "IS_REACT_ACT_ENVIRONMENT", true);
|
|
28
|
+
|
|
29
|
+
const SOURCE = '<div data-hf-id="hf-card" style="color: red">Card</div>';
|
|
30
|
+
const PATCHED = '<div data-hf-id="hf-card" style="color: blue">Card</div>';
|
|
31
|
+
const OPERATIONS: PatchOperation[] = [{ type: "inline-style", property: "color", value: "blue" }];
|
|
32
|
+
const selection = {
|
|
33
|
+
hfId: "hf-card",
|
|
34
|
+
selector: '[data-hf-id="hf-card"]',
|
|
35
|
+
selectorIndex: 0,
|
|
36
|
+
sourceFile: "index.html",
|
|
37
|
+
} as DomEditSelection;
|
|
38
|
+
|
|
39
|
+
function stubServer(patchResponse: Record<string, unknown>) {
|
|
40
|
+
const fetchMock = vi.fn(async (input: Parameters<typeof fetch>[0], _init?: RequestInit) => {
|
|
41
|
+
const url = requestUrl(input);
|
|
42
|
+
if (url.includes("/api/projects/p1/files/")) return jsonResponse({ content: SOURCE });
|
|
43
|
+
if (url.includes("/api/projects/p1/file-mutations/patch-element/")) {
|
|
44
|
+
return jsonResponse(patchResponse);
|
|
45
|
+
}
|
|
46
|
+
throw new Error(`Unexpected fetch: ${url}`);
|
|
47
|
+
});
|
|
48
|
+
vi.stubGlobal("fetch", fetchMock);
|
|
49
|
+
return fetchMock;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const CHANGED = { changed: true, content: PATCHED, path: "index.html", version: "v2" };
|
|
53
|
+
const PREPARED = PATCHED.replace("Card", "Card!");
|
|
54
|
+
const sdkResult = (result: CutoverResult) => vi.fn(async (): Promise<CutoverResult> => result);
|
|
55
|
+
|
|
56
|
+
const patchPosts = (fetchMock: ReturnType<typeof stubServer>) =>
|
|
57
|
+
fetchMock.mock.calls.filter(([input]) => requestUrl(input).includes("/patch-element/"));
|
|
58
|
+
|
|
59
|
+
let unmount: (() => void) | null = null;
|
|
60
|
+
|
|
61
|
+
function renderPersist(over: Partial<UseDomEditPersistParams> = {}) {
|
|
62
|
+
const params: UseDomEditPersistParams = {
|
|
63
|
+
activeCompPath: "index.html",
|
|
64
|
+
previewIframeRef: { current: null },
|
|
65
|
+
showToast: vi.fn(),
|
|
66
|
+
queueDomEditSave: (save) => save(),
|
|
67
|
+
writeProjectFile: vi.fn(async () => {}),
|
|
68
|
+
editHistory: { recordEdit: vi.fn(async () => {}) },
|
|
69
|
+
projectIdRef: { current: "p1" },
|
|
70
|
+
reloadPreview: vi.fn(),
|
|
71
|
+
forceReloadSdkSession: vi.fn(),
|
|
72
|
+
...over,
|
|
73
|
+
};
|
|
74
|
+
const captured: { persist: PersistDomEditOperations | null } = { persist: null };
|
|
75
|
+
function Probe() {
|
|
76
|
+
captured.persist = useDomEditPersist(params);
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
const root = createRoot(document.createElement("div"));
|
|
80
|
+
act(() => root.render(createElement(Probe)));
|
|
81
|
+
unmount = () => act(() => root.unmount());
|
|
82
|
+
if (!captured.persist) throw new Error("hook did not initialize");
|
|
83
|
+
return { persist: captured.persist, params };
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
describe("useDomEditPersist", () => {
|
|
87
|
+
beforeEach(() => {
|
|
88
|
+
trackStudioEvent.mockReset();
|
|
89
|
+
reseekPreviewRuntime.mockReset();
|
|
90
|
+
});
|
|
91
|
+
afterEach(() => {
|
|
92
|
+
unmount?.();
|
|
93
|
+
unmount = null;
|
|
94
|
+
vi.unstubAllGlobals();
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it("patches the source, records the edit and reloads the preview", async () => {
|
|
98
|
+
const fetchMock = stubServer({
|
|
99
|
+
changed: true,
|
|
100
|
+
content: PATCHED,
|
|
101
|
+
path: "index.html",
|
|
102
|
+
version: "v2",
|
|
103
|
+
});
|
|
104
|
+
const { persist, params } = renderPersist();
|
|
105
|
+
|
|
106
|
+
const outcome = await persist(selection, OPERATIONS, { label: "Recolor" });
|
|
107
|
+
|
|
108
|
+
expect(outcome).toEqual({ sourceFile: "index.html", version: "v2", changed: true });
|
|
109
|
+
const [, init] = patchPosts(fetchMock)[0]!;
|
|
110
|
+
expect(JSON.parse(String(init?.body))).toEqual({
|
|
111
|
+
target: { hfId: "hf-card", selector: '[data-hf-id="hf-card"]', selectorIndex: 0 },
|
|
112
|
+
operations: OPERATIONS,
|
|
113
|
+
});
|
|
114
|
+
expect(params.editHistory.recordEdit).toHaveBeenCalledWith({
|
|
115
|
+
label: "Recolor",
|
|
116
|
+
coalesceKey: undefined,
|
|
117
|
+
coalesceMs: undefined,
|
|
118
|
+
files: { "index.html": { before: SOURCE, after: PATCHED } },
|
|
119
|
+
});
|
|
120
|
+
expect(params.forceReloadSdkSession).toHaveBeenCalledTimes(1);
|
|
121
|
+
expect(params.reloadPreview).toHaveBeenCalledTimes(1);
|
|
122
|
+
expect(reseekPreviewRuntime).not.toHaveBeenCalled();
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it("with skipRefresh reseeks the preview instead of reloading it", async () => {
|
|
126
|
+
stubServer({ changed: true, content: PATCHED, path: "index.html", version: "v2" });
|
|
127
|
+
const { persist, params } = renderPersist();
|
|
128
|
+
|
|
129
|
+
await persist(selection, OPERATIONS, { skipRefresh: true });
|
|
130
|
+
|
|
131
|
+
expect(params.reloadPreview).not.toHaveBeenCalled();
|
|
132
|
+
expect(reseekPreviewRuntime).toHaveBeenCalledTimes(1);
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it("an unchanged patch records nothing and reports changed: false", async () => {
|
|
136
|
+
stubServer({ changed: false, matched: true, path: "index.html", version: "v1" });
|
|
137
|
+
const { persist, params } = renderPersist();
|
|
138
|
+
|
|
139
|
+
const outcome = await persist(selection, OPERATIONS);
|
|
140
|
+
|
|
141
|
+
expect(outcome).toEqual({ sourceFile: "index.html", version: "v1", changed: false });
|
|
142
|
+
expect(params.editHistory.recordEdit).not.toHaveBeenCalled();
|
|
143
|
+
expect(params.reloadPreview).not.toHaveBeenCalled();
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
it("an unmatched target throws and is reported once per target", async () => {
|
|
147
|
+
stubServer({ changed: false, matched: false });
|
|
148
|
+
const { persist } = renderPersist();
|
|
149
|
+
|
|
150
|
+
await expect(persist(selection, OPERATIONS)).rejects.toBeInstanceOf(
|
|
151
|
+
DomEditPersistUnresolvableError,
|
|
152
|
+
);
|
|
153
|
+
await expect(persist(selection, OPERATIONS)).rejects.toBeInstanceOf(
|
|
154
|
+
DomEditPersistUnresolvableError,
|
|
155
|
+
);
|
|
156
|
+
|
|
157
|
+
expect(trackStudioEvent).toHaveBeenCalledTimes(1);
|
|
158
|
+
expect(trackStudioEvent).toHaveBeenCalledWith(
|
|
159
|
+
"save_skipped_unresolvable",
|
|
160
|
+
expect.objectContaining({ target_selector: '[data-hf-id="hf-card"]' }),
|
|
161
|
+
);
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
it("refuses a non-finite layout value before any request, with a toast", async () => {
|
|
165
|
+
const fetchMock = stubServer({ changed: true });
|
|
166
|
+
const { persist, params } = renderPersist();
|
|
167
|
+
|
|
168
|
+
await expect(
|
|
169
|
+
persist({ ...selection, selectorIndex: Number.NaN }, OPERATIONS),
|
|
170
|
+
).rejects.toBeInstanceOf(DomEditPersistUnsafeValueError);
|
|
171
|
+
|
|
172
|
+
expect(fetchMock).not.toHaveBeenCalled();
|
|
173
|
+
expect(params.showToast).toHaveBeenCalledWith(
|
|
174
|
+
"Couldn't save edit because it contains invalid layout values",
|
|
175
|
+
"error",
|
|
176
|
+
);
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
it("a committed SDK cutover skips the server patch and the reload", async () => {
|
|
180
|
+
const fetchMock = stubServer({ changed: true });
|
|
181
|
+
const onTrySdkPersist = vi.fn(
|
|
182
|
+
async (): Promise<CutoverResult> => ({
|
|
183
|
+
status: "committed",
|
|
184
|
+
version: "sdk-3",
|
|
185
|
+
before: SOURCE,
|
|
186
|
+
after: PATCHED,
|
|
187
|
+
}),
|
|
188
|
+
);
|
|
189
|
+
const { persist, params } = renderPersist({ onTrySdkPersist });
|
|
190
|
+
|
|
191
|
+
const outcome = await persist(selection, OPERATIONS, { label: "Recolor" });
|
|
192
|
+
|
|
193
|
+
expect(outcome).toEqual({ sourceFile: "index.html", version: "sdk-3", changed: true });
|
|
194
|
+
expect(onTrySdkPersist).toHaveBeenCalledWith(selection, OPERATIONS, SOURCE, "index.html", {
|
|
195
|
+
label: "Recolor",
|
|
196
|
+
coalesceKey: undefined,
|
|
197
|
+
skipRefresh: undefined,
|
|
198
|
+
});
|
|
199
|
+
expect(patchPosts(fetchMock)).toHaveLength(0);
|
|
200
|
+
expect(params.reloadPreview).not.toHaveBeenCalled();
|
|
201
|
+
expect(params.forceReloadSdkSession).not.toHaveBeenCalled();
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
it("records the edit once, and reloads only after the record has settled", async () => {
|
|
205
|
+
stubServer(CHANGED);
|
|
206
|
+
let finishRecord = () => {};
|
|
207
|
+
const recordEdit = vi.fn(
|
|
208
|
+
() =>
|
|
209
|
+
new Promise<void>((resolve) => {
|
|
210
|
+
finishRecord = resolve;
|
|
211
|
+
}),
|
|
212
|
+
);
|
|
213
|
+
const { persist, params } = renderPersist({ editHistory: { recordEdit } });
|
|
214
|
+
|
|
215
|
+
const saving = persist(selection, OPERATIONS);
|
|
216
|
+
await vi.waitFor(() => expect(recordEdit).toHaveBeenCalledTimes(1));
|
|
217
|
+
expect(params.reloadPreview).not.toHaveBeenCalled();
|
|
218
|
+
finishRecord();
|
|
219
|
+
await saving;
|
|
220
|
+
|
|
221
|
+
expect(recordEdit).toHaveBeenCalledTimes(1);
|
|
222
|
+
expect(params.reloadPreview).toHaveBeenCalledTimes(1);
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
it("writes prepared content after the patch and records it as the saved file", async () => {
|
|
226
|
+
stubServer(CHANGED);
|
|
227
|
+
const { persist, params } = renderPersist();
|
|
228
|
+
|
|
229
|
+
const outcome = await persist(selection, OPERATIONS, { prepareContent: () => PREPARED });
|
|
230
|
+
|
|
231
|
+
expect(params.writeProjectFile).toHaveBeenCalledWith("index.html", PREPARED, PATCHED);
|
|
232
|
+
expect(params.editHistory.recordEdit).toHaveBeenCalledWith(
|
|
233
|
+
expect.objectContaining({ files: { "index.html": { before: SOURCE, after: PREPARED } } }),
|
|
234
|
+
);
|
|
235
|
+
expect(params.reloadPreview).toHaveBeenCalledTimes(1);
|
|
236
|
+
expect(outcome).toBeUndefined();
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
it("a failed prepared write keeps the patch, reloads, then rejects", async () => {
|
|
240
|
+
stubServer(CHANGED);
|
|
241
|
+
const { persist, params } = renderPersist({
|
|
242
|
+
writeProjectFile: vi.fn(async () => {
|
|
243
|
+
throw new Error("disk full");
|
|
244
|
+
}),
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
const saving = persist(selection, OPERATIONS, { prepareContent: () => PREPARED });
|
|
248
|
+
|
|
249
|
+
await expect(saving).rejects.toBeInstanceOf(DomEditPersistPreparedWriteError);
|
|
250
|
+
expect(params.editHistory.recordEdit).toHaveBeenCalledWith(
|
|
251
|
+
expect.objectContaining({ files: { "index.html": { before: SOURCE, after: PATCHED } } }),
|
|
252
|
+
);
|
|
253
|
+
expect(params.reloadPreview).toHaveBeenCalledTimes(1);
|
|
254
|
+
expect(params.showToast).toHaveBeenCalledWith(
|
|
255
|
+
"Saved, but couldn't finish updating index.html: disk full",
|
|
256
|
+
"error",
|
|
257
|
+
);
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
it("a declined SDK cutover falls back to the server patch", async () => {
|
|
261
|
+
const fetchMock = stubServer(CHANGED);
|
|
262
|
+
const onTrySdkPersist = sdkResult({ status: "declined", reason: "unsupported" });
|
|
263
|
+
const { persist, params } = renderPersist({ onTrySdkPersist });
|
|
264
|
+
|
|
265
|
+
await persist(selection, OPERATIONS);
|
|
266
|
+
|
|
267
|
+
expect(onTrySdkPersist).toHaveBeenCalledTimes(1);
|
|
268
|
+
expect(patchPosts(fetchMock)).toHaveLength(1);
|
|
269
|
+
expect(params.editHistory.recordEdit).toHaveBeenCalledTimes(1);
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
it("a failed SDK cutover rethrows without a server patch", async () => {
|
|
273
|
+
const fetchMock = stubServer(CHANGED);
|
|
274
|
+
const error = new Error("sdk broke");
|
|
275
|
+
const { persist } = renderPersist({ onTrySdkPersist: sdkResult({ status: "failed", error }) });
|
|
276
|
+
|
|
277
|
+
await expect(persist(selection, OPERATIONS)).rejects.toBe(error);
|
|
278
|
+
expect(patchPosts(fetchMock)).toHaveLength(0);
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
it("an imported font or prepared content skips the SDK and goes to the server", async () => {
|
|
282
|
+
const fetchMock = stubServer(CHANGED);
|
|
283
|
+
const onTrySdkPersist = sdkResult({ status: "declined", reason: "unused" });
|
|
284
|
+
const { persist } = renderPersist({ onTrySdkPersist });
|
|
285
|
+
const importedFont = { family: "Inter", path: "fonts/inter.woff2", url: "/fonts/inter.woff2" };
|
|
286
|
+
|
|
287
|
+
await persist(selection, OPERATIONS, { importedFont });
|
|
288
|
+
await persist(selection, OPERATIONS, { prepareContent: (html) => html });
|
|
289
|
+
|
|
290
|
+
expect(onTrySdkPersist).not.toHaveBeenCalled();
|
|
291
|
+
const [[, fontInit]] = patchPosts(fetchMock);
|
|
292
|
+
expect(JSON.parse(String(fontInit?.body)).fontFaceCss).toContain("Inter");
|
|
293
|
+
expect(patchPosts(fetchMock)).toHaveLength(2);
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
it("counts a save on the edited element's document until it settles", async () => {
|
|
297
|
+
stubServer(CHANGED);
|
|
298
|
+
const element = document.createElement("div");
|
|
299
|
+
const { persist } = renderPersist();
|
|
300
|
+
const before = studioManualEditSavesIn(document);
|
|
301
|
+
|
|
302
|
+
const saving = persist({ ...selection, element }, OPERATIONS);
|
|
303
|
+
expect(studioManualEditSavesIn(document)).toBe(before + 1);
|
|
304
|
+
await saving;
|
|
305
|
+
|
|
306
|
+
expect(studioManualEditSavesIn(document)).toBe(before + 2);
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
it("does nothing when shouldSave says no", async () => {
|
|
310
|
+
const fetchMock = stubServer({ changed: true });
|
|
311
|
+
const { persist } = renderPersist();
|
|
312
|
+
|
|
313
|
+
await expect(persist(selection, OPERATIONS, { shouldSave: () => false })).resolves.toBe(
|
|
314
|
+
undefined,
|
|
315
|
+
);
|
|
316
|
+
expect(fetchMock).not.toHaveBeenCalled();
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
it("refuses to save once the active project has changed", async () => {
|
|
320
|
+
stubServer({ changed: true });
|
|
321
|
+
const projectIdRef = { current: "p1" as string | null };
|
|
322
|
+
const { persist, params } = renderPersist({
|
|
323
|
+
projectIdRef,
|
|
324
|
+
queueDomEditSave: async (save) => {
|
|
325
|
+
projectIdRef.current = "p2";
|
|
326
|
+
return save();
|
|
327
|
+
},
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
await expect(persist(selection, OPERATIONS)).rejects.toThrow(
|
|
331
|
+
"Active project changed before the edit could be saved",
|
|
332
|
+
);
|
|
333
|
+
expect(params.editHistory.recordEdit).not.toHaveBeenCalled();
|
|
334
|
+
});
|
|
335
|
+
});
|
|
@@ -10,6 +10,7 @@ import type { DomEditSelection } from "../components/editor/domEditingTypes";
|
|
|
10
10
|
import type { TimelineElement } from "../player";
|
|
11
11
|
import type { UseDomEditSessionParams } from "./useDomEditSession";
|
|
12
12
|
import { makeSelection } from "./domSelectionTestHarness";
|
|
13
|
+
import { readTranslatePx } from "../components/editor/plainTranslate";
|
|
13
14
|
|
|
14
15
|
const styleOp = (property: string, value: string): PatchOperation => ({
|
|
15
16
|
type: "inline-style",
|
|
@@ -66,6 +67,7 @@ const groupSelectionSpy = vi.fn();
|
|
|
66
67
|
const gsapCommitMutation = Object.assign(vi.fn(), { batch: vi.fn() });
|
|
67
68
|
const neverParsed = new Promise<{ animations: GsapAnimation[] }>(() => undefined);
|
|
68
69
|
const parsedFile = { current: neverParsed };
|
|
70
|
+
const groupCommitSpy = vi.fn(async () => {});
|
|
69
71
|
|
|
70
72
|
function createSessionParams(
|
|
71
73
|
overrides: Partial<UseDomEditSessionParams> = {},
|
|
@@ -256,7 +258,7 @@ vi.mock("./usePreviewInteraction", () => ({
|
|
|
256
258
|
vi.mock("./useGsapAwareEditing", () => ({
|
|
257
259
|
useGsapAwareEditing: () => ({
|
|
258
260
|
handleGsapAwarePathOffsetCommit: vi.fn(),
|
|
259
|
-
handleGsapAwareGroupPathOffsetCommit:
|
|
261
|
+
handleGsapAwareGroupPathOffsetCommit: groupCommitSpy,
|
|
260
262
|
handleGsapAwareBoxSizeCommit: vi.fn(),
|
|
261
263
|
handleGsapAwareRotationCommit: vi.fn(),
|
|
262
264
|
commitAnimatedProperty: vi.fn(),
|
|
@@ -605,3 +607,30 @@ describe("a shadow reload promoted without a host re-render", () => {
|
|
|
605
607
|
act(() => root.unmount());
|
|
606
608
|
});
|
|
607
609
|
});
|
|
610
|
+
|
|
611
|
+
describe("handleDomGroupMoveBy", () => {
|
|
612
|
+
it("saves through the group drag's commit, leaving a refusal for the host to explain", async () => {
|
|
613
|
+
const { useDomEditSession } = await import("./useDomEditSession");
|
|
614
|
+
const element = document.createElement("div");
|
|
615
|
+
document.body.append(element);
|
|
616
|
+
element.getBoundingClientRect = () => {
|
|
617
|
+
const { x, y } = readTranslatePx(element);
|
|
618
|
+
return new DOMRect(x, y, 40, 20);
|
|
619
|
+
};
|
|
620
|
+
const selection = makeSelection("card", element);
|
|
621
|
+
let moveBy!: ReturnType<typeof useDomEditSession>["handleDomGroupMoveBy"];
|
|
622
|
+
function Probe() {
|
|
623
|
+
moveBy = useDomEditSession(createSessionParams()).handleDomGroupMoveBy;
|
|
624
|
+
return null;
|
|
625
|
+
}
|
|
626
|
+
const root = createRoot(document.createElement("div"));
|
|
627
|
+
act(() => root.render(<Probe />));
|
|
628
|
+
await act(() => moveBy([{ selection, delta: { x: 10, y: 0 } }]));
|
|
629
|
+
expect(groupCommitSpy).toHaveBeenCalledWith(
|
|
630
|
+
[{ selection, next: { x: 10, y: 0 }, plainTranslate: true }],
|
|
631
|
+
{ refusalToast: false },
|
|
632
|
+
);
|
|
633
|
+
act(() => root.unmount());
|
|
634
|
+
element.remove();
|
|
635
|
+
});
|
|
636
|
+
});
|
|
@@ -25,6 +25,7 @@ import { membersForDelete, timelineElementsForDelete } from "./domEditDeleteMemb
|
|
|
25
25
|
import type { RecordEditInput } from "./domEditDeleteMembers";
|
|
26
26
|
import type { DomEditTimelineParams } from "./useDomSelectionTypes";
|
|
27
27
|
import { useLivePreviewIframe } from "./useLivePreviewIframe";
|
|
28
|
+
import { moveDomGroupBy, type DomGroupMove } from "../components/editor/domGroupMoveBy";
|
|
28
29
|
// Re-exported: the delete rule lives in its own module; callers and its test import it from here.
|
|
29
30
|
export { membersForDelete };
|
|
30
31
|
|
|
@@ -445,6 +446,7 @@ export function useDomEditSession({
|
|
|
445
446
|
domEditSelection,
|
|
446
447
|
selectedGsapAnimations,
|
|
447
448
|
gsapCommitMutation,
|
|
449
|
+
activeCompPath,
|
|
448
450
|
previewIframeRef,
|
|
449
451
|
showToast,
|
|
450
452
|
bumpGsapCache,
|
|
@@ -459,6 +461,13 @@ export function useDomEditSession({
|
|
|
459
461
|
setArcPath,
|
|
460
462
|
updateArcSegment,
|
|
461
463
|
});
|
|
464
|
+
const handleDomGroupMoveBy = useCallback(
|
|
465
|
+
(moves: DomGroupMove[]) =>
|
|
466
|
+
moveDomGroupBy(moves, (updates) =>
|
|
467
|
+
handleGsapAwareGroupPathOffsetCommit(updates, { refusalToast: false }),
|
|
468
|
+
),
|
|
469
|
+
[handleGsapAwareGroupPathOffsetCommit],
|
|
470
|
+
);
|
|
462
471
|
const { handleUpdateSegmentEase, handleUpdateKeyframeEase, handleSetAllKeyframeEases } =
|
|
463
472
|
useKeyframeEaseCommits({ gsapCommitMutation, domEditSelectionRef });
|
|
464
473
|
const committable = useCommitPreflightCapabilities({
|
|
@@ -497,6 +506,7 @@ export function useDomEditSession({
|
|
|
497
506
|
handleDomAttributeBatchCommit,
|
|
498
507
|
handleDomPathOffsetCommit: handleGsapAwarePathOffsetCommit,
|
|
499
508
|
handleDomGroupPathOffsetCommit: handleGsapAwareGroupPathOffsetCommit,
|
|
509
|
+
handleDomGroupMoveBy,
|
|
500
510
|
handleDomZIndexReorderCommit,
|
|
501
511
|
handleDomBoxSizeCommit: handleGsapAwareBoxSizeCommit,
|
|
502
512
|
handleDomRotationCommit: handleGsapAwareRotationCommit,
|
|
@@ -350,6 +350,29 @@ describe("useDomEditTextCommits", () => {
|
|
|
350
350
|
expect(outcome).toEqual({ ok: true, persistence });
|
|
351
351
|
});
|
|
352
352
|
|
|
353
|
+
it("saves a style map on a selection as one patch, so it is one undo step", async () => {
|
|
354
|
+
const { iframe, element } = previewElement("<div id='card'>Original</div>", "card");
|
|
355
|
+
const persist = vi.fn().mockResolvedValue({ sourceFile: "index.html", changed: true });
|
|
356
|
+
const hook = renderTextCommitHook(
|
|
357
|
+
commitParams({
|
|
358
|
+
previewIframeRef: { current: iframe },
|
|
359
|
+
domEditSelection: null,
|
|
360
|
+
persistDomEditOperations: persist,
|
|
361
|
+
}),
|
|
362
|
+
);
|
|
363
|
+
|
|
364
|
+
await act(async () => {
|
|
365
|
+
await hook.handleDomStyleCommitForSelection(selectionFor(element), {
|
|
366
|
+
"border-width": "4px",
|
|
367
|
+
"border-style": "solid",
|
|
368
|
+
});
|
|
369
|
+
});
|
|
370
|
+
|
|
371
|
+
expect(persist).toHaveBeenCalledTimes(1);
|
|
372
|
+
expect(element.style.borderWidth).toBe("4px");
|
|
373
|
+
expect(element.style.borderStyle).toBe("solid");
|
|
374
|
+
});
|
|
375
|
+
|
|
353
376
|
it("declines a style commit with no selection, without reaching the writer", async () => {
|
|
354
377
|
const persistDomEditOperations = vi.fn().mockResolvedValue(undefined);
|
|
355
378
|
const hook = renderTextCommitHook(
|
|
@@ -109,7 +109,11 @@ export function useDomEditTextCommits({
|
|
|
109
109
|
});
|
|
110
110
|
|
|
111
111
|
const handleDomStyleCommitForSelection = useCallback(
|
|
112
|
-
(
|
|
112
|
+
(
|
|
113
|
+
selection: DomEditSelection,
|
|
114
|
+
propertyOrStylesAsOnePatch: string | Record<string, string>,
|
|
115
|
+
value = "",
|
|
116
|
+
): Promise<DomEditCommitOutcome> =>
|
|
113
117
|
commitDomStyles(
|
|
114
118
|
{
|
|
115
119
|
activeCompPath,
|
|
@@ -121,7 +125,9 @@ export function useDomEditTextCommits({
|
|
|
121
125
|
resync: refreshDomEditSelectionFromPreview,
|
|
122
126
|
},
|
|
123
127
|
selection,
|
|
124
|
-
|
|
128
|
+
typeof propertyOrStylesAsOnePatch === "string"
|
|
129
|
+
? { [propertyOrStylesAsOnePatch]: value }
|
|
130
|
+
: propertyOrStylesAsOnePatch,
|
|
125
131
|
),
|
|
126
132
|
[
|
|
127
133
|
activeCompPath,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// @vitest-environment happy-dom
|
|
2
2
|
import { act, createElement, useRef } from "react";
|
|
3
3
|
import { createRoot } from "react-dom/client";
|
|
4
|
-
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
4
|
+
import { afterEach, beforeEach, describe, expect, expectTypeOf, it, vi } from "vitest";
|
|
5
5
|
import {
|
|
6
6
|
useDomGeometryCommit,
|
|
7
7
|
usePlayerStore,
|
|
@@ -108,6 +108,10 @@ afterEach(() => {
|
|
|
108
108
|
});
|
|
109
109
|
|
|
110
110
|
describe("useDomGeometryCommit, from the package entry", () => {
|
|
111
|
+
it("takes the host's own preview reload, with no fallback", () => {
|
|
112
|
+
expectTypeOf<UseDomGeometryCommitOptions["reloadPreview"]>().toEqualTypeOf<() => void>();
|
|
113
|
+
});
|
|
114
|
+
|
|
111
115
|
it("plugs into DomEditOverlay and saves a move as one GSAP write and one undo step", async () => {
|
|
112
116
|
const mutations = stubServer();
|
|
113
117
|
const { element, recordEdit, hook, unmount } = renderHost();
|
|
@@ -26,8 +26,8 @@ import { useMountEffect } from "./useMountEffect";
|
|
|
26
26
|
* mounts; drain `waitForPendingSaves` before switching projects.
|
|
27
27
|
*/
|
|
28
28
|
export interface UseDomGeometryCommitOptions extends UseDomStyleCommitOptions {
|
|
29
|
-
/**
|
|
30
|
-
reloadPreview
|
|
29
|
+
/** Reloads the host's preview when a save cannot patch it in place. */
|
|
30
|
+
reloadPreview: () => void;
|
|
31
31
|
}
|
|
32
32
|
|
|
33
33
|
export interface DomGeometryCommits {
|
|
@@ -42,6 +42,7 @@ export interface DomGeometryCommits {
|
|
|
42
42
|
next: { width: number; height: number },
|
|
43
43
|
offset?: { x: number; y: number },
|
|
44
44
|
restore?: () => void,
|
|
45
|
+
route?: { plainTranslate: boolean },
|
|
45
46
|
) => Promise<DomEditCommitOutcome>;
|
|
46
47
|
commitRotation: (
|
|
47
48
|
selection: DomEditSelection,
|
|
@@ -70,17 +71,13 @@ export function useDomGeometryCommit({
|
|
|
70
71
|
projectIdRef.current = projectId;
|
|
71
72
|
const pending = useRef(new Set<Promise<GeometryCommitResult | undefined>>()).current;
|
|
72
73
|
const editHistory = useMemo(() => ({ recordEdit }), [recordEdit]);
|
|
73
|
-
const reload = useCallback(
|
|
74
|
-
() => (reloadPreview ? reloadPreview() : iframeRef.current?.contentWindow?.location.reload()),
|
|
75
|
-
[reloadPreview, iframeRef],
|
|
76
|
-
);
|
|
77
74
|
const { bump: bumpGsapCache } = useGsapCacheVersion();
|
|
78
75
|
const gsap = useGsapScriptCommits({
|
|
79
76
|
projectIdRef,
|
|
80
77
|
activeCompPath,
|
|
81
78
|
previewIframeRef: iframeRef,
|
|
82
79
|
editHistory,
|
|
83
|
-
reloadPreview
|
|
80
|
+
reloadPreview,
|
|
84
81
|
onCacheInvalidate: bumpGsapCache,
|
|
85
82
|
showToast,
|
|
86
83
|
writeProjectFile,
|
|
@@ -128,6 +125,7 @@ export function useDomGeometryCommit({
|
|
|
128
125
|
domEditSelection: null,
|
|
129
126
|
selectedGsapAnimations: NO_SELECTED_ANIMATIONS,
|
|
130
127
|
gsapCommitMutation: gsap.commitMutation,
|
|
128
|
+
activeCompPath,
|
|
131
129
|
previewIframeRef: iframeRef,
|
|
132
130
|
showToast,
|
|
133
131
|
bumpGsapCache,
|
|
@@ -173,8 +171,8 @@ export function useDomGeometryCommit({
|
|
|
173
171
|
saved(() => handleGsapAwarePathOffsetCommit(selection, next, modifiers)),
|
|
174
172
|
commitGroupPathOffset: (updates) =>
|
|
175
173
|
saved(() => handleGsapAwareGroupPathOffsetCommit(updates)),
|
|
176
|
-
commitBoxSize: (selection, next, offset, restore) =>
|
|
177
|
-
saved(() => handleGsapAwareBoxSizeCommit(selection, next, offset, restore), restore),
|
|
174
|
+
commitBoxSize: (selection, next, offset, restore, route) =>
|
|
175
|
+
saved(() => handleGsapAwareBoxSizeCommit(selection, next, offset, restore, route), restore),
|
|
178
176
|
commitRotation: (selection, next) =>
|
|
179
177
|
saved(() => handleGsapAwareRotationCommit(selection, next)),
|
|
180
178
|
waitForPendingSaves: () => Promise.allSettled([...pending]).then(noop),
|