@hyperframes/studio 0.7.54 → 0.7.56
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/{index-uBY329wb.js → index-BWnOxAiH.js} +1 -1
- package/dist/assets/{index-CMHYjEZ5.js → index-C4csZims.js} +1 -1
- package/dist/assets/index-CmVCjZjp.js +423 -0
- package/dist/assets/index-D-GyYi2d.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 +146 -144
- package/dist/index.html +2 -2
- package/dist/index.js +40582 -37803
- 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 +115 -0
- package/src/components/editor/CanvasContextMenu.tsx +198 -0
- 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 +158 -126
- 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 +43 -1
- 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 +208 -0
- package/src/components/editor/canvasContextMenuZOrder.test.ts +435 -0
- package/src/components/editor/canvasContextMenuZOrder.ts +352 -0
- 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/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/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/nle/AssetPreviewOverlay.tsx +147 -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 +240 -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.tsx +326 -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 +39 -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 +12 -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 +34 -1
- package/src/hooks/timelineEditingHelpers.ts +22 -48
- package/src/hooks/timelineMoveAdapter.test.ts +77 -0
- package/src/hooks/timelineMoveAdapter.ts +37 -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/useContextMenuDismiss.ts +31 -7
- package/src/hooks/useDomEditCommits.test.tsx +154 -1
- package/src/hooks/useDomEditCommits.ts +100 -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 +258 -0
- package/src/hooks/useElementLifecycleOps.ts +40 -71
- 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 +303 -0
- 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/useTimelineEditing.test.tsx +20 -29
- package/src/hooks/useTimelineEditingTypes.ts +6 -0
- package/src/hooks/useTimelineGroupEditing.ts +18 -4
- 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/PlayheadIndicator.tsx +50 -5
- package/src/player/components/Timeline.test.ts +102 -19
- package/src/player/components/Timeline.tsx +194 -194
- package/src/player/components/TimelineCanvas.tsx +207 -521
- 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/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 +943 -0
- package/src/player/components/timelineClipDragCommit.ts +401 -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 +477 -0
- package/src/player/components/timelineCollision.ts +263 -0
- 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 +120 -61
- package/src/player/components/timelineMarquee.test.ts +197 -0
- package/src/player/components/timelineMarquee.ts +112 -0
- package/src/player/components/timelineMultiDragPreview.test.ts +127 -0
- package/src/player/components/timelineMultiDragPreview.ts +106 -0
- package/src/player/components/timelineOptimisticRevision.ts +42 -0
- package/src/player/components/timelineSnapping.test.ts +134 -0
- package/src/player/components/timelineSnapping.ts +110 -0
- package/src/player/components/timelineStackingSync.test.ts +244 -0
- package/src/player/components/timelineStackingSync.ts +331 -0
- package/src/player/components/timelineTheme.ts +6 -1
- package/src/player/components/timelineZones.test.ts +425 -0
- package/src/player/components/timelineZones.ts +198 -0
- package/src/player/components/timelineZoom.test.ts +67 -0
- package/src/player/components/timelineZoom.ts +51 -0
- 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/useTimelineStackingSync.test.tsx +67 -0
- package/src/player/components/useTimelineStackingSync.ts +83 -0
- package/src/player/hooks/useExpandedTimelineElements.ts +0 -3
- package/src/player/hooks/usePlaybackKeyboard.ts +5 -0
- package/src/player/hooks/useTimelinePlayer.ts +48 -53
- package/src/player/hooks/useTimelinePlayerLoop.ts +85 -0
- package/src/player/hooks/useTimelineSyncCallbacks.test.ts +219 -0
- package/src/player/hooks/useTimelineSyncCallbacks.ts +104 -4
- package/src/player/index.ts +3 -2
- 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 +12 -81
- package/src/player/lib/timelineElementHelpers.ts +54 -5
- package/src/player/lib/timelineIframeHelpers.ts +2 -0
- package/src/player/store/playerStore.ts +62 -3
- package/src/styles/studio.css +27 -0
- package/src/utils/assetClickBehavior.test.ts +104 -0
- package/src/utils/assetClickBehavior.ts +75 -0
- package/src/utils/assetPreviewStore.ts +33 -0
- package/src/utils/blockInstaller.ts +10 -2
- package/src/utils/canvasNudgeGate.test.ts +31 -0
- package/src/utils/canvasNudgeGate.ts +32 -0
- 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/studioUiPreferences.test.ts +38 -0
- package/src/utils/studioUiPreferences.ts +27 -0
- package/src/utils/studioUrlState.test.ts +0 -1
- package/src/utils/timelineAssetDrop.ts +62 -0
- package/src/utils/timelineDiscovery.ts +0 -17
- package/src/utils/timelineInspector.test.ts +121 -0
- package/src/utils/timelineInspector.ts +32 -1
- package/dist/assets/index-Dq7FEg0K.css +0 -1
- package/dist/assets/index-pRhCpGPz.js +0 -423
- 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,135 @@
|
|
|
1
|
+
// @vitest-environment jsdom
|
|
2
|
+
import { act } from "react";
|
|
3
|
+
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
4
|
+
import type { DomEditSelection, DomEditTextField } from "../components/editor/domEditing";
|
|
5
|
+
import { mountReactHarness } from "./domSelectionTestHarness";
|
|
6
|
+
import { useDomEditTextCommits, type UseDomEditTextCommitsParams } from "./useDomEditTextCommits";
|
|
7
|
+
|
|
8
|
+
Reflect.set(globalThis, "IS_REACT_ACT_ENVIRONMENT", true);
|
|
9
|
+
|
|
10
|
+
interface Deferred<T> {
|
|
11
|
+
promise: Promise<T>;
|
|
12
|
+
resolve: (value: T | PromiseLike<T>) => void;
|
|
13
|
+
reject: (reason?: unknown) => void;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function createDeferred<T>(): Deferred<T> {
|
|
17
|
+
let resolve: Deferred<T>["resolve"] | undefined;
|
|
18
|
+
let reject: Deferred<T>["reject"] | undefined;
|
|
19
|
+
const promise = new Promise<T>((resolvePromise, rejectPromise) => {
|
|
20
|
+
resolve = resolvePromise;
|
|
21
|
+
reject = rejectPromise;
|
|
22
|
+
});
|
|
23
|
+
if (!resolve || !reject) throw new Error("deferred callbacks were not initialized");
|
|
24
|
+
return { promise, resolve, reject };
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function textField(value: string): DomEditTextField {
|
|
28
|
+
return {
|
|
29
|
+
key: "self",
|
|
30
|
+
label: "Text",
|
|
31
|
+
value,
|
|
32
|
+
tagName: "div",
|
|
33
|
+
attributes: [],
|
|
34
|
+
inlineStyles: {},
|
|
35
|
+
computedStyles: {},
|
|
36
|
+
source: "self",
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function selectionFor(element: HTMLElement): DomEditSelection {
|
|
41
|
+
return {
|
|
42
|
+
id: element.id,
|
|
43
|
+
element,
|
|
44
|
+
label: "Card",
|
|
45
|
+
tagName: "div",
|
|
46
|
+
sourceFile: "index.html",
|
|
47
|
+
compositionPath: "index.html",
|
|
48
|
+
isCompositionHost: false,
|
|
49
|
+
isInsideLockedComposition: false,
|
|
50
|
+
boundingBox: { x: 0, y: 0, width: 100, height: 100 },
|
|
51
|
+
textContent: element.textContent,
|
|
52
|
+
dataAttributes: {},
|
|
53
|
+
inlineStyles: {},
|
|
54
|
+
computedStyles: {},
|
|
55
|
+
textFields: [textField(element.textContent ?? "")],
|
|
56
|
+
capabilities: {
|
|
57
|
+
canSelect: true,
|
|
58
|
+
canEditStyles: true,
|
|
59
|
+
canCrop: true,
|
|
60
|
+
canMove: true,
|
|
61
|
+
canResize: true,
|
|
62
|
+
canApplyManualOffset: true,
|
|
63
|
+
canApplyManualSize: true,
|
|
64
|
+
canApplyManualRotation: true,
|
|
65
|
+
},
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
let cleanup: (() => void) | null = null;
|
|
70
|
+
|
|
71
|
+
function renderTextCommitHook(params: UseDomEditTextCommitsParams) {
|
|
72
|
+
const captured: { hook: ReturnType<typeof useDomEditTextCommits> | null } = { hook: null };
|
|
73
|
+
function TextCommitProbe() {
|
|
74
|
+
captured.hook = useDomEditTextCommits(params);
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
const root = mountReactHarness(<TextCommitProbe />);
|
|
78
|
+
cleanup = () => act(() => root.unmount());
|
|
79
|
+
if (!captured.hook) throw new Error("hook did not initialize");
|
|
80
|
+
return captured.hook;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
afterEach(() => {
|
|
84
|
+
cleanup?.();
|
|
85
|
+
cleanup = null;
|
|
86
|
+
document.body.replaceChildren();
|
|
87
|
+
vi.restoreAllMocks();
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
describe("useDomEditTextCommits", () => {
|
|
91
|
+
it("does not let a stale failed fields commit revert newer text", async () => {
|
|
92
|
+
const iframe = document.createElement("iframe");
|
|
93
|
+
document.body.append(iframe);
|
|
94
|
+
const doc = iframe.contentDocument;
|
|
95
|
+
if (!doc) throw new Error("expected iframe document");
|
|
96
|
+
doc.body.innerHTML = '<div id="card">Original</div>';
|
|
97
|
+
const element = doc.getElementById("card");
|
|
98
|
+
const HTMLElementCtor = doc.defaultView?.HTMLElement;
|
|
99
|
+
if (!HTMLElementCtor || !(element instanceof HTMLElementCtor)) {
|
|
100
|
+
throw new Error("expected preview element");
|
|
101
|
+
}
|
|
102
|
+
vi.spyOn(console, "warn").mockImplementation(() => {});
|
|
103
|
+
const selection = selectionFor(element);
|
|
104
|
+
const stalePersist = createDeferred<void>();
|
|
105
|
+
const persistDomEditOperations = vi
|
|
106
|
+
.fn()
|
|
107
|
+
.mockImplementationOnce(() => stalePersist.promise)
|
|
108
|
+
.mockResolvedValueOnce(undefined);
|
|
109
|
+
const hook = renderTextCommitHook({
|
|
110
|
+
activeCompPath: "index.html",
|
|
111
|
+
previewIframeRef: { current: iframe },
|
|
112
|
+
showToast: vi.fn(),
|
|
113
|
+
domEditSelection: selection,
|
|
114
|
+
applyDomSelection: vi.fn(),
|
|
115
|
+
refreshDomEditSelectionFromPreview: vi.fn(),
|
|
116
|
+
buildDomSelectionFromTarget: vi.fn(async () => null),
|
|
117
|
+
persistDomEditOperations,
|
|
118
|
+
resolveImportedFontAsset: () => null,
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
let staleCommit: Promise<void> | undefined;
|
|
122
|
+
act(() => {
|
|
123
|
+
staleCommit = hook.commitDomTextFields(selection, [textField("Stale")]);
|
|
124
|
+
});
|
|
125
|
+
await act(async () => {
|
|
126
|
+
await hook.commitDomTextFields(selection, [textField("Newest")]);
|
|
127
|
+
});
|
|
128
|
+
stalePersist.reject(new Error("stale request failed"));
|
|
129
|
+
await act(async () => {
|
|
130
|
+
await staleCommit;
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
expect(element.innerHTML).toBe("Newest");
|
|
134
|
+
});
|
|
135
|
+
});
|
|
@@ -174,7 +174,11 @@ export function useDomEditTextCommits({
|
|
|
174
174
|
let editedElement: HTMLElement | null = null;
|
|
175
175
|
let previousInlineValue: string | null = null;
|
|
176
176
|
const operations = buildDomStyleCommitOperations(property, value, isImageBackgroundCommit);
|
|
177
|
-
|
|
177
|
+
// Inline-style commits never full-reload the preview (that blanks the iframe
|
|
178
|
+
// until it re-renders): the live element was already mutated optimistically in
|
|
179
|
+
// apply(). z-index is no exception — setting `element.style.zIndex` restacks the
|
|
180
|
+
// element in-browser immediately, so a reload would only cost a black blink.
|
|
181
|
+
const skipRefresh = true;
|
|
178
182
|
|
|
179
183
|
await runDomEditCommit({
|
|
180
184
|
capture: () => {
|
|
@@ -305,6 +309,7 @@ export function useDomEditTextCommits({
|
|
|
305
309
|
nextTextFields: DomEditTextField[],
|
|
306
310
|
options?: { importedFont?: ImportedFontAsset | null },
|
|
307
311
|
) => {
|
|
312
|
+
const isLatestTextCommit = bumpDomEditCommitVersion(domTextCommitVersionRef);
|
|
308
313
|
const textCommit = planDomTextCommit(
|
|
309
314
|
selection.textFields,
|
|
310
315
|
nextTextFields,
|
|
@@ -344,14 +349,14 @@ export function useDomEditTextCommits({
|
|
|
344
349
|
: undefined,
|
|
345
350
|
});
|
|
346
351
|
},
|
|
347
|
-
shouldRevert: () =>
|
|
352
|
+
shouldRevert: () => isLatestTextCommit(),
|
|
348
353
|
revert: () => {
|
|
349
354
|
if (!editedElement || previousInnerHtml === null) return;
|
|
350
355
|
editedElement.innerHTML = previousInnerHtml;
|
|
351
356
|
},
|
|
352
357
|
onError: (error) =>
|
|
353
358
|
reportDomEditPersistFailure(selection, textCommit.operations, error, showToast),
|
|
354
|
-
shouldResync:
|
|
359
|
+
shouldResync: isLatestTextCommit,
|
|
355
360
|
resync: () =>
|
|
356
361
|
resyncDomTextSelectionFromPreview(
|
|
357
362
|
doc,
|
|
@@ -11,7 +11,6 @@ import { useCallback, useEffect, useRef } from "react";
|
|
|
11
11
|
import type { DomEditSelection } from "../components/editor/domEditingTypes";
|
|
12
12
|
import { STUDIO_GSAP_PANEL_ENABLED } from "../components/editor/manualEditingAvailability";
|
|
13
13
|
import { usePlayerStore } from "../player";
|
|
14
|
-
import { resolveTimelineIdForSelection } from "../utils/studioHelpers";
|
|
15
14
|
import { useDomEditPreviewSync } from "./useDomEditPreviewSync";
|
|
16
15
|
import { useGsapAnimationsForElement, usePopulateKeyframeCacheForFile } from "./useGsapTweenCache";
|
|
17
16
|
import { useGsapAnimationFetchFallback } from "./useGsapAnimationFetchFallback";
|
|
@@ -171,14 +170,13 @@ export function useDomEditWiring({
|
|
|
171
170
|
|
|
172
171
|
useEffect(() => {
|
|
173
172
|
if (!domEditSelection?.id) return;
|
|
174
|
-
const { selectedElementId, elements,
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
}, [domEditSelection, activeCompPath]);
|
|
173
|
+
const { selectedElementId, elements, setSelectedElementId } = usePlayerStore.getState();
|
|
174
|
+
const matchKey = elements.find(
|
|
175
|
+
(el) => el.domId === domEditSelection.id || el.id === domEditSelection.id,
|
|
176
|
+
);
|
|
177
|
+
const key = matchKey ? (matchKey.key ?? matchKey.id) : null;
|
|
178
|
+
if (key && key !== selectedElementId) setSelectedElementId(key);
|
|
179
|
+
}, [domEditSelection?.id]);
|
|
182
180
|
|
|
183
181
|
// ── GSAP cache sync ──
|
|
184
182
|
|
|
@@ -61,14 +61,29 @@ export function useDomGeometryCommits({
|
|
|
61
61
|
);
|
|
62
62
|
|
|
63
63
|
const handleDomBoxSizeCommit = useCallback(
|
|
64
|
-
(
|
|
64
|
+
(
|
|
65
|
+
selection: DomEditSelection,
|
|
66
|
+
next: { width: number; height: number },
|
|
67
|
+
offset?: { x: number; y: number },
|
|
68
|
+
) => {
|
|
65
69
|
if (isElementGsapTargeted(previewIframeRef.current, selection.element)) {
|
|
66
70
|
const error = new Error(GSAP_CSS_FALLBACK_BLOCKED_MESSAGE);
|
|
67
71
|
showToast(error.message, "error");
|
|
68
72
|
return Promise.reject(error);
|
|
69
73
|
}
|
|
70
74
|
applyStudioBoxSize(selection.element, next);
|
|
71
|
-
|
|
75
|
+
// Anchored-corner resize (NW/NE/SW) also moves the element to keep the
|
|
76
|
+
// opposite corner fixed. Apply the offset and emit BOTH patch sets in a
|
|
77
|
+
// SINGLE commit: one persist = one undo entry, and there is no
|
|
78
|
+
// intermediate re-stamp where the new size is in source but the anchor
|
|
79
|
+
// offset is not (that frame was the release "jump"). Both builders read
|
|
80
|
+
// the already-mutated live element, so concatenation is safe.
|
|
81
|
+
const patches = buildBoxSizePatches(selection.element);
|
|
82
|
+
if (offset) {
|
|
83
|
+
applyStudioPathOffset(selection.element, offset);
|
|
84
|
+
patches.push(...buildPathOffsetPatches(selection.element));
|
|
85
|
+
}
|
|
86
|
+
return commitPositionPatchToHtml(selection, patches, {
|
|
72
87
|
label: "Resize layer box",
|
|
73
88
|
coalesceKey: `box-size:${getDomEditTargetKey(selection)}`,
|
|
74
89
|
});
|
|
@@ -2,9 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import React, { act } from "react";
|
|
4
4
|
import { createRoot } from "react-dom/client";
|
|
5
|
-
import {
|
|
6
|
-
import type { TimelineElement } from "../player";
|
|
7
|
-
import { usePlayerStore } from "../player/store/playerStore";
|
|
5
|
+
import { describe, expect, it, vi } from "vitest";
|
|
8
6
|
import { installReactActEnvironment, makeSelection } from "./domSelectionTestHarness";
|
|
9
7
|
import { useDomSelection } from "./useDomSelection";
|
|
10
8
|
|
|
@@ -14,7 +12,6 @@ interface HarnessProps {
|
|
|
14
12
|
activeCompPath: string | null;
|
|
15
13
|
projectId: string | null;
|
|
16
14
|
refreshKey: number;
|
|
17
|
-
timelineElements?: TimelineElement[];
|
|
18
15
|
}
|
|
19
16
|
|
|
20
17
|
function renderHarness(initialProps: HarnessProps): {
|
|
@@ -35,7 +32,7 @@ function renderHarness(initialProps: HarnessProps): {
|
|
|
35
32
|
compIdToSrc: new Map(),
|
|
36
33
|
captionEditMode: false,
|
|
37
34
|
previewIframeRef: { current: null },
|
|
38
|
-
timelineElements:
|
|
35
|
+
timelineElements: [],
|
|
39
36
|
setSelectedTimelineElementId: vi.fn(),
|
|
40
37
|
setRightCollapsed: vi.fn(),
|
|
41
38
|
setRightPanelTab: vi.fn(),
|
|
@@ -67,10 +64,6 @@ function renderHarness(initialProps: HarnessProps): {
|
|
|
67
64
|
};
|
|
68
65
|
}
|
|
69
66
|
|
|
70
|
-
afterEach(() => {
|
|
71
|
-
usePlayerStore.getState().reset();
|
|
72
|
-
});
|
|
73
|
-
|
|
74
67
|
function setupSelectedHarness() {
|
|
75
68
|
const element = document.createElement("div");
|
|
76
69
|
element.id = "headline";
|
|
@@ -138,31 +131,4 @@ describe("useDomSelection", () => {
|
|
|
138
131
|
expect(harness.current().domEditSelection).toBe(selection);
|
|
139
132
|
harness.cleanup();
|
|
140
133
|
});
|
|
141
|
-
|
|
142
|
-
it("keeps preview marquee selections mirrored to the full timeline selection set", () => {
|
|
143
|
-
const first = document.createElement("div");
|
|
144
|
-
first.id = "clip-1";
|
|
145
|
-
const second = document.createElement("div");
|
|
146
|
-
second.id = "clip-2";
|
|
147
|
-
const firstSelection = makeSelection("First", first);
|
|
148
|
-
const secondSelection = makeSelection("Second", second);
|
|
149
|
-
const harness = renderHarness({
|
|
150
|
-
activeCompPath: "intro.html",
|
|
151
|
-
projectId: "project-1",
|
|
152
|
-
refreshKey: 0,
|
|
153
|
-
timelineElements: [
|
|
154
|
-
{ id: "clip-1", domId: "clip-1", tag: "div", start: 0, duration: 1, track: 0 },
|
|
155
|
-
{ id: "clip-2", domId: "clip-2", tag: "div", start: 1, duration: 1, track: 1 },
|
|
156
|
-
],
|
|
157
|
-
});
|
|
158
|
-
|
|
159
|
-
act(() => harness.current().applyMarqueeSelection([secondSelection, firstSelection], false));
|
|
160
|
-
|
|
161
|
-
const state = usePlayerStore.getState();
|
|
162
|
-
expect([...state.selectedElementIds]).toEqual(["clip-2", "clip-1"]);
|
|
163
|
-
expect(state.selectedElementId).toBe("clip-2");
|
|
164
|
-
expect(harness.current().domEditGroupSelections).toHaveLength(2);
|
|
165
|
-
expect(harness.current().domEditSelection).toBe(secondSelection);
|
|
166
|
-
harness.cleanup();
|
|
167
|
-
});
|
|
168
134
|
});
|
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
import { useState, useCallback, useRef, useEffect } from "react";
|
|
2
|
-
import type { TimelineElement } from "../player";
|
|
2
|
+
import type { SelectElementOptions, TimelineElement } from "../player";
|
|
3
3
|
import {
|
|
4
4
|
getAllPreviewTargetsFromPointer,
|
|
5
5
|
getPreviewTargetFromPointer,
|
|
6
6
|
} from "../utils/studioPreviewHelpers";
|
|
7
|
-
import {
|
|
7
|
+
import {
|
|
8
|
+
findMatchingTimelineElementId,
|
|
9
|
+
findTimelineIdByAncestor,
|
|
10
|
+
type RightPanelTab,
|
|
11
|
+
} from "../utils/studioHelpers";
|
|
8
12
|
import {
|
|
9
13
|
domEditSelectionsTargetSame,
|
|
10
14
|
domEditSelectionInGroup,
|
|
@@ -20,7 +24,6 @@ import {
|
|
|
20
24
|
type DomEditSelection,
|
|
21
25
|
} from "../components/editor/domEditing";
|
|
22
26
|
import { reapplyPositionEditsAfterSeek } from "../components/editor/manualEdits";
|
|
23
|
-
import { usePlayerStore } from "../player/store/playerStore";
|
|
24
27
|
|
|
25
28
|
// ── Types ──
|
|
26
29
|
|
|
@@ -44,7 +47,7 @@ export interface UseDomSelectionParams {
|
|
|
44
47
|
captionEditMode: boolean;
|
|
45
48
|
previewIframeRef: React.MutableRefObject<HTMLIFrameElement | null>;
|
|
46
49
|
timelineElements: TimelineElement[];
|
|
47
|
-
setSelectedTimelineElementId: (id: string | null) => void;
|
|
50
|
+
setSelectedTimelineElementId: (id: string | null, options?: SelectElementOptions) => void;
|
|
48
51
|
setRightCollapsed: (collapsed: boolean) => void;
|
|
49
52
|
setRightPanelTab: (tab: RightPanelTab) => void;
|
|
50
53
|
previewIframe: HTMLIFrameElement | null;
|
|
@@ -92,6 +95,7 @@ export interface UseDomSelectionReturn {
|
|
|
92
95
|
) => Promise<DomEditSelection | null>;
|
|
93
96
|
handleTimelineElementSelect: (element: TimelineElement | null) => Promise<void>;
|
|
94
97
|
refreshDomEditSelectionFromPreview: (selection: DomEditSelection) => Promise<void>;
|
|
98
|
+
refreshDomEditGroupSelectionsFromPreview: (selections: DomEditSelection[]) => Promise<void>;
|
|
95
99
|
applyMarqueeSelection: (selections: DomEditSelection[], additive: boolean) => void;
|
|
96
100
|
}
|
|
97
101
|
|
|
@@ -206,42 +210,27 @@ export function useDomSelection({
|
|
|
206
210
|
if (nextSelection) {
|
|
207
211
|
if (options?.revealPanel !== false) {
|
|
208
212
|
setRightCollapsed(false);
|
|
209
|
-
// Keep the Variables tab in place — selecting elements is part of
|
|
210
|
-
//
|
|
213
|
+
// Keep the Variables tab in place — selecting elements is part of the bind
|
|
214
|
+
// flow there; yanking to Design would lose the context.
|
|
211
215
|
if (rightPanelTabRef.current !== "variables") {
|
|
212
216
|
setRightPanelTab("design");
|
|
213
217
|
}
|
|
214
218
|
}
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
.map((selection) =>
|
|
225
|
-
resolveTimelineIdForSelection(selection, timelineElements, activeCompPath),
|
|
226
|
-
)
|
|
227
|
-
.filter((id): id is string => Boolean(id));
|
|
228
|
-
if (groupIds.length > 0) {
|
|
229
|
-
usePlayerStore.getState().setSelection(groupIds, anchorId);
|
|
230
|
-
} else {
|
|
231
|
-
setSelectedTimelineElementId(anchorId);
|
|
232
|
-
}
|
|
219
|
+
const nextSelectedTimelineId =
|
|
220
|
+
findMatchingTimelineElementId(nextSelection, timelineElements) ??
|
|
221
|
+
findTimelineIdByAncestor(
|
|
222
|
+
nextSelection.element,
|
|
223
|
+
timelineElements,
|
|
224
|
+
nextSelection.sourceFile || "index.html",
|
|
225
|
+
);
|
|
226
|
+
// Late marquee notify: a primary already in the live set must not collapse it.
|
|
227
|
+
setSelectedTimelineElementId(nextSelectedTimelineId, { preserveSet: true });
|
|
233
228
|
return;
|
|
234
229
|
}
|
|
235
230
|
|
|
236
231
|
setSelectedTimelineElementId(null);
|
|
237
232
|
},
|
|
238
|
-
[
|
|
239
|
-
setSelectedTimelineElementId,
|
|
240
|
-
timelineElements,
|
|
241
|
-
setRightCollapsed,
|
|
242
|
-
setRightPanelTab,
|
|
243
|
-
activeCompPath,
|
|
244
|
-
],
|
|
233
|
+
[setSelectedTimelineElementId, timelineElements, setRightCollapsed, setRightPanelTab],
|
|
245
234
|
);
|
|
246
235
|
|
|
247
236
|
const clearDomSelection = useCallback(() => {
|
|
@@ -424,6 +413,55 @@ export function useDomSelection({
|
|
|
424
413
|
[activeCompPath, applyDomSelection, buildDomSelectionFromTarget, previewIframeRef],
|
|
425
414
|
);
|
|
426
415
|
|
|
416
|
+
const refreshDomEditGroupSelectionsFromPreview = useCallback(
|
|
417
|
+
// fallow-ignore-next-line complexity
|
|
418
|
+
async (selections: DomEditSelection[]) => {
|
|
419
|
+
const iframe = previewIframeRef.current;
|
|
420
|
+
let doc: Document | null = null;
|
|
421
|
+
try {
|
|
422
|
+
doc = iframe?.contentDocument ?? null;
|
|
423
|
+
} catch {
|
|
424
|
+
return;
|
|
425
|
+
}
|
|
426
|
+
if (!doc) return;
|
|
427
|
+
|
|
428
|
+
const nextGroup: DomEditSelection[] = [];
|
|
429
|
+
for (const selection of selections) {
|
|
430
|
+
const element = findElementForSelection(doc, selection, activeCompPath);
|
|
431
|
+
if (!element) continue;
|
|
432
|
+
const nextSelection = await buildDomSelectionFromTarget(element);
|
|
433
|
+
if (nextSelection) nextGroup.push(nextSelection);
|
|
434
|
+
}
|
|
435
|
+
if (nextGroup.length === 0) return;
|
|
436
|
+
|
|
437
|
+
const currentSelection = domEditSelectionRef.current;
|
|
438
|
+
const nextSelection =
|
|
439
|
+
nextGroup.find((selection) => domEditSelectionsTargetSame(selection, currentSelection)) ??
|
|
440
|
+
nextGroup[0] ??
|
|
441
|
+
null;
|
|
442
|
+
|
|
443
|
+
domEditSelectionRef.current = nextSelection;
|
|
444
|
+
domEditGroupSelectionsRef.current = nextGroup;
|
|
445
|
+
setDomEditSelection(nextSelection);
|
|
446
|
+
setDomEditGroupSelections(nextGroup);
|
|
447
|
+
|
|
448
|
+
if (nextSelection) {
|
|
449
|
+
setSelectedTimelineElementId(
|
|
450
|
+
findMatchingTimelineElementId(nextSelection, timelineElements),
|
|
451
|
+
);
|
|
452
|
+
} else {
|
|
453
|
+
setSelectedTimelineElementId(null);
|
|
454
|
+
}
|
|
455
|
+
},
|
|
456
|
+
[
|
|
457
|
+
activeCompPath,
|
|
458
|
+
buildDomSelectionFromTarget,
|
|
459
|
+
setSelectedTimelineElementId,
|
|
460
|
+
timelineElements,
|
|
461
|
+
previewIframeRef,
|
|
462
|
+
],
|
|
463
|
+
);
|
|
464
|
+
|
|
427
465
|
// ── Effects ──
|
|
428
466
|
|
|
429
467
|
// Clear hover unconditionally on composition/project/preview change
|
|
@@ -471,8 +509,7 @@ export function useDomSelection({
|
|
|
471
509
|
const applyMarqueeSelection = useCallback(
|
|
472
510
|
// fallow-ignore-next-line complexity
|
|
473
511
|
(selections: DomEditSelection[], additive: boolean) => {
|
|
474
|
-
// Honor the inspector-panels kill switch like applyDomSelection does
|
|
475
|
-
// marquee can't land selections while the inspector UI is suppressed.
|
|
512
|
+
// Honor the inspector-panels kill switch like applyDomSelection does.
|
|
476
513
|
if (!STUDIO_INSPECTOR_PANELS_ENABLED) {
|
|
477
514
|
domEditSelectionRef.current = null;
|
|
478
515
|
domEditGroupSelectionsRef.current = [];
|
|
@@ -493,8 +530,7 @@ export function useDomSelection({
|
|
|
493
530
|
if (!domEditSelectionInGroup(nextGroup, s)) nextGroup = [...nextGroup, s];
|
|
494
531
|
}
|
|
495
532
|
} else {
|
|
496
|
-
// Dedupe by target:
|
|
497
|
-
// to the same group, which must count as one selection, not many duplicates.
|
|
533
|
+
// Dedupe by target: select-as-unit collapses marquee'd members to one group.
|
|
498
534
|
nextGroup = [];
|
|
499
535
|
for (const s of selections) {
|
|
500
536
|
if (!domEditSelectionInGroup(nextGroup, s)) nextGroup.push(s);
|
|
@@ -505,23 +541,16 @@ export function useDomSelection({
|
|
|
505
541
|
domEditGroupSelectionsRef.current = nextGroup;
|
|
506
542
|
setDomEditSelection(nextSelection);
|
|
507
543
|
setDomEditGroupSelections(nextGroup);
|
|
508
|
-
const nextTimelineId =
|
|
509
|
-
nextSelection,
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
)
|
|
517
|
-
.filter((id): id is string => Boolean(id));
|
|
518
|
-
if (nextTimelineIds.length > 0) {
|
|
519
|
-
usePlayerStore.getState().setSelection(nextTimelineIds, nextTimelineId);
|
|
520
|
-
} else {
|
|
521
|
-
setSelectedTimelineElementId(null);
|
|
522
|
-
}
|
|
544
|
+
const nextTimelineId =
|
|
545
|
+
findMatchingTimelineElementId(nextSelection, timelineElements) ??
|
|
546
|
+
findTimelineIdByAncestor(
|
|
547
|
+
nextSelection.element,
|
|
548
|
+
timelineElements,
|
|
549
|
+
nextSelection.sourceFile || "index.html",
|
|
550
|
+
);
|
|
551
|
+
setSelectedTimelineElementId(nextTimelineId);
|
|
523
552
|
},
|
|
524
|
-
[applyDomSelection, timelineElements, setSelectedTimelineElementId
|
|
553
|
+
[applyDomSelection, timelineElements, setSelectedTimelineElementId],
|
|
525
554
|
);
|
|
526
555
|
|
|
527
556
|
// Disabled inspector effect
|
|
@@ -558,6 +587,7 @@ export function useDomSelection({
|
|
|
558
587
|
buildDomSelectionForTimelineElement,
|
|
559
588
|
handleTimelineElementSelect,
|
|
560
589
|
refreshDomEditSelectionFromPreview,
|
|
590
|
+
refreshDomEditGroupSelectionsFromPreview,
|
|
561
591
|
applyMarqueeSelection,
|
|
562
592
|
};
|
|
563
593
|
}
|