@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
package/src/styles/studio.css
CHANGED
|
@@ -141,6 +141,22 @@ body {
|
|
|
141
141
|
opacity: 1;
|
|
142
142
|
}
|
|
143
143
|
|
|
144
|
+
/* Audio clips read as a distinct kind — a persistent violet tint vs the teal-
|
|
145
|
+
accented visual clips — so the audio zone at the bottom is visually separate
|
|
146
|
+
at a glance (the timeline's kinds: visual on top, audio below). */
|
|
147
|
+
.timeline-clip.is-audio {
|
|
148
|
+
background-color: rgba(167, 139, 250, 0.16);
|
|
149
|
+
border-color: rgba(167, 139, 250, 0.4);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
.timeline-clip.is-audio.is-hovered {
|
|
153
|
+
background-color: rgba(167, 139, 250, 0.24);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
.timeline-clip.is-audio.is-dragging {
|
|
157
|
+
background-color: rgba(60, 52, 84, 0.96);
|
|
158
|
+
}
|
|
159
|
+
|
|
144
160
|
.timeline-clip.is-hovered {
|
|
145
161
|
background-color: rgba(255, 255, 255, 0.09);
|
|
146
162
|
}
|
|
@@ -158,9 +174,20 @@ body {
|
|
|
158
174
|
}
|
|
159
175
|
|
|
160
176
|
.timeline-clip.is-dragging {
|
|
177
|
+
/* Solid background so the picked-up clip reads clearly while dragging — audio
|
|
178
|
+
clips (waveform drawn on a separate layer) would otherwise look transparent. */
|
|
179
|
+
background-color: rgba(38, 42, 52, 0.96);
|
|
161
180
|
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.4);
|
|
162
181
|
}
|
|
163
182
|
|
|
183
|
+
/* Keep the white selection outline while dragging (a plain box-shadow alone gets
|
|
184
|
+
overridden by .is-dragging's drop shadow, so re-state both together). */
|
|
185
|
+
.timeline-clip.is-selected.is-dragging {
|
|
186
|
+
box-shadow:
|
|
187
|
+
0 0 0 1.5px rgba(255, 255, 255, 0.85),
|
|
188
|
+
0 8px 24px rgba(0, 0, 0, 0.4);
|
|
189
|
+
}
|
|
190
|
+
|
|
164
191
|
.timeline-clip[data-active].is-selected.is-dragging {
|
|
165
192
|
box-shadow:
|
|
166
193
|
0 0 0 1.5px rgba(255, 255, 255, 0.85),
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { findClipForAsset, isPointerClick, DRAG_THRESHOLD_PX } from "./assetClickBehavior";
|
|
3
|
+
import type { TimelineElement } from "../player/store/playerStore";
|
|
4
|
+
|
|
5
|
+
// Minimal TimelineElement factory — only the fields the function inspects.
|
|
6
|
+
function makeEl(overrides: Partial<TimelineElement> & { id: string }): TimelineElement {
|
|
7
|
+
return {
|
|
8
|
+
tag: "div",
|
|
9
|
+
start: 0,
|
|
10
|
+
duration: 5,
|
|
11
|
+
track: 0,
|
|
12
|
+
...overrides,
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
describe("findClipForAsset", () => {
|
|
17
|
+
it("returns null when elements array is empty", () => {
|
|
18
|
+
expect(findClipForAsset([], "assets/foo.mp4")).toBeNull();
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it("returns null when no element matches", () => {
|
|
22
|
+
const el = makeEl({ id: "el1", src: "assets/other.mp4" });
|
|
23
|
+
expect(findClipForAsset([el], "assets/foo.mp4")).toBeNull();
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it("matches a bare relative src against the project-relative asset path", () => {
|
|
27
|
+
const el = makeEl({ id: "el1", src: "assets/clip.mp4", start: 2 });
|
|
28
|
+
expect(findClipForAsset([el], "assets/clip.mp4")).toBe(el);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it("matches a src with a ./ prefix", () => {
|
|
32
|
+
const el = makeEl({ id: "el1", src: "./assets/logo.png" });
|
|
33
|
+
expect(findClipForAsset([el], "assets/logo.png")).toBe(el);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it("matches a server-relative /api/projects/…/preview/ src", () => {
|
|
37
|
+
const el = makeEl({ id: "el1", src: "/api/projects/demo/preview/assets/bgm.mp3" });
|
|
38
|
+
expect(findClipForAsset([el], "assets/bgm.mp3")).toBe(el);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it("matches a fully-absolute URL (as produced by the core runtime)", () => {
|
|
42
|
+
const el = makeEl({
|
|
43
|
+
id: "el1",
|
|
44
|
+
src: "http://localhost:3012/api/projects/demo/preview/assets/clip.mp4",
|
|
45
|
+
});
|
|
46
|
+
expect(findClipForAsset([el], "assets/clip.mp4")).toBe(el);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it("decodes percent-encoded filenames when matching", () => {
|
|
50
|
+
const el = makeEl({
|
|
51
|
+
id: "el1",
|
|
52
|
+
src: "http://localhost:3012/api/projects/p/preview/assets/my%20file%20(1).mp4",
|
|
53
|
+
});
|
|
54
|
+
expect(findClipForAsset([el], "assets/my file (1).mp4")).toBe(el);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it("strips query strings from src before matching", () => {
|
|
58
|
+
const el = makeEl({ id: "el1", src: "assets/clip.mp4?v=2" });
|
|
59
|
+
expect(findClipForAsset([el], "assets/clip.mp4")).toBe(el);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it("returns the element with the earliest start when multiple clips match", () => {
|
|
63
|
+
const later = makeEl({ id: "late", src: "assets/clip.mp4", start: 10 });
|
|
64
|
+
const earlier = makeEl({ id: "early", src: "assets/clip.mp4", start: 2 });
|
|
65
|
+
const first = makeEl({ id: "first", src: "assets/clip.mp4", start: 0 });
|
|
66
|
+
expect(findClipForAsset([later, earlier, first], "assets/clip.mp4")).toBe(first);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it("prefers the key over id when the element has both", () => {
|
|
70
|
+
// findClipForAsset returns the element object itself; callers do `clip.key ?? clip.id`
|
|
71
|
+
const el = makeEl({ id: "el1", key: "clip-key", src: "assets/img.png" });
|
|
72
|
+
const found = findClipForAsset([el], "assets/img.png");
|
|
73
|
+
expect(found?.key).toBe("clip-key");
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it("skips elements with no src", () => {
|
|
77
|
+
const noSrc = makeEl({ id: "nosrc" });
|
|
78
|
+
const withSrc = makeEl({ id: "withsrc", src: "assets/img.png" });
|
|
79
|
+
expect(findClipForAsset([noSrc, withSrc], "assets/img.png")).toBe(withSrc);
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
describe("isPointerClick", () => {
|
|
84
|
+
it("returns true for zero movement", () => {
|
|
85
|
+
expect(isPointerClick(0, 0)).toBe(true);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it("returns true for movement within the threshold", () => {
|
|
89
|
+
expect(isPointerClick(DRAG_THRESHOLD_PX - 1, 0)).toBe(true);
|
|
90
|
+
expect(isPointerClick(0, DRAG_THRESHOLD_PX - 1)).toBe(true);
|
|
91
|
+
expect(isPointerClick(DRAG_THRESHOLD_PX - 1, DRAG_THRESHOLD_PX - 1)).toBe(true);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it("returns false at or beyond the threshold", () => {
|
|
95
|
+
expect(isPointerClick(DRAG_THRESHOLD_PX, 0)).toBe(false);
|
|
96
|
+
expect(isPointerClick(0, DRAG_THRESHOLD_PX)).toBe(false);
|
|
97
|
+
expect(isPointerClick(DRAG_THRESHOLD_PX + 10, 0)).toBe(false);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it("handles negative movement (pointer moved left/up)", () => {
|
|
101
|
+
expect(isPointerClick(-(DRAG_THRESHOLD_PX - 1), 0)).toBe(true);
|
|
102
|
+
expect(isPointerClick(-DRAG_THRESHOLD_PX, 0)).toBe(false);
|
|
103
|
+
});
|
|
104
|
+
});
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure helpers for CapCut-style asset card click behavior.
|
|
3
|
+
*
|
|
4
|
+
* Clicking an asset card that is ALREADY ADDED to the timeline selects the
|
|
5
|
+
* corresponding clip. Clicking one NOT yet in the timeline opens a lightweight
|
|
6
|
+
* preview overlay. Both behaviors are gated on "this was a click, not a drag".
|
|
7
|
+
*
|
|
8
|
+
* Pure — unit-tested.
|
|
9
|
+
*/
|
|
10
|
+
import type { TimelineElement } from "../player/store/playerStore";
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Find the TimelineElement that references `assetPath`, returning the one with
|
|
14
|
+
* the earliest start time when multiple clips share the same source.
|
|
15
|
+
*
|
|
16
|
+
* Matching mirrors `deriveUsedPaths` in AssetsTab: an element's `src` may be a
|
|
17
|
+
* fully-absolute URL, a server-relative `/api/projects/…/preview/…` path, a
|
|
18
|
+
* `./`-prefixed relative path, or a bare relative path — all normalised to the
|
|
19
|
+
* project-relative form that `assetPath` carries.
|
|
20
|
+
*
|
|
21
|
+
* Returns `null` when no element matches.
|
|
22
|
+
*/
|
|
23
|
+
export function findClipForAsset(
|
|
24
|
+
elements: TimelineElement[],
|
|
25
|
+
assetPath: string,
|
|
26
|
+
): TimelineElement | null {
|
|
27
|
+
let best: TimelineElement | null = null;
|
|
28
|
+
for (const el of elements) {
|
|
29
|
+
if (!el.src) continue;
|
|
30
|
+
if (normalizeSrc(el.src) !== assetPath) continue;
|
|
31
|
+
if (best === null || el.start < best.start) best = el;
|
|
32
|
+
}
|
|
33
|
+
return best;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Normalise a raw element `src` to the bare project-relative path so it can be
|
|
38
|
+
* compared against the asset-list strings (which have no leading slash, no
|
|
39
|
+
* origin, no query string).
|
|
40
|
+
*
|
|
41
|
+
* Mirrors the logic in `deriveUsedPaths` (AssetsTab.tsx) — keep in sync.
|
|
42
|
+
*/
|
|
43
|
+
function normalizeSrc(src: string): string {
|
|
44
|
+
let s = src;
|
|
45
|
+
try {
|
|
46
|
+
const u = new URL(s);
|
|
47
|
+
s = u.pathname;
|
|
48
|
+
} catch {
|
|
49
|
+
// Not an absolute URL — leave as-is
|
|
50
|
+
}
|
|
51
|
+
s = s
|
|
52
|
+
.replace(/^\/api\/projects\/[^/]+\/preview\//, "")
|
|
53
|
+
.replace(/^\.?\//, "")
|
|
54
|
+
.split(/[?#]/)[0];
|
|
55
|
+
try {
|
|
56
|
+
s = decodeURIComponent(s);
|
|
57
|
+
} catch {
|
|
58
|
+
// Malformed encoding — use as-is
|
|
59
|
+
}
|
|
60
|
+
return s;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** Drag-detection threshold in pixels — movements within this are treated as clicks. */
|
|
64
|
+
export const DRAG_THRESHOLD_PX = 4;
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Determine whether a pointer-up event should be treated as a click given the
|
|
68
|
+
* total pointer displacement since pointer-down.
|
|
69
|
+
*
|
|
70
|
+
* @param dx Horizontal distance moved in pixels.
|
|
71
|
+
* @param dy Vertical distance moved in pixels.
|
|
72
|
+
*/
|
|
73
|
+
export function isPointerClick(dx: number, dy: number): boolean {
|
|
74
|
+
return Math.abs(dx) < DRAG_THRESHOLD_PX && Math.abs(dy) < DRAG_THRESHOLD_PX;
|
|
75
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tiny Zustand slice that carries the "asset preview overlay" state.
|
|
3
|
+
*
|
|
4
|
+
* When a user clicks an asset card that has NOT yet been added to the
|
|
5
|
+
* timeline the overlay fires up: a dark scrim + centered media element
|
|
6
|
+
* (img / video / audio) + filename label rendered inside PreviewPane.
|
|
7
|
+
*
|
|
8
|
+
* State lives here so AssetsTab (sidebar) and PreviewPane (preview column)
|
|
9
|
+
* can communicate without prop-drilling through the multi-layer EditorShell
|
|
10
|
+
* tree. The store is project-scoped: NLEProvider (NLEContext.tsx) clears it
|
|
11
|
+
* whenever `projectId` changes, so a preview opened in one project can't
|
|
12
|
+
* bleed into another (the overlay itself stays mounted across project
|
|
13
|
+
* switches — EditorShell isn't keyed by projectId).
|
|
14
|
+
*/
|
|
15
|
+
import { create } from "zustand";
|
|
16
|
+
|
|
17
|
+
interface AssetPreviewState {
|
|
18
|
+
/** Project-relative asset path currently being previewed, or null. */
|
|
19
|
+
previewAsset: string | null;
|
|
20
|
+
/** projectId for which the preview was opened (used to build the serve URL). */
|
|
21
|
+
previewProjectId: string | null;
|
|
22
|
+
/** Open a media preview for the given asset. */
|
|
23
|
+
setPreviewAsset: (asset: string, projectId: string) => void;
|
|
24
|
+
/** Close the preview overlay. */
|
|
25
|
+
clearPreviewAsset: () => void;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export const useAssetPreviewStore = create<AssetPreviewState>((set) => ({
|
|
29
|
+
previewAsset: null,
|
|
30
|
+
previewProjectId: null,
|
|
31
|
+
setPreviewAsset: (asset, projectId) => set({ previewAsset: asset, previewProjectId: projectId }),
|
|
32
|
+
clearPreviewAsset: () => set({ previewAsset: null, previewProjectId: null }),
|
|
33
|
+
}));
|
|
@@ -2,9 +2,10 @@ import type { RegistryItem } from "@hyperframes/core/registry";
|
|
|
2
2
|
import type { TimelineElement } from "../player";
|
|
3
3
|
import {
|
|
4
4
|
insertTimelineAssetIntoSource,
|
|
5
|
-
|
|
5
|
+
resolveTimelineAssetCompositionSize,
|
|
6
6
|
} from "./timelineAssetDrop";
|
|
7
7
|
import { collectHtmlIds } from "./studioHelpers";
|
|
8
|
+
import { generateId } from "./generateId";
|
|
8
9
|
import { formatTimelineAttributeNumber } from "../player/components/timelineEditing";
|
|
9
10
|
import { saveProjectFilesWithHistory } from "./studioFileHistory";
|
|
10
11
|
import type { EditHistoryKind } from "./editHistory";
|
|
@@ -120,7 +121,9 @@ export async function addBlockToProject(
|
|
|
120
121
|
);
|
|
121
122
|
|
|
122
123
|
const isBlock = block.type === "hyperframes:block";
|
|
123
|
-
const
|
|
124
|
+
const { width: hostWidth, height: hostHeight } =
|
|
125
|
+
resolveTimelineAssetCompositionSize(originalContent);
|
|
126
|
+
const hostDims = { left: 0, top: 0, width: hostWidth, height: hostHeight };
|
|
124
127
|
|
|
125
128
|
const currentTime = opts.currentTime ?? 0;
|
|
126
129
|
const start = placement
|
|
@@ -152,6 +155,11 @@ export async function addBlockToProject(
|
|
|
152
155
|
|
|
153
156
|
const subCompHtml = [
|
|
154
157
|
`<div`,
|
|
158
|
+
// A stable id (+ hf-id) is what authored sub-comps carry; without it the
|
|
159
|
+
// timeline can't dedup the host and renders duplicate clips that multiply
|
|
160
|
+
// on every interaction. Matches the authored-comp shape.
|
|
161
|
+
` id="${compId}"`,
|
|
162
|
+
` data-hf-id="hf-${generateId()}"`,
|
|
155
163
|
` data-composition-id="${compId}"`,
|
|
156
164
|
` data-composition-src="${compositionFile}"`,
|
|
157
165
|
` data-start="${formatTimelineAttributeNumber(start)}"`,
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, it } from "vitest";
|
|
2
|
+
import { __resetForTests, acquireCanvasNudgeKeys, canvasNudgeKeysClaimed } from "./canvasNudgeGate";
|
|
3
|
+
|
|
4
|
+
describe("canvasNudgeGate", () => {
|
|
5
|
+
// The claim counter is module-level state; reset it so an unbalanced claim in one
|
|
6
|
+
// test can't leak into the next.
|
|
7
|
+
beforeEach(() => {
|
|
8
|
+
__resetForTests();
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
it("reports claimed while at least one claim is held", () => {
|
|
12
|
+
expect(canvasNudgeKeysClaimed()).toBe(false);
|
|
13
|
+
const releaseA = acquireCanvasNudgeKeys();
|
|
14
|
+
const releaseB = acquireCanvasNudgeKeys();
|
|
15
|
+
expect(canvasNudgeKeysClaimed()).toBe(true);
|
|
16
|
+
releaseA();
|
|
17
|
+
expect(canvasNudgeKeysClaimed()).toBe(true);
|
|
18
|
+
releaseB();
|
|
19
|
+
expect(canvasNudgeKeysClaimed()).toBe(false);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it("makes release idempotent so an effect re-run cannot underflow", () => {
|
|
23
|
+
const release = acquireCanvasNudgeKeys();
|
|
24
|
+
release();
|
|
25
|
+
release();
|
|
26
|
+
expect(canvasNudgeKeysClaimed()).toBe(false);
|
|
27
|
+
const releaseNext = acquireCanvasNudgeKeys();
|
|
28
|
+
expect(canvasNudgeKeysClaimed()).toBe(true);
|
|
29
|
+
releaseNext();
|
|
30
|
+
});
|
|
31
|
+
});
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Arrow-key ownership gate between the canvas nudge (DomEditOverlay) and the
|
|
3
|
+
* playback frame-step shortcuts (usePlaybackKeyboard). Both listen for arrow
|
|
4
|
+
* keys with window capture listeners, and their relative order depends on
|
|
5
|
+
* mount order (DomEditOverlay remounts when caption edit mode toggles), so
|
|
6
|
+
* `event.defaultPrevented` alone can't arbitrate. While a nudgeable canvas
|
|
7
|
+
* selection holds a claim, the playback handler skips ArrowLeft/ArrowRight.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
let claims = 0;
|
|
11
|
+
|
|
12
|
+
/** Claim the arrow keys for canvas nudging. Returns an idempotent release. */
|
|
13
|
+
export function acquireCanvasNudgeKeys(): () => void {
|
|
14
|
+
claims += 1;
|
|
15
|
+
let released = false;
|
|
16
|
+
return () => {
|
|
17
|
+
if (released) return;
|
|
18
|
+
released = true;
|
|
19
|
+
claims -= 1;
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** True while a nudgeable canvas selection owns the arrow keys. */
|
|
24
|
+
export function canvasNudgeKeysClaimed(): boolean {
|
|
25
|
+
return claims > 0;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Test-only: reset the module-level claim counter so leaked claims from one test
|
|
29
|
+
* can't bleed into the next (call in a `beforeEach`). */
|
|
30
|
+
export function __resetForTests(): void {
|
|
31
|
+
claims = 0;
|
|
32
|
+
}
|
|
@@ -209,6 +209,41 @@ describe("edit history", () => {
|
|
|
209
209
|
expect(state.undo[0].files["index.html"].after).toBe("c");
|
|
210
210
|
});
|
|
211
211
|
|
|
212
|
+
it("merges a lane-change move with its z-reorder past the default window via entry coalesceMs", () => {
|
|
213
|
+
// The z entry records only after the move persist's round-trip — often >300ms.
|
|
214
|
+
// Both sides pass coalesceMs: 5000 with the shared gesture key so the pair
|
|
215
|
+
// still folds into ONE undo step.
|
|
216
|
+
const move = buildEditHistoryEntry({
|
|
217
|
+
projectId: "project-1",
|
|
218
|
+
label: "Move timeline clips",
|
|
219
|
+
kind: "timeline",
|
|
220
|
+
coalesceKey: "clip-lane-move:1",
|
|
221
|
+
coalesceMs: 5000,
|
|
222
|
+
files: { "index.html": { before: "a", after: "b" } },
|
|
223
|
+
now: 100,
|
|
224
|
+
id: "move-entry",
|
|
225
|
+
});
|
|
226
|
+
const zReorder = buildEditHistoryEntry({
|
|
227
|
+
projectId: "project-1",
|
|
228
|
+
label: "Reorder layers",
|
|
229
|
+
kind: "manual",
|
|
230
|
+
coalesceKey: "clip-lane-move:1",
|
|
231
|
+
coalesceMs: 5000,
|
|
232
|
+
files: { "index.html": { before: "b", after: "c" } },
|
|
233
|
+
now: 500,
|
|
234
|
+
id: "z-entry",
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
const state = pushEditHistoryEntry(
|
|
238
|
+
pushEditHistoryEntry(createEmptyEditHistory(), move),
|
|
239
|
+
zReorder,
|
|
240
|
+
);
|
|
241
|
+
|
|
242
|
+
expect(state.undo).toHaveLength(1);
|
|
243
|
+
expect(state.undo[0].files["index.html"].before).toBe("a");
|
|
244
|
+
expect(state.undo[0].files["index.html"].after).toBe("c");
|
|
245
|
+
});
|
|
246
|
+
|
|
212
247
|
it("folds a slow GSAP follow-up into the timing edit via a per-entry coalesceMs override", () => {
|
|
213
248
|
const timing = buildEditHistoryEntry({
|
|
214
249
|
projectId: "project-1",
|
|
@@ -1,7 +1,12 @@
|
|
|
1
1
|
// @vitest-environment happy-dom
|
|
2
2
|
|
|
3
3
|
import { describe, it, expect, vi } from "vitest";
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
applySoftReload,
|
|
6
|
+
ensureMotionPathPluginLoaded,
|
|
7
|
+
diffSoftReloadableRestore,
|
|
8
|
+
applyUndoRestoreToPreview,
|
|
9
|
+
} from "./gsapSoftReload";
|
|
5
10
|
|
|
6
11
|
const SCRIPT_TEXT = `
|
|
7
12
|
window.__timelines = window.__timelines || {};
|
|
@@ -447,3 +452,116 @@ describe("applySoftReload authored-opacity restore", () => {
|
|
|
447
452
|
expect(restoreOpacity(el)).toBe("");
|
|
448
453
|
});
|
|
449
454
|
});
|
|
455
|
+
|
|
456
|
+
// ── Bug 2: undo/redo restore soft-apply ──────────────────────────────────────
|
|
457
|
+
|
|
458
|
+
const wrap = (body: string) => `<html><body>${body}</body></html>`;
|
|
459
|
+
|
|
460
|
+
describe("diffSoftReloadableRestore", () => {
|
|
461
|
+
it("reports the changed id for an attribute/inline-style-only diff", () => {
|
|
462
|
+
const prev = wrap(`<div id="a" style="translate: 10px 10px">t</div>`);
|
|
463
|
+
const next = wrap(`<div id="a" style="translate: 0px 0px">t</div>`);
|
|
464
|
+
expect(diffSoftReloadableRestore(prev, next)).toEqual({ changedElementIds: ["a"] });
|
|
465
|
+
});
|
|
466
|
+
|
|
467
|
+
it("treats a structural change (added element) as NOT soft-reloadable", () => {
|
|
468
|
+
const prev = wrap(`<div id="a">t</div>`);
|
|
469
|
+
const next = wrap(`<div id="a">t</div><div id="a-split">t</div>`);
|
|
470
|
+
expect(diffSoftReloadableRestore(prev, next)).toBeNull();
|
|
471
|
+
});
|
|
472
|
+
|
|
473
|
+
it("treats an element text/child change as NOT soft-reloadable", () => {
|
|
474
|
+
const prev = wrap(`<div id="a">one</div>`);
|
|
475
|
+
const next = wrap(`<div id="a">two</div>`);
|
|
476
|
+
expect(diffSoftReloadableRestore(prev, next)).toBeNull();
|
|
477
|
+
});
|
|
478
|
+
|
|
479
|
+
it("allows a GSAP-script-only change (no id'd-attribute diff)", () => {
|
|
480
|
+
const prev = wrap(
|
|
481
|
+
`<div id="a">t</div><script>window.__timelines["root"]=gsap.timeline().to("#a",{x:1});</script>`,
|
|
482
|
+
);
|
|
483
|
+
const next = wrap(
|
|
484
|
+
`<div id="a">t</div><script>window.__timelines["root"]=gsap.timeline().to("#a",{x:9});</script>`,
|
|
485
|
+
);
|
|
486
|
+
expect(diffSoftReloadableRestore(prev, next)).toEqual({ changedElementIds: [] });
|
|
487
|
+
});
|
|
488
|
+
});
|
|
489
|
+
|
|
490
|
+
function buildLiveIframe(bodyHtml: string) {
|
|
491
|
+
const doc = document.implementation.createHTMLDocument("");
|
|
492
|
+
doc.body.innerHTML = bodyHtml;
|
|
493
|
+
const contentWindow = {
|
|
494
|
+
gsap: { timeline: () => {} },
|
|
495
|
+
__hfForceTimelineRebind: () => {},
|
|
496
|
+
__timelines: {} as Record<string, unknown>,
|
|
497
|
+
__player: { getTime: () => 3, seek: vi.fn() },
|
|
498
|
+
__hfStudioManualEditsApply: vi.fn(),
|
|
499
|
+
};
|
|
500
|
+
return {
|
|
501
|
+
iframe: { contentWindow, contentDocument: doc } as unknown as HTMLIFrameElement,
|
|
502
|
+
contentWindow,
|
|
503
|
+
doc,
|
|
504
|
+
};
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
describe("applyUndoRestoreToPreview", () => {
|
|
508
|
+
const ROOT = "index.html";
|
|
509
|
+
|
|
510
|
+
it("soft-applies an attribute/style-only restore: syncs the live element, no full reload", () => {
|
|
511
|
+
const { iframe, contentWindow, doc } = buildLiveIframe(
|
|
512
|
+
`<div id="a" style="translate: 10px 10px" data-hf-path-offset="true">t</div>`,
|
|
513
|
+
);
|
|
514
|
+
const reloadPreview = vi.fn();
|
|
515
|
+
const files = {
|
|
516
|
+
[ROOT]: {
|
|
517
|
+
previous: wrap(
|
|
518
|
+
`<div id="a" style="translate: 10px 10px" data-hf-path-offset="true">t</div>`,
|
|
519
|
+
),
|
|
520
|
+
restored: wrap(`<div id="a" style="translate: 0px 0px" data-hf-path-offset="true">t</div>`),
|
|
521
|
+
},
|
|
522
|
+
};
|
|
523
|
+
const outcome = applyUndoRestoreToPreview(iframe, ROOT, files, 3, reloadPreview);
|
|
524
|
+
expect(outcome).toBe("soft");
|
|
525
|
+
expect(reloadPreview).not.toHaveBeenCalled();
|
|
526
|
+
// Live element reverted to the restored inline style.
|
|
527
|
+
expect(doc.getElementById("a")!.getAttribute("style")).toBe("translate: 0px 0px");
|
|
528
|
+
// No GSAP script in the restore → the manual-edit reapply runs, playhead held.
|
|
529
|
+
expect(contentWindow.__player.seek).toHaveBeenCalledWith(3);
|
|
530
|
+
expect(contentWindow.__hfStudioManualEditsApply).toHaveBeenCalled();
|
|
531
|
+
});
|
|
532
|
+
|
|
533
|
+
it("full-reloads a multi-file restore", () => {
|
|
534
|
+
const { iframe } = buildLiveIframe(`<div id="a">t</div>`);
|
|
535
|
+
const reloadPreview = vi.fn();
|
|
536
|
+
const files = {
|
|
537
|
+
[ROOT]: {
|
|
538
|
+
previous: wrap(`<div id="a" style="x">t</div>`),
|
|
539
|
+
restored: wrap(`<div id="a">t</div>`),
|
|
540
|
+
},
|
|
541
|
+
"scenes/intro.html": { previous: "a", restored: "b" },
|
|
542
|
+
};
|
|
543
|
+
expect(applyUndoRestoreToPreview(iframe, ROOT, files, 3, reloadPreview)).toBe("full");
|
|
544
|
+
expect(reloadPreview).toHaveBeenCalledTimes(1);
|
|
545
|
+
});
|
|
546
|
+
|
|
547
|
+
it("full-reloads a structural restore (split/delete undo)", () => {
|
|
548
|
+
const { iframe } = buildLiveIframe(`<div id="a">t</div><div id="a-split">t</div>`);
|
|
549
|
+
const reloadPreview = vi.fn();
|
|
550
|
+
const files = {
|
|
551
|
+
[ROOT]: {
|
|
552
|
+
previous: wrap(`<div id="a">t</div><div id="a-split">t</div>`),
|
|
553
|
+
restored: wrap(`<div id="a">t</div>`),
|
|
554
|
+
},
|
|
555
|
+
};
|
|
556
|
+
expect(applyUndoRestoreToPreview(iframe, ROOT, files, 3, reloadPreview)).toBe("full");
|
|
557
|
+
expect(reloadPreview).toHaveBeenCalledTimes(1);
|
|
558
|
+
});
|
|
559
|
+
|
|
560
|
+
it("full-reloads when the restore touches a sub-comp, not the active comp", () => {
|
|
561
|
+
const { iframe } = buildLiveIframe(`<div id="a">t</div>`);
|
|
562
|
+
const reloadPreview = vi.fn();
|
|
563
|
+
const files = { "scenes/intro.html": { previous: "a", restored: "b" } };
|
|
564
|
+
expect(applyUndoRestoreToPreview(iframe, ROOT, files, 3, reloadPreview)).toBe("full");
|
|
565
|
+
expect(reloadPreview).toHaveBeenCalledTimes(1);
|
|
566
|
+
});
|
|
567
|
+
});
|