@hyperframes/studio 0.8.75 → 0.8.77
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-BSDTJ9EF.js +464 -0
- package/dist/assets/{index-BlUYw1MB.js → index-2GgHjQq-.js} +163 -163
- package/dist/assets/{index-0w6iIlwZ.css → index-BWJqxeaH.css} +1 -1
- package/dist/assets/{index-BiFKcNjs.js → index-CsFCRdMo.js} +1 -1
- package/dist/assets/{index-HjexPsny.js → index-DcxWQ7Za.js} +1 -1
- package/dist/{chunk-7BP32G62.js → chunk-5VANA2TG.js} +12 -1
- package/dist/chunk-5VANA2TG.js.map +1 -0
- package/dist/{domEditingLayers-UKMVYXT6.js → domEditingLayers-IGPLTI5E.js} +4 -2
- package/dist/index.d.ts +34 -69
- package/dist/index.html +2 -2
- package/dist/index.js +5287 -5277
- package/dist/index.js.map +1 -1
- package/dist/styles.css +1 -1
- package/package.json +7 -7
- package/src/components/StudioRightPanels.types.ts +0 -2
- package/src/components/editor/DomEditOverlay.test.ts +5 -0
- package/src/components/editor/LayersPanel.tsx +3 -14
- package/src/components/editor/PreviewOverlayProvider.test.tsx +6 -1
- package/src/components/editor/domEditing.ts +1 -0
- package/src/components/editor/domEditingElement.ts +13 -0
- package/src/components/editor/domEditingLayers.test.ts +27 -0
- package/src/components/editor/domEditingLayers.ts +9 -0
- package/src/components/editor/gsapLivePreview.test.ts +63 -0
- package/src/components/editor/gsapLivePreview.ts +2 -14
- package/src/components/editor/offCanvasIndicatorRefresh.test.tsx +57 -35
- package/src/components/editor/overlayFrameLoop.test.ts +16 -0
- package/src/components/editor/overlayFrameLoop.ts +11 -0
- package/src/components/editor/propertyPanelHelpers.ts +4 -6
- package/src/components/editor/usePreviewCompositionRect.test.tsx +6 -1
- package/src/components/nle/NLEPreview.test.ts +202 -22
- package/src/components/nle/NLEPreview.tsx +65 -46
- package/src/components/nle/PreviewPoster.tsx +99 -0
- package/src/components/nle/PreviewZoomOverlay.tsx +111 -0
- package/src/components/nle/previewZoom.test.ts +42 -0
- package/src/components/nle/previewZoom.ts +33 -0
- package/src/components/nle/useCanvasZOrderTimelineMirror.test.tsx +30 -49
- package/src/components/panels/VariablesPanel.tsx +0 -2
- package/src/components/sidebar/CompositionsTab.drag.test.tsx +5 -1
- package/src/components/sidebar/CompositionsTab.rootBadge.test.tsx +14 -0
- package/src/components/sidebar/CompositionsTab.test.ts +4 -6
- package/src/components/sidebar/CompositionsTab.tsx +28 -21
- package/src/components/studioColorGradingScope.ts +0 -3
- package/src/hooks/domEditDeleteMembers.ts +0 -2
- package/src/hooks/gsapScriptCommitTypes.ts +0 -2
- package/src/hooks/keyframeCacheAstLoad.test.ts +31 -1
- package/src/hooks/keyframeCacheAstLoad.ts +2 -1
- package/src/hooks/timelineAudioGroupCreate.ts +0 -1
- package/src/hooks/timelineEditingHelpers.ts +0 -3
- package/src/hooks/timelineTimingSync.ts +0 -1
- package/src/hooks/timelineTrackVisibility.ts +0 -1
- package/src/hooks/useBlockHandlers.ts +0 -2
- package/src/hooks/useClipboard.test.ts +69 -10
- package/src/hooks/useClipboard.ts +28 -19
- package/src/hooks/useDomEditCommits.test.tsx +0 -1
- package/src/hooks/useDomEditCommits.ts +0 -4
- package/src/hooks/useDomEditWiring.ts +1 -3
- package/src/hooks/useEditHistoryActions.test.tsx +31 -14
- package/src/hooks/useEditHistoryActions.ts +12 -7
- package/src/hooks/useEditorSave.ts +0 -3
- package/src/hooks/useElementLifecycleOps.ts +0 -1
- package/src/hooks/useEnableKeyframes.test.ts +9 -1
- package/src/hooks/useFileManager.ts +0 -2
- package/src/hooks/useGestureRecording.test.tsx +1 -1
- package/src/hooks/useGestureRecording.ts +6 -10
- package/src/hooks/useGroupCommits.ts +0 -1
- package/src/hooks/useGsapScriptCommits.ts +0 -1
- package/src/hooks/useGsapTweenCache.element.test.tsx +39 -0
- package/src/hooks/useGsapTweenCache.ts +5 -13
- package/src/hooks/useLintModal.test.tsx +34 -1
- package/src/hooks/useLintModal.ts +32 -30
- package/src/hooks/usePersistentEditHistory.test.ts +221 -315
- package/src/hooks/usePersistentEditHistory.ts +155 -342
- package/src/hooks/usePopulateKeyframeCacheForFile.test.tsx +2 -0
- package/src/hooks/usePreviewPersistence.ts +0 -2
- package/src/hooks/useProjectCompositionVariables.ts +1 -3
- package/src/hooks/useRazorSplit.history.test.tsx +46 -67
- package/src/hooks/useRazorSplit.test.ts +17 -47
- package/src/hooks/useRenderClipContent.test.ts +29 -0
- package/src/hooks/useRenderClipContent.ts +6 -3
- package/src/hooks/useSdkSession.lifecycle.test.tsx +34 -4
- package/src/hooks/useSdkSession.ts +8 -2
- package/src/hooks/useSlideshowPersist.ts +0 -2
- package/src/hooks/useStudioContextValue.ts +6 -1
- package/src/hooks/useTimelineAssetDropOps.ts +0 -1
- package/src/hooks/useTimelineDeleteOps.ts +0 -1
- package/src/hooks/useTimelineEditing.test.tsx +0 -1
- package/src/hooks/useTimelineEditingTypes.ts +0 -2
- package/src/hostMount.test.tsx +53 -22
- package/src/main.tsx +3 -0
- package/src/player/components/CompositionThumbnail.tsx +14 -0
- package/src/player/components/Player.test.ts +49 -0
- package/src/player/components/Player.tsx +21 -7
- package/src/player/components/timelineClipDragCommit.test.ts +7 -39
- package/src/player/hooks/timelineDomBinding.test.ts +50 -0
- package/src/player/hooks/timelineSyncHydration.test.ts +25 -1
- package/src/player/hooks/timelineSyncHydration.ts +3 -3
- package/src/player/hooks/useShadowPreviewReload.ts +1 -0
- package/src/player/hooks/useTimelinePlayer.play.test.ts +120 -0
- package/src/player/hooks/useTimelinePlayer.ts +1 -0
- package/src/player/hooks/useTimelineSyncCallbacks.ts +2 -0
- package/src/player/lib/automationStoreSync.test.ts +24 -0
- package/src/player/lib/automationStoreSync.ts +3 -9
- package/src/player/lib/playbackScrub.ts +1 -1
- package/src/player/lib/timelineDOM.test.ts +28 -0
- package/src/player/lib/timelineDOM.ts +4 -2
- package/src/player/lib/timelineElementHelpers.ts +78 -9
- package/src/player/lib/timelineIframeHelpers.test.ts +49 -5
- package/src/player/lib/timelineIframeHelpers.ts +13 -12
- package/src/player/store/playerStore.test.ts +35 -1
- package/src/player/store/playerStore.ts +29 -0
- package/src/player/store/readinessSlice.ts +34 -6
- package/src/utils/blockInstaller.ts +0 -3
- package/src/utils/previewPrefetch.test.ts +22 -0
- package/src/utils/previewPrefetch.ts +14 -0
- package/src/utils/razorSplitTransaction.test.ts +0 -1
- package/src/utils/razorSplitTransaction.ts +1 -1
- package/src/utils/sdkEditTransaction.ts +0 -3
- package/src/utils/studioFileHistory.test.ts +0 -9
- package/src/utils/studioFileHistory.ts +1 -5
- package/src/utils/studioUiPreferences.test.ts +9 -3
- package/src/utils/studioUiPreferences.ts +0 -20
- package/src/utils/timelineCompositionInsert.ts +0 -1
- package/dist/assets/hyperframes-player-WbBVbWuO.js +0 -464
- package/dist/chunk-7BP32G62.js.map +0 -1
- package/src/hooks/usePersistentEditHistory.projectOwnership.test.tsx +0 -60
- package/src/utils/editHistory.test.ts +0 -534
- package/src/utils/editHistory.ts +0 -236
- package/src/utils/editHistoryStorage.test.ts +0 -37
- package/src/utils/editHistoryStorage.ts +0 -99
- /package/dist/{domEditingLayers-UKMVYXT6.js.map → domEditingLayers-IGPLTI5E.js.map} +0 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hyperframes/studio",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.77",
|
|
4
4
|
"description": "",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -52,11 +52,11 @@
|
|
|
52
52
|
"dockview-react": "8.3.1",
|
|
53
53
|
"gsap": "^3.13.0",
|
|
54
54
|
"mediabunny": "^1.45.3",
|
|
55
|
-
"@hyperframes/core": "0.8.
|
|
56
|
-
"@hyperframes/
|
|
57
|
-
"@hyperframes/
|
|
58
|
-
"@hyperframes/
|
|
59
|
-
"@hyperframes/
|
|
55
|
+
"@hyperframes/core": "0.8.77",
|
|
56
|
+
"@hyperframes/parsers": "0.8.77",
|
|
57
|
+
"@hyperframes/studio-server": "0.8.77",
|
|
58
|
+
"@hyperframes/player": "0.8.77",
|
|
59
|
+
"@hyperframes/sdk": "0.8.77"
|
|
60
60
|
},
|
|
61
61
|
"devDependencies": {
|
|
62
62
|
"@tailwindcss/vite": "^4.3.3",
|
|
@@ -72,7 +72,7 @@
|
|
|
72
72
|
"vite": "^6.4.2",
|
|
73
73
|
"vitest": "^4.1.11",
|
|
74
74
|
"zustand": "^5.0.0",
|
|
75
|
-
"@hyperframes/producer": "0.8.
|
|
75
|
+
"@hyperframes/producer": "0.8.77"
|
|
76
76
|
},
|
|
77
77
|
"peerDependencies": {
|
|
78
78
|
"react": "19",
|
|
@@ -9,7 +9,6 @@
|
|
|
9
9
|
import type { StudioEditPersistenceProps } from "./panels/VariablesPanel";
|
|
10
10
|
import type { BlockParam } from "@hyperframes/core/registry";
|
|
11
11
|
import type { Composition } from "@hyperframes/sdk";
|
|
12
|
-
import type { EditHistoryKind } from "../utils/editHistory";
|
|
13
12
|
import type { UseSlideshowPersistParams } from "../hooks/useSlideshowPersist";
|
|
14
13
|
import type { AddMediaOverlayHandler } from "./editor/propertyPanelTypes";
|
|
15
14
|
import type { ToggleHiddenHandler } from "../utils/studioHelpers";
|
|
@@ -46,7 +45,6 @@ export interface StudioRightPanelsProps extends StudioEditPersistenceProps {
|
|
|
46
45
|
reloadPreview: () => void;
|
|
47
46
|
recordEdit: (entry: {
|
|
48
47
|
label: string;
|
|
49
|
-
kind: EditHistoryKind;
|
|
50
48
|
files: Record<string, { before: string; after: string }>;
|
|
51
49
|
}) => Promise<void>;
|
|
52
50
|
onToggleElementHidden?: ToggleHiddenHandler;
|
|
@@ -18,6 +18,7 @@ import {
|
|
|
18
18
|
hoverCacheDescribesPoint,
|
|
19
19
|
resolveResizeCenterAnchorOffset,
|
|
20
20
|
} from "./domEditOverlayGestures";
|
|
21
|
+
import { usePlayerStore } from "../../player/store/playerStore";
|
|
21
22
|
|
|
22
23
|
// React 19 warns unless the test environment opts into act().
|
|
23
24
|
globalThis.IS_REACT_ACT_ENVIRONMENT = true;
|
|
@@ -242,6 +243,10 @@ function dispatchOverlayPointerDown(target: Element, clientX = 120, clientY = 80
|
|
|
242
243
|
});
|
|
243
244
|
}
|
|
244
245
|
|
|
246
|
+
beforeEach(() => {
|
|
247
|
+
usePlayerStore.setState({ previewBooted: true });
|
|
248
|
+
});
|
|
249
|
+
|
|
245
250
|
describe("focusDomEditOverlayElement", () => {
|
|
246
251
|
it("focuses the canvas overlay without scrolling", () => {
|
|
247
252
|
const calls: Array<FocusOptions | undefined> = [];
|
|
@@ -3,6 +3,7 @@ import { memo, useState, useCallback, useEffect, useRef } from "react";
|
|
|
3
3
|
import {
|
|
4
4
|
collectDomEditLayerItems,
|
|
5
5
|
getDomEditLayerKey,
|
|
6
|
+
liveLayerElement,
|
|
6
7
|
resolveDomEditSelection,
|
|
7
8
|
type DomEditLayerItem,
|
|
8
9
|
} from "./domEditing";
|
|
@@ -190,20 +191,8 @@ export const LayersPanel = memo(function LayersPanel() {
|
|
|
190
191
|
|
|
191
192
|
const resolveSelection = useCallback(
|
|
192
193
|
(layer: DomEditLayerItem) => {
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
let el = layer.element;
|
|
196
|
-
if (!el.isConnected) {
|
|
197
|
-
const iframe = previewIframeRef.current;
|
|
198
|
-
const doc = iframe?.contentDocument;
|
|
199
|
-
if (doc) {
|
|
200
|
-
const found =
|
|
201
|
-
(layer.id ? doc.getElementById(layer.id) : null) ??
|
|
202
|
-
(layer.hfId ? doc.querySelector(`[data-hf-id="${CSS.escape(layer.hfId)}"]`) : null) ??
|
|
203
|
-
doc.getElementById(layer.key);
|
|
204
|
-
if (found instanceof HTMLElement) el = found;
|
|
205
|
-
}
|
|
206
|
-
}
|
|
194
|
+
const doc = previewIframeRef.current?.contentDocument;
|
|
195
|
+
const el = liveLayerElement(layer, doc, activeCompPath);
|
|
207
196
|
return resolveDomEditSelection(el, {
|
|
208
197
|
activeCompositionPath: activeCompPath,
|
|
209
198
|
isMasterView,
|
|
@@ -2,10 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
import { act } from "react";
|
|
4
4
|
import { createRoot } from "react-dom/client";
|
|
5
|
-
import { afterEach, describe, expect, it } from "vitest";
|
|
5
|
+
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
|
6
6
|
import { GridOverlay } from "./GridOverlay";
|
|
7
7
|
import { PreviewOverlayProvider } from "./PreviewOverlayProvider";
|
|
8
8
|
import { usePreviewIframeStore } from "../../player/store/previewIframeStore";
|
|
9
|
+
import { usePlayerStore } from "../../player/store/playerStore";
|
|
9
10
|
|
|
10
11
|
(globalThis as unknown as { IS_REACT_ACT_ENVIRONMENT: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
|
|
11
12
|
|
|
@@ -27,6 +28,10 @@ function compositionFrame() {
|
|
|
27
28
|
return iframe;
|
|
28
29
|
}
|
|
29
30
|
|
|
31
|
+
beforeEach(() => {
|
|
32
|
+
usePlayerStore.setState({ previewBooted: true });
|
|
33
|
+
});
|
|
34
|
+
|
|
30
35
|
describe("PreviewOverlayProvider", () => {
|
|
31
36
|
it("derives grid geometry after the iframe prop becomes available", async () => {
|
|
32
37
|
window.localStorage.setItem(
|
|
@@ -314,6 +314,19 @@ export function findElementForSelection(
|
|
|
314
314
|
return findAll(selection.selector)[selection.selectorIndex ?? 0] ?? null;
|
|
315
315
|
}
|
|
316
316
|
|
|
317
|
+
// The selected node while mounted, else its copy in the selection's own file (ids repeat across files), else any copy.
|
|
318
|
+
export function findPreviewNode(
|
|
319
|
+
doc: Document | null | undefined,
|
|
320
|
+
el: DomEditSelection,
|
|
321
|
+
): Element | null {
|
|
322
|
+
if (!doc) return null;
|
|
323
|
+
if (el.element?.isConnected && el.element.ownerDocument === doc) return el.element;
|
|
324
|
+
return (
|
|
325
|
+
findElementForSelection(doc, el) ??
|
|
326
|
+
findElementForSelection(doc, { ...el, sourceFile: undefined })
|
|
327
|
+
);
|
|
328
|
+
}
|
|
329
|
+
|
|
317
330
|
// fallow-ignore-next-line complexity
|
|
318
331
|
export function findElementForTimelineElement(
|
|
319
332
|
doc: Document,
|
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
buildDomEditPatchTarget,
|
|
7
7
|
buildTextFieldChildLocator,
|
|
8
8
|
readHfId,
|
|
9
|
+
liveLayerElement,
|
|
9
10
|
} from "./domEditingLayers";
|
|
10
11
|
import type { DomEditTextField } from "./domEditingTypes";
|
|
11
12
|
|
|
@@ -305,3 +306,29 @@ describe("collectDomEditLayerItems selector-index cost", () => {
|
|
|
305
306
|
expect(classSelectorQueries(12)).toBe(1);
|
|
306
307
|
});
|
|
307
308
|
});
|
|
309
|
+
|
|
310
|
+
describe("liveLayerElement", () => {
|
|
311
|
+
it("finds a replaced layer again in its own file when a sub-composition repeats its id", () => {
|
|
312
|
+
document.body.innerHTML =
|
|
313
|
+
'<div data-composition-id="main">' +
|
|
314
|
+
'<div data-composition-id="strip" data-composition-src="compositions/strip.html">' +
|
|
315
|
+
'<div id="card-1">strip</div></div><div id="card-1">root</div></div>';
|
|
316
|
+
const stale = document.createElement("div");
|
|
317
|
+
const layer = {
|
|
318
|
+
key: "index.html:card-1:0",
|
|
319
|
+
element: stale,
|
|
320
|
+
label: "card-1",
|
|
321
|
+
tagName: "div",
|
|
322
|
+
depth: 0,
|
|
323
|
+
childCount: 0,
|
|
324
|
+
id: "card-1",
|
|
325
|
+
sourceFile: "index.html",
|
|
326
|
+
};
|
|
327
|
+
|
|
328
|
+
expect(liveLayerElement(layer, document, "index.html").textContent).toBe("root");
|
|
329
|
+
expect(
|
|
330
|
+
liveLayerElement({ ...layer, sourceFile: "compositions/strip.html" }, document, "index.html")
|
|
331
|
+
.textContent,
|
|
332
|
+
).toBe("strip");
|
|
333
|
+
});
|
|
334
|
+
});
|
|
@@ -491,6 +491,15 @@ export function collectDomEditLayerItems(
|
|
|
491
491
|
return items;
|
|
492
492
|
}
|
|
493
493
|
|
|
494
|
+
export function liveLayerElement(
|
|
495
|
+
layer: DomEditLayerItem,
|
|
496
|
+
doc: Document | null | undefined,
|
|
497
|
+
activeCompositionPath: string | null,
|
|
498
|
+
): HTMLElement {
|
|
499
|
+
if (layer.element.isConnected || !doc) return layer.element;
|
|
500
|
+
return findElementForSelection(doc, layer, activeCompositionPath) ?? layer.element;
|
|
501
|
+
}
|
|
502
|
+
|
|
494
503
|
// ─── Patch operations ────────────────────────────────────────────────────────
|
|
495
504
|
|
|
496
505
|
export function buildDomEditStylePatchOperation(
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
// @vitest-environment happy-dom
|
|
2
|
+
import { expect, it, vi } from "vitest";
|
|
3
|
+
import { createGsapLivePreview } from "./gsapLivePreview";
|
|
4
|
+
import type { DomEditSelection } from "./domEditingTypes";
|
|
5
|
+
import type { GsapAnimation } from "@hyperframes/parsers/gsap-parser";
|
|
6
|
+
import { readGsapRuntimeValuesForPanel } from "./propertyPanelHelpers";
|
|
7
|
+
|
|
8
|
+
it("previews on the selected element, not an earlier same-id copy in a sub-composition", () => {
|
|
9
|
+
document.body.innerHTML =
|
|
10
|
+
'<div data-composition-id="strip"><div id="card" data-hf-id="hf-inner"></div></div>' +
|
|
11
|
+
'<div id="card" data-hf-id="hf-root"></div>';
|
|
12
|
+
const set = vi.fn();
|
|
13
|
+
const iframe = { contentWindow: { gsap: { set } }, contentDocument: document };
|
|
14
|
+
const preview = createGsapLivePreview({ current: iframe as unknown as HTMLIFrameElement });
|
|
15
|
+
preview({ id: "card", hfId: "hf-root" } as DomEditSelection, { x: 10 });
|
|
16
|
+
expect(set.mock.calls[0]?.[0]).toBe(document.querySelector('[data-hf-id="hf-root"]'));
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it("previews on the copy in the selection's own file when a sub-composition repeats its hf-id", () => {
|
|
20
|
+
document.body.innerHTML =
|
|
21
|
+
'<div data-composition-id="strip" data-composition-src="compositions/strip.html">' +
|
|
22
|
+
'<div id="card" data-hf-id="hf-card"></div></div><div id="card" data-hf-id="hf-card" class="root"></div>';
|
|
23
|
+
const set = vi.fn();
|
|
24
|
+
const iframe = { contentWindow: { gsap: { set } }, contentDocument: document };
|
|
25
|
+
const preview = createGsapLivePreview({ current: iframe as unknown as HTMLIFrameElement });
|
|
26
|
+
preview({ id: "card", hfId: "hf-card", sourceFile: "index.html" } as DomEditSelection, { x: 10 });
|
|
27
|
+
expect(set.mock.calls[0]?.[0]).toBe(document.querySelector(".root"));
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it("previews on the selected node while it is mounted, even in a second mount of the same sub-composition", () => {
|
|
31
|
+
document.body.innerHTML =
|
|
32
|
+
'<div data-composition-id="strip" data-composition-src="compositions/strip.html"><div id="card" data-hf-id="hf-card"></div></div>' +
|
|
33
|
+
'<div data-composition-id="strip" data-composition-src="compositions/strip.html"><div id="card" data-hf-id="hf-card"></div></div>';
|
|
34
|
+
const second = document.querySelectorAll<HTMLElement>("#card")[1];
|
|
35
|
+
const selection = {
|
|
36
|
+
id: "card",
|
|
37
|
+
hfId: "hf-card",
|
|
38
|
+
sourceFile: "compositions/strip.html",
|
|
39
|
+
element: second,
|
|
40
|
+
};
|
|
41
|
+
const set = vi.fn();
|
|
42
|
+
const iframe = { contentWindow: { gsap: { set } }, contentDocument: document };
|
|
43
|
+
createGsapLivePreview({ current: iframe as unknown as HTMLIFrameElement })(
|
|
44
|
+
selection as DomEditSelection,
|
|
45
|
+
{ x: 10 },
|
|
46
|
+
);
|
|
47
|
+
expect(set.mock.calls[0]?.[0]).toBe(second);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it("the panel reads GSAP values off the node the live preview moves", () => {
|
|
51
|
+
document.body.innerHTML =
|
|
52
|
+
'<div data-composition-id="strip" data-composition-src="compositions/strip.html"><div id="card"></div></div>' +
|
|
53
|
+
'<div id="card" class="root"></div>';
|
|
54
|
+
const root = document.querySelector<HTMLElement>(".root");
|
|
55
|
+
const getProperty = vi.fn(() => 5);
|
|
56
|
+
const iframe = { contentWindow: { gsap: { getProperty } }, contentDocument: document };
|
|
57
|
+
const selection = { id: "card", sourceFile: "index.html", element: root } as DomEditSelection;
|
|
58
|
+
const animations = [{ properties: { x: 5 } }] as unknown as GsapAnimation[];
|
|
59
|
+
readGsapRuntimeValuesForPanel("anim", animations, selection, {
|
|
60
|
+
current: iframe as unknown as HTMLIFrameElement,
|
|
61
|
+
});
|
|
62
|
+
expect(getProperty.mock.calls[0]?.[0]).toBe(root);
|
|
63
|
+
});
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { DomEditSelection } from "./domEditingTypes";
|
|
2
|
+
import { findPreviewNode } from "./domEditingElement";
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
5
|
* Build the "live preview" callback the 3D-transform sub-view fires while a
|
|
@@ -8,19 +9,6 @@ import type { DomEditSelection } from "./domEditingTypes";
|
|
|
8
9
|
* Extracted so the identical closure exists once — shared by the legacy
|
|
9
10
|
* PropertyPanel Layout section and the flat Layout group (PropertyPanelFlat).
|
|
10
11
|
*/
|
|
11
|
-
// Resolve by id when unique, otherwise by selector + selectorIndex — a bare
|
|
12
|
-
// querySelector(selector) always hits the FIRST match, so dragging on the
|
|
13
|
-
// second of two same-selector siblings would animate the wrong element.
|
|
14
|
-
function resolvePreviewNode(
|
|
15
|
-
doc: Document | null | undefined,
|
|
16
|
-
el: DomEditSelection,
|
|
17
|
-
): Element | null {
|
|
18
|
-
if (!doc) return null;
|
|
19
|
-
if (el.id) return doc.querySelector(`#${el.id}`);
|
|
20
|
-
if (!el.selector) return null;
|
|
21
|
-
return doc.querySelectorAll(el.selector)[el.selectorIndex ?? 0] ?? null;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
12
|
export function createGsapLivePreview(iframeRef: { readonly current: HTMLIFrameElement | null }) {
|
|
25
13
|
return (el: DomEditSelection, props: Record<string, number>) => {
|
|
26
14
|
const iframe = iframeRef.current;
|
|
@@ -28,7 +16,7 @@ export function createGsapLivePreview(iframeRef: { readonly current: HTMLIFrameE
|
|
|
28
16
|
| { gsap?: { set: (t: Element, v: Record<string, number>) => void } }
|
|
29
17
|
| null
|
|
30
18
|
| undefined;
|
|
31
|
-
const node =
|
|
19
|
+
const node = findPreviewNode(iframe?.contentDocument, el);
|
|
32
20
|
if (win?.gsap && node) win.gsap.set(node, props);
|
|
33
21
|
};
|
|
34
22
|
}
|
|
@@ -2,9 +2,10 @@
|
|
|
2
2
|
|
|
3
3
|
import React, { act } from "react";
|
|
4
4
|
import { createRoot, type Root } from "react-dom/client";
|
|
5
|
-
import { afterAll, beforeAll, describe, expect, it } from "vitest";
|
|
5
|
+
import { afterAll, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
|
|
6
6
|
import { DomEditOverlay } from "./DomEditOverlay";
|
|
7
7
|
import { RECOMPUTE_INTERVAL_MS } from "./offCanvasIndicatorRefresh";
|
|
8
|
+
import { usePlayerStore } from "../../player/store/playerStore";
|
|
8
9
|
|
|
9
10
|
Reflect.set(globalThis, "IS_REACT_ACT_ENVIRONMENT", true);
|
|
10
11
|
|
|
@@ -59,6 +60,30 @@ async function flushAnimationFrames(): Promise<void> {
|
|
|
59
60
|
});
|
|
60
61
|
}
|
|
61
62
|
|
|
63
|
+
// The refresh waits on MutationObserver delivery and then on its own throttle, so no fixed
|
|
64
|
+
// number of frames is guaranteed to be enough on a loaded runner. Flush until the indicator
|
|
65
|
+
// reaches the state under test.
|
|
66
|
+
async function untilIndicator(host: HTMLElement, check: (indicator: Element | null) => void) {
|
|
67
|
+
await vi.waitFor(
|
|
68
|
+
async () => {
|
|
69
|
+
await act(async () => {
|
|
70
|
+
await flushAnimationFrames();
|
|
71
|
+
});
|
|
72
|
+
check(host.querySelector(INDICATOR));
|
|
73
|
+
},
|
|
74
|
+
{ timeout: 4000, interval: 0 },
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Delivers each batch of mutation records `ms` late, as a busy runner can.
|
|
79
|
+
function lateObserver(Base: typeof MutationObserver, ms: number): typeof MutationObserver {
|
|
80
|
+
return class extends Base {
|
|
81
|
+
constructor(callback: MutationCallback) {
|
|
82
|
+
super((records, observer) => setTimeout(() => callback(records, observer), ms));
|
|
83
|
+
}
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
62
87
|
interface OverlayHarness {
|
|
63
88
|
host: HTMLElement;
|
|
64
89
|
movedElement: HTMLElement;
|
|
@@ -68,7 +93,7 @@ interface OverlayHarness {
|
|
|
68
93
|
// Mount DomEditOverlay over an iframe whose #headline sits at `initialLeft`, with a
|
|
69
94
|
// getBoundingClientRect stub that reads the element's live inline geometry (so a
|
|
70
95
|
// style mutation moves it) and reports the composition/overlay as 800x450 at origin.
|
|
71
|
-
function mountOverlayWithHeadline(initialLeft: number): OverlayHarness {
|
|
96
|
+
function mountOverlayWithHeadline(initialLeft: number, observerDelayMs = 0): OverlayHarness {
|
|
72
97
|
const originalGetBoundingClientRect = Element.prototype.getBoundingClientRect;
|
|
73
98
|
const host = document.createElement("div");
|
|
74
99
|
document.body.append(host);
|
|
@@ -85,6 +110,9 @@ function mountOverlayWithHeadline(initialLeft: number): OverlayHarness {
|
|
|
85
110
|
`;
|
|
86
111
|
const movedElement = doc.getElementById("headline");
|
|
87
112
|
if (!movedElement) throw new Error("Expected test element");
|
|
113
|
+
const view = doc.defaultView?.MutationObserver ? doc.defaultView : globalThis;
|
|
114
|
+
const RealObserver = view.MutationObserver;
|
|
115
|
+
if (observerDelayMs > 0) view.MutationObserver = lateObserver(RealObserver, observerDelayMs);
|
|
88
116
|
|
|
89
117
|
Element.prototype.getBoundingClientRect = function (): DOMRect {
|
|
90
118
|
if (this === movedElement) {
|
|
@@ -125,58 +153,52 @@ function mountOverlayWithHeadline(initialLeft: number): OverlayHarness {
|
|
|
125
153
|
cleanup: () => {
|
|
126
154
|
act(() => root.unmount());
|
|
127
155
|
Element.prototype.getBoundingClientRect = originalGetBoundingClientRect;
|
|
156
|
+
view.MutationObserver = RealObserver;
|
|
128
157
|
iframe.remove();
|
|
129
158
|
host.remove();
|
|
130
159
|
},
|
|
131
160
|
};
|
|
132
161
|
}
|
|
133
162
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
try {
|
|
138
|
-
await act(async () => {
|
|
139
|
-
await flushAnimationFrames();
|
|
140
|
-
});
|
|
141
|
-
expect(h.host.querySelector(INDICATOR)).toBeTruthy();
|
|
142
|
-
|
|
143
|
-
act(() => {
|
|
144
|
-
h.movedElement.style.left = "120px";
|
|
145
|
-
});
|
|
146
|
-
await act(async () => {
|
|
147
|
-
await Promise.resolve();
|
|
148
|
-
await flushAnimationFrames();
|
|
149
|
-
});
|
|
163
|
+
beforeEach(() => {
|
|
164
|
+
usePlayerStore.setState({ previewBooted: true });
|
|
165
|
+
});
|
|
150
166
|
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
167
|
+
describe("off-canvas indicator refresh", () => {
|
|
168
|
+
for (const [label, delayMs] of [
|
|
169
|
+
["", 0],
|
|
170
|
+
[" when mutation delivery lags past a fixed wait", 300],
|
|
171
|
+
] as const) {
|
|
172
|
+
it(`removes the indicator when an off-canvas element moves in-canvas (off->on)${label}`, async () => {
|
|
173
|
+
const h = mountOverlayWithHeadline(760, delayMs);
|
|
174
|
+
try {
|
|
175
|
+
await untilIndicator(h.host, (indicator) => expect(indicator).toBeTruthy());
|
|
176
|
+
|
|
177
|
+
act(() => {
|
|
178
|
+
h.movedElement.style.left = "120px";
|
|
179
|
+
});
|
|
180
|
+
await untilIndicator(h.host, (indicator) => expect(indicator).toBeNull());
|
|
181
|
+
} finally {
|
|
182
|
+
h.cleanup();
|
|
183
|
+
}
|
|
184
|
+
});
|
|
185
|
+
}
|
|
156
186
|
|
|
157
187
|
it("tracks the indicator to the new position when it stays off-canvas (off->off)", async () => {
|
|
158
188
|
const h = mountOverlayWithHeadline(760);
|
|
159
189
|
try {
|
|
160
|
-
await
|
|
161
|
-
await flushAnimationFrames();
|
|
162
|
-
});
|
|
190
|
+
await untilIndicator(h.host, (indicator) => expect(indicator).toBeTruthy());
|
|
163
191
|
const before = h.host.querySelector(INDICATOR);
|
|
164
|
-
expect(before).toBeTruthy();
|
|
165
192
|
const leftBefore = (before!.parentElement as HTMLElement).style.left;
|
|
166
193
|
|
|
167
194
|
// Move further off-canvas (still outside the 800px-wide composition).
|
|
168
195
|
act(() => {
|
|
169
196
|
h.movedElement.style.left = "1200px";
|
|
170
197
|
});
|
|
171
|
-
await
|
|
172
|
-
|
|
173
|
-
|
|
198
|
+
await untilIndicator(h.host, (indicator) => {
|
|
199
|
+
expect(indicator).toBeTruthy();
|
|
200
|
+
expect((indicator!.parentElement as HTMLElement).style.left).not.toEqual(leftBefore);
|
|
174
201
|
});
|
|
175
|
-
|
|
176
|
-
const after = h.host.querySelector(INDICATOR);
|
|
177
|
-
expect(after).toBeTruthy();
|
|
178
|
-
const leftAfter = (after!.parentElement as HTMLElement).style.left;
|
|
179
|
-
expect(leftAfter).not.toEqual(leftBefore);
|
|
180
202
|
} finally {
|
|
181
203
|
h.cleanup();
|
|
182
204
|
}
|
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
resetOverlayFrameLoopForTests,
|
|
8
8
|
subscribeOverlayFrame,
|
|
9
9
|
} from "./overlayFrameLoop";
|
|
10
|
+
import { usePlayerStore } from "../../player/store/playerStore";
|
|
10
11
|
|
|
11
12
|
/**
|
|
12
13
|
* The editor's overlay polls run on one shared, parkable frame loop. The two
|
|
@@ -37,6 +38,7 @@ describe("overlay frame loop", () => {
|
|
|
37
38
|
};
|
|
38
39
|
|
|
39
40
|
beforeEach(() => {
|
|
41
|
+
usePlayerStore.setState({ previewBooted: true });
|
|
40
42
|
vi.useFakeTimers({ toFake: ["setTimeout", "clearTimeout", "performance", "Date"] });
|
|
41
43
|
queued = [];
|
|
42
44
|
window.requestAnimationFrame = ((callback: FrameRequestCallback) => {
|
|
@@ -55,6 +57,20 @@ describe("overlay frame loop", () => {
|
|
|
55
57
|
window.cancelAnimationFrame = originalCancelRaf;
|
|
56
58
|
});
|
|
57
59
|
|
|
60
|
+
it("does not poll the preview before it boots, and wakes when it does", () => {
|
|
61
|
+
usePlayerStore.setState({ previewBooted: false });
|
|
62
|
+
let runs = 0;
|
|
63
|
+
subscribeOverlayFrame(() => {
|
|
64
|
+
runs += 1;
|
|
65
|
+
});
|
|
66
|
+
framesOver(1000);
|
|
67
|
+
expect(runs).toBe(0);
|
|
68
|
+
|
|
69
|
+
usePlayerStore.getState().markPreviewBooted();
|
|
70
|
+
framesOver(32);
|
|
71
|
+
expect(runs).toBeGreaterThan(0);
|
|
72
|
+
});
|
|
73
|
+
|
|
58
74
|
it("runs every subscriber on one frame, not one frame each", () => {
|
|
59
75
|
const calls: string[] = [];
|
|
60
76
|
subscribeOverlayFrame(() => calls.push("a"));
|
|
@@ -25,6 +25,8 @@
|
|
|
25
25
|
* the next couple of hundred milliseconds, and no further event is dispatched
|
|
26
26
|
* while it does.
|
|
27
27
|
*/
|
|
28
|
+
import { usePlayerStore } from "../../player/store/playerStore";
|
|
29
|
+
|
|
28
30
|
const AWAKE_MS = 400;
|
|
29
31
|
/** How often a parked loop runs one frame anyway, in case a wake was missed. */
|
|
30
32
|
export const IDLE_POLL_MS = 250;
|
|
@@ -35,6 +37,7 @@ let frameId: number | null = null;
|
|
|
35
37
|
let idleTimerId: ReturnType<typeof setTimeout> | null = null;
|
|
36
38
|
let awakeUntil = 0;
|
|
37
39
|
let listenersAttached = false;
|
|
40
|
+
let stopBootWake: (() => void) | null = null;
|
|
38
41
|
|
|
39
42
|
/** Wake sources. All passive reads; none of them can be cancelled by us. */
|
|
40
43
|
const WINDOW_EVENTS = [
|
|
@@ -111,6 +114,9 @@ function runFrame(): void {
|
|
|
111
114
|
// it — and a shared loop makes that failure four overlays wide plus the
|
|
112
115
|
// idle-poll safety net, permanently, rather than one overlay's own problem.
|
|
113
116
|
schedule();
|
|
117
|
+
// Nothing to track until the live preview boots, and a poll before then forces layout
|
|
118
|
+
// on a document that is still loading, on the thread the boot runs on.
|
|
119
|
+
if (!usePlayerStore.getState().previewBooted) return;
|
|
114
120
|
for (const subscriber of subscribers) {
|
|
115
121
|
try {
|
|
116
122
|
subscriber();
|
|
@@ -154,6 +160,9 @@ function attachListeners(): void {
|
|
|
154
160
|
window.addEventListener(type, requestOverlayFrames, { capture: true, passive: true });
|
|
155
161
|
}
|
|
156
162
|
window.addEventListener("message", onPreviewMessage, { capture: true, passive: true });
|
|
163
|
+
stopBootWake = usePlayerStore.subscribe((state, prev) => {
|
|
164
|
+
if (state.previewBooted && !prev.previewBooted) requestOverlayFrames();
|
|
165
|
+
});
|
|
157
166
|
}
|
|
158
167
|
|
|
159
168
|
function detachListeners(): void {
|
|
@@ -163,6 +172,8 @@ function detachListeners(): void {
|
|
|
163
172
|
window.removeEventListener(type, requestOverlayFrames, { capture: true });
|
|
164
173
|
}
|
|
165
174
|
window.removeEventListener("message", onPreviewMessage, { capture: true });
|
|
175
|
+
stopBootWake?.();
|
|
176
|
+
stopBootWake = null;
|
|
166
177
|
}
|
|
167
178
|
|
|
168
179
|
/**
|
|
@@ -4,6 +4,7 @@ import type { DomEditSelection } from "./domEditing";
|
|
|
4
4
|
import type { GsapAnimation } from "@hyperframes/parsers/gsap-parser";
|
|
5
5
|
import type { TimelineElement } from "../../player";
|
|
6
6
|
import { roundToCenti } from "../../utils/rounding";
|
|
7
|
+
import { findPreviewNode } from "./domEditingElement";
|
|
7
8
|
|
|
8
9
|
export type {
|
|
9
10
|
BackgroundRemovalProgress,
|
|
@@ -470,8 +471,6 @@ export function readGsapRuntimeValuesForPanel(
|
|
|
470
471
|
if (!gsapAnimId || gsapAnimations.length === 0) return null;
|
|
471
472
|
const iframe = previewIframeRef?.current;
|
|
472
473
|
if (!iframe?.contentWindow) return null;
|
|
473
|
-
const selector = element.id ? `#${element.id}` : element.selector;
|
|
474
|
-
if (!selector) return null;
|
|
475
474
|
try {
|
|
476
475
|
const gsap = (
|
|
477
476
|
iframe.contentWindow as unknown as {
|
|
@@ -479,7 +478,7 @@ export function readGsapRuntimeValuesForPanel(
|
|
|
479
478
|
}
|
|
480
479
|
).gsap;
|
|
481
480
|
if (!gsap?.getProperty) return null;
|
|
482
|
-
const el = iframe.contentDocument
|
|
481
|
+
const el = findPreviewNode(iframe.contentDocument, element);
|
|
483
482
|
if (!el) return null;
|
|
484
483
|
const propKeys = collectPanelPropKeys(gsapAnimations);
|
|
485
484
|
const result: Record<string, number> = {};
|
|
@@ -508,10 +507,9 @@ export function readGsapBorderRadiusForPanel(
|
|
|
508
507
|
if (!hasBRProp) return null;
|
|
509
508
|
}
|
|
510
509
|
const iframe = previewIframeRef?.current;
|
|
511
|
-
|
|
512
|
-
if (!iframe?.contentDocument || !selector) return null;
|
|
510
|
+
if (!iframe?.contentDocument) return null;
|
|
513
511
|
try {
|
|
514
|
-
const el = iframe.contentDocument
|
|
512
|
+
const el = findPreviewNode(iframe.contentDocument, element);
|
|
515
513
|
if (!el || !iframe.contentWindow) return null;
|
|
516
514
|
const cs = iframe.contentWindow.getComputedStyle(el);
|
|
517
515
|
const parse = (v: string) => Number.parseFloat(v) || 0;
|
|
@@ -2,12 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|
import { act, useRef, useState } from "react";
|
|
4
4
|
import { createRoot } from "react-dom/client";
|
|
5
|
-
import { afterEach, describe, expect, it } from "vitest";
|
|
5
|
+
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
|
6
6
|
import { resetOverlayFrameLoopForTests } from "./overlayFrameLoop";
|
|
7
7
|
import {
|
|
8
8
|
usePreviewCompositionRect,
|
|
9
9
|
type PreviewCompositionRect,
|
|
10
10
|
} from "./usePreviewCompositionRect";
|
|
11
|
+
import { usePlayerStore } from "../../player/store/playerStore";
|
|
11
12
|
|
|
12
13
|
(globalThis as unknown as { IS_REACT_ACT_ENVIRONMENT: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
|
|
13
14
|
|
|
@@ -59,6 +60,10 @@ function compositionFrame(box: [number, number, number, number], width: number,
|
|
|
59
60
|
|
|
60
61
|
const settle = () => new Promise((r) => setTimeout(r, 400));
|
|
61
62
|
|
|
63
|
+
beforeEach(() => {
|
|
64
|
+
usePlayerStore.setState({ previewBooted: true });
|
|
65
|
+
});
|
|
66
|
+
|
|
62
67
|
describe("usePreviewCompositionRect", () => {
|
|
63
68
|
it("reports the live preview iframe's box relative to the overlay, scaled to the composition", async () => {
|
|
64
69
|
const probe = mountProbe();
|