@hyperframes/studio 0.7.55 → 0.7.57
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/assets/hyperframes-player--Z69cEkE.js +459 -0
- package/dist/assets/{index-CPetwHFV.js → index-Bf1x1y8H.js} +1 -1
- package/dist/assets/index-DfmYkU44.js +423 -0
- package/dist/assets/{index-BXaqaVKt.js → index-KsfE1bUu.js} +1 -1
- package/dist/assets/index-_pqzyxB1.css +1 -0
- package/dist/{chunk-SOTCF4DF.js → chunk-5QSIMBEJ.js} +2 -1
- package/dist/chunk-5QSIMBEJ.js.map +1 -0
- package/dist/{domEditingLayers-2ECJK24D.js → domEditingLayers-6LQGKPOI.js} +2 -2
- package/dist/index.d.ts +172 -144
- package/dist/index.html +2 -2
- package/dist/index.js +43613 -40346
- package/dist/index.js.map +1 -1
- package/package.json +7 -7
- package/src/App.tsx +106 -96
- package/src/components/EditorShell.tsx +253 -0
- package/src/components/StudioGlobalDragOverlay.tsx +1 -3
- package/src/components/StudioLeftSidebar.tsx +13 -3
- package/src/components/StudioRightPanel.tsx +8 -3
- package/src/components/TimelineToolbar.test.tsx +2 -2
- package/src/components/TimelineToolbar.tsx +179 -121
- package/src/components/editor/CanvasContextMenu.test.tsx +152 -1
- package/src/components/editor/CanvasContextMenu.tsx +33 -18
- package/src/components/editor/DomEditCropHandles.test.tsx +74 -3
- package/src/components/editor/DomEditCropHandles.tsx +64 -29
- package/src/components/editor/DomEditOverlay.test.ts +245 -279
- package/src/components/editor/DomEditOverlay.tsx +126 -125
- package/src/components/editor/DomEditOverlayHover.test.tsx +90 -0
- package/src/components/editor/DomEditRotateHandle.tsx +38 -18
- package/src/components/editor/DomEditSelectionChrome.test.tsx +80 -0
- package/src/components/editor/DomEditSelectionChrome.tsx +257 -0
- package/src/components/editor/LayersPanel.test.ts +66 -2
- package/src/components/editor/LayersPanel.tsx +53 -2
- package/src/components/editor/OffCanvasIndicators.test.tsx +34 -0
- package/src/components/editor/OffCanvasIndicators.tsx +49 -10
- package/src/components/editor/SnapGuideOverlay.tsx +28 -23
- package/src/components/editor/SnapToolbar.test.tsx +0 -1
- package/src/components/editor/anchoredResizeCommitFeedsOffset.test.ts +214 -0
- package/src/components/editor/anchoredResizeReleaseShift.test.ts +106 -10
- package/src/components/editor/canvasContextMenuZOrder.test.ts +186 -2
- package/src/components/editor/canvasContextMenuZOrder.ts +144 -22
- package/src/components/editor/domEditNudge.test.ts +80 -0
- package/src/components/editor/domEditNudge.ts +44 -0
- package/src/components/editor/domEditOverlayGeometry.test.ts +167 -1
- package/src/components/editor/domEditOverlayGeometry.ts +280 -20
- package/src/components/editor/domEditOverlayGestures.ts +44 -41
- package/src/components/editor/domEditOverlayStartGesture.ts +62 -2
- package/src/components/editor/domEditResizeLocal.test.ts +131 -0
- package/src/components/editor/domEditResizeLocal.ts +125 -0
- package/src/components/editor/domEditingDom.ts +1 -1
- package/src/components/editor/manualEditsDomPatches.test.ts +47 -0
- package/src/components/editor/manualOffsetDrag.ts +55 -12
- package/src/components/editor/offCanvasIndicatorGeometry.ts +31 -10
- package/src/components/editor/offCanvasIndicatorRefresh.test.tsx +25 -1
- package/src/components/editor/propertyPanelColorGradingSection.tsx +15 -21
- package/src/components/editor/resizeDraft.ts +108 -0
- package/src/components/editor/snapEngine.test.ts +12 -78
- package/src/components/editor/snapEngine.ts +13 -53
- package/src/components/editor/useCanvasContextMenuState.ts +92 -0
- package/src/components/editor/useDomEditNudge.test.tsx +228 -0
- package/src/components/editor/useDomEditNudge.ts +254 -0
- package/src/components/editor/useDomEditOverlayGestures.ts +115 -116
- package/src/components/editor/useDomEditOverlayRects.ts +9 -2
- package/src/components/editor/useZOrderCrossedFlash.tsx +68 -0
- package/src/components/nle/AssetPreviewOverlay.test.tsx +106 -0
- package/src/components/nle/AssetPreviewOverlay.tsx +171 -0
- package/src/components/nle/NLEContext.test.ts +144 -0
- package/src/components/nle/NLEContext.tsx +328 -0
- package/src/components/nle/NLEPreview.tsx +1 -1
- package/src/components/nle/PreviewOverlays.tsx +260 -0
- package/src/components/nle/PreviewPane.tsx +163 -0
- package/src/components/nle/TimelinePane.test.ts +60 -0
- package/src/components/nle/TimelinePane.tsx +293 -0
- package/src/components/nle/TimelineResizeDivider.tsx +8 -2
- package/src/components/nle/useCompositionStack.test.tsx +44 -0
- package/src/components/nle/useCompositionStack.ts +17 -9
- package/src/components/nle/useTimelineEditCallbacks.ts +217 -0
- package/src/components/sidebar/AssetCard.test.tsx +106 -0
- package/src/components/sidebar/AssetCard.tsx +343 -0
- package/src/components/sidebar/AssetContextMenu.tsx +15 -30
- package/src/components/sidebar/AssetsTab.test.ts +96 -0
- package/src/components/sidebar/AssetsTab.tsx +71 -206
- package/src/components/sidebar/AudioRow.tsx +56 -2
- package/src/components/sidebar/BlocksTab.tsx +8 -1
- package/src/components/sidebar/LeftSidebar.tsx +4 -1
- package/src/components/sidebar/assetHelpers.ts +29 -0
- package/src/contexts/DomEditContext.tsx +9 -0
- package/src/contexts/StudioContext.tsx +9 -8
- package/src/contexts/TimelineEditContext.tsx +3 -7
- package/src/hooks/deleteSelectedKeyframes.ts +35 -0
- package/src/hooks/domEditCommitTypes.ts +23 -1
- package/src/hooks/domSelectionTestHarness.ts +14 -0
- package/src/hooks/gestureTransaction.test.ts +311 -0
- package/src/hooks/gestureTransaction.ts +199 -0
- package/src/hooks/gsapDragCommit.test.ts +141 -41
- package/src/hooks/gsapDragCommit.ts +64 -74
- package/src/hooks/gsapDragStaticSetHelpers.ts +9 -21
- package/src/hooks/gsapResizeIntercept.test.ts +69 -0
- package/src/hooks/gsapResizeIntercept.ts +38 -10
- package/src/hooks/gsapRuntimeBridge.test.ts +49 -13
- package/src/hooks/gsapRuntimeBridge.ts +4 -4
- package/src/hooks/gsapScriptCommitTypes.ts +16 -5
- package/src/hooks/gsapShared.test.ts +19 -1
- package/src/hooks/gsapShared.ts +14 -0
- package/src/hooks/patchDocumentRootDuration.test.ts +71 -0
- package/src/hooks/timelineEditingGsap.ts +14 -0
- package/src/hooks/timelineEditingHelpers.test.ts +125 -1
- package/src/hooks/timelineEditingHelpers.ts +53 -292
- package/src/hooks/timelineMoveAdapter.test.ts +81 -0
- package/src/hooks/timelineMoveAdapter.ts +39 -0
- package/src/hooks/timelineTimingSync.test.ts +150 -0
- package/src/hooks/timelineTimingSync.ts +412 -0
- package/src/hooks/useAnimatedPropertyCommit.test.tsx +36 -8
- package/src/hooks/useAnimatedPropertyCommit.ts +31 -26
- package/src/hooks/useAppHotkeys.ts +16 -25
- package/src/hooks/useBlockHandlers.ts +55 -27
- package/src/hooks/useCaptionDetection.ts +2 -0
- package/src/hooks/useCompositionDimensions.ts +32 -5
- package/src/hooks/useContextMenuDismiss.ts +31 -7
- package/src/hooks/useDomEditCommits.test.tsx +245 -1
- package/src/hooks/useDomEditCommits.ts +162 -3
- package/src/hooks/useDomEditPositionPatchCommit.ts +2 -0
- package/src/hooks/useDomEditSession.ts +2 -2
- package/src/hooks/useDomEditTextCommits.test.tsx +135 -0
- package/src/hooks/useDomEditTextCommits.ts +8 -3
- package/src/hooks/useDomEditWiring.ts +7 -9
- package/src/hooks/useDomGeometryCommits.ts +17 -2
- package/src/hooks/useDomSelection.test.ts +2 -36
- package/src/hooks/useDomSelection.ts +81 -51
- package/src/hooks/useDomSelectionSelectionGuards.test.ts +276 -0
- package/src/hooks/useElementLifecycleOps.test.tsx +423 -0
- package/src/hooks/useElementLifecycleOps.ts +53 -71
- package/src/hooks/useElementPicker.ts +7 -8
- package/src/hooks/useEnableKeyframes.test.ts +142 -1
- package/src/hooks/useEnableKeyframes.ts +73 -29
- package/src/hooks/useGestureCommit.test.tsx +130 -0
- package/src/hooks/useGestureCommit.ts +32 -8
- package/src/hooks/useGroupCommits.ts +7 -28
- package/src/hooks/useGsapAwareEditing.test.tsx +239 -0
- package/src/hooks/useGsapAwareEditing.ts +115 -20
- package/src/hooks/useGsapKeyframeOps.test.tsx +59 -0
- package/src/hooks/useGsapKeyframeOps.ts +38 -10
- package/src/hooks/useGsapScriptCommits.test.tsx +40 -1
- package/src/hooks/useGsapScriptCommits.ts +134 -50
- package/src/hooks/useGsapSelectionHandlers.ts +49 -16
- package/src/hooks/useMusicBeatAnalysis.ts +72 -36
- package/src/hooks/usePersistentEditHistory.test.ts +33 -0
- package/src/hooks/usePersistentEditHistory.ts +92 -58
- package/src/hooks/usePreviewPersistence.ts +25 -7
- package/src/hooks/useRazorSplit.history.test.tsx +148 -18
- package/src/hooks/useRazorSplit.test.ts +269 -0
- package/src/hooks/useRazorSplit.testHelpers.ts +64 -0
- package/src/hooks/useRazorSplit.ts +107 -36
- package/src/hooks/useRenderClipContent.ts +24 -6
- package/src/hooks/useStudioContextValue.ts +15 -5
- package/src/hooks/useStudioUrlState.ts +2 -4
- package/src/hooks/useTimelineAssetDropOps.ts +175 -0
- package/src/hooks/useTimelineEditing.test.tsx +398 -241
- package/src/hooks/useTimelineEditing.ts +181 -265
- package/src/hooks/useTimelineEditingTypes.ts +6 -0
- package/src/hooks/useTimelineGroupEditing.ts +167 -132
- package/src/index.ts +1 -1
- package/src/player/components/ImageThumbnail.test.tsx +173 -0
- package/src/player/components/ImageThumbnail.tsx +160 -0
- package/src/player/components/PlayerControls.tsx +5 -5
- package/src/player/components/PlayheadIndicator.tsx +55 -5
- package/src/player/components/Timeline.test.ts +183 -23
- package/src/player/components/Timeline.tsx +194 -194
- package/src/player/components/TimelineCanvas.tsx +218 -523
- package/src/player/components/TimelineClip.tsx +4 -1
- package/src/player/components/TimelineClipDiamonds.tsx +9 -40
- package/src/player/components/TimelineEmptyState.tsx +3 -1
- package/src/player/components/TimelineLanes.tsx +487 -0
- package/src/player/components/TimelineOverlays.tsx +122 -0
- package/src/player/components/TimelineRuler.tsx +58 -48
- package/src/player/components/VideoThumbnail.test.tsx +152 -0
- package/src/player/components/VideoThumbnail.tsx +28 -7
- package/src/player/components/thumbnailUtils.test.ts +127 -0
- package/src/player/components/thumbnailUtils.ts +54 -0
- package/src/player/components/timelineCallbacks.ts +18 -18
- package/src/player/components/timelineClipChildren.tsx +39 -0
- package/src/player/components/timelineClipDragCommit.test.ts +1026 -0
- package/src/player/components/timelineClipDragCommit.ts +498 -0
- package/src/player/components/timelineClipDragPreview.test.ts +144 -0
- package/src/player/components/timelineClipDragPreview.ts +323 -38
- package/src/player/components/timelineClipDragTypes.ts +65 -0
- package/src/player/components/timelineCollision.test.ts +42 -2
- package/src/player/components/timelineDragDrop.ts +69 -60
- package/src/player/components/timelineEditCapabilities.ts +52 -0
- package/src/player/components/timelineEditing.test.ts +1 -1
- package/src/player/components/timelineEditing.ts +76 -59
- package/src/player/components/timelineGroupEditing.test.ts +126 -0
- package/src/player/components/timelineGroupEditing.ts +133 -0
- package/src/player/components/timelineGroupResizeCommit.ts +54 -0
- package/src/player/components/timelineLayout.test.ts +95 -23
- package/src/player/components/timelineLayout.ts +207 -75
- package/src/player/components/timelineMarquee.test.ts +197 -0
- package/src/player/components/timelineMarquee.ts +112 -0
- package/src/player/components/timelineOptimisticRevision.ts +42 -0
- package/src/player/components/timelineRevealScroll.test.ts +133 -0
- package/src/player/components/timelineRevealScroll.ts +91 -0
- package/src/player/components/timelineStackingSync.test.ts +60 -0
- package/src/player/components/timelineStackingSync.ts +27 -2
- package/src/player/components/timelineTheme.ts +6 -1
- package/src/player/components/timelineTrackPersistPipeline.test.ts +168 -0
- package/src/player/components/timelineZones.test.ts +122 -310
- package/src/player/components/timelineZones.ts +82 -135
- package/src/player/components/useResolvedTimelineEditCallbacks.ts +25 -3
- package/src/player/components/useTimelineClipDrag.resize.test.tsx +241 -0
- package/src/player/components/useTimelineClipDrag.ts +380 -386
- package/src/player/components/useTimelineEditPinning.ts +121 -0
- package/src/player/components/useTimelineGeometry.ts +129 -0
- package/src/player/components/useTimelinePlayhead.ts +4 -16
- package/src/player/components/useTimelineRangeSelection.ts +341 -50
- package/src/player/components/useTimelineRevealClip.ts +56 -0
- package/src/player/components/useTimelineStackingSync.test.tsx +77 -0
- package/src/player/components/useTimelineStackingSync.ts +94 -0
- package/src/player/hooks/useExpandedTimelineElements.ts +7 -3
- package/src/player/hooks/usePlaybackKeyboard.ts +5 -0
- package/src/player/hooks/useTimelinePlayer.seek.test.ts +55 -0
- package/src/player/hooks/useTimelinePlayer.test.ts +36 -3
- package/src/player/hooks/useTimelinePlayer.ts +52 -53
- package/src/player/hooks/useTimelinePlayerLoop.ts +85 -0
- package/src/player/hooks/useTimelineSyncCallbacks.ts +9 -1
- package/src/player/index.ts +5 -2
- package/src/player/lib/runtimeProtocol.test.ts +48 -0
- package/src/player/lib/runtimeProtocol.ts +65 -0
- package/src/player/lib/time.test.ts +19 -19
- package/src/player/lib/time.ts +6 -2
- package/src/player/lib/timelineDOM.test.ts +25 -98
- package/src/player/lib/timelineDOM.ts +21 -81
- package/src/player/lib/timelineElementHelpers.ts +73 -5
- package/src/player/lib/timelineIframeHelpers.ts +4 -4
- package/src/player/store/playerStore.test.ts +38 -13
- package/src/player/store/playerStore.ts +99 -3
- package/src/styles/studio.css +27 -0
- package/src/utils/assetPreviewDismiss.test.ts +27 -0
- package/src/utils/assetPreviewDismiss.ts +29 -0
- package/src/utils/assetPreviewStore.ts +33 -0
- package/src/utils/blockInstaller.ts +10 -2
- package/src/utils/editHistory.test.ts +35 -0
- package/src/utils/gsapSoftReload.test.ts +119 -1
- package/src/utils/gsapSoftReload.ts +153 -0
- package/src/utils/mediaTypes.ts +3 -2
- package/src/utils/resizeDebug.ts +55 -0
- package/src/utils/rootDuration.test.ts +90 -1
- package/src/utils/rootDuration.ts +76 -13
- package/src/utils/sdkCutover.ts +4 -0
- package/src/utils/studioFileHistory.ts +37 -7
- package/src/utils/studioHelpers.ts +62 -0
- package/src/utils/studioPreviewHelpers.test.ts +65 -8
- package/src/utils/studioPreviewHelpers.ts +10 -0
- package/src/utils/studioTelemetry.ts +4 -1
- package/src/utils/studioUrlState.test.ts +0 -1
- package/src/utils/timelineAssetDrop.test.ts +144 -52
- package/src/utils/timelineAssetDrop.ts +67 -29
- package/src/utils/timelineDiscovery.ts +0 -17
- package/dist/assets/hyperframes-player-BBrKzTGd.js +0 -459
- package/dist/assets/index-BRwkMj0w.js +0 -423
- package/dist/assets/index-Dq7FEg0K.css +0 -1
- package/dist/chunk-SOTCF4DF.js.map +0 -1
- package/src/components/StudioPreviewArea.tsx +0 -500
- package/src/components/nle/NLELayout.test.ts +0 -12
- package/src/components/nle/NLELayout.tsx +0 -591
- package/src/player/components/TimelineLayerGutter.tsx +0 -61
- package/src/player/components/TimelineSelectionOverlays.tsx +0 -55
- package/src/player/components/timelineMarqueeSelection.test.ts +0 -87
- package/src/player/components/timelineSnapTargets.test.ts +0 -144
- package/src/player/components/timelineSnapTargets.ts +0 -164
- package/src/player/components/useTimelineClipDrag.test.tsx +0 -506
- package/src/player/components/useTimelineClipGroupDrag.ts +0 -417
- package/src/player/components/useTimelineMarqueeSelection.test.tsx +0 -231
- package/src/player/components/useTimelineMarqueeSelection.ts +0 -276
- package/src/utils/timelineDiscovery.test.ts +0 -90
- /package/dist/{domEditingLayers-2ECJK24D.js.map → domEditingLayers-6LQGKPOI.js.map} +0 -0
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
// @vitest-environment happy-dom
|
|
2
|
+
|
|
3
|
+
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
4
|
+
import type { DomEditSelection } from "./domEditing";
|
|
5
|
+
import type { GestureState, UseDomEditOverlayGesturesOptions } from "./domEditOverlayGestures";
|
|
6
|
+
|
|
7
|
+
// Origin box (overlay px). The gesture-start center is its centroid; the
|
|
8
|
+
// center-anchored resize must keep THIS point planted, which means the release
|
|
9
|
+
// must commit a nonzero offset equal to minus half the size growth.
|
|
10
|
+
const ORIGIN = { left: 0, top: 0, width: 200, height: 100 };
|
|
11
|
+
const ORIGIN_CENTER = {
|
|
12
|
+
x: ORIGIN.left + ORIGIN.width / 2,
|
|
13
|
+
y: ORIGIN.top + ORIGIN.height / 2,
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
// Consistent geometry stub: model the physical truth the real DOM would report.
|
|
17
|
+
// A CSS width/height change grows the box from its top-left, so the rendered
|
|
18
|
+
// center drifts by half the size delta; the manual offset the gesture applies
|
|
19
|
+
// (read back from the element's studio vars) pulls it back. `elementCornerOverlayPoints`
|
|
20
|
+
// returns the four corners of that drifted box; `overlayCornersCentroid` (kept
|
|
21
|
+
// real) averages them so the anchor loop can measure the true center each frame.
|
|
22
|
+
vi.mock("./domEditOverlayGeometry", async () => {
|
|
23
|
+
const actual = await vi.importActual<typeof import("./domEditOverlayGeometry")>(
|
|
24
|
+
"./domEditOverlayGeometry",
|
|
25
|
+
);
|
|
26
|
+
const { readStudioBoxSize, readStudioPathOffset } = await import("./manualEditsDom");
|
|
27
|
+
const physicalCenter = (element: HTMLElement) => {
|
|
28
|
+
const size = readStudioBoxSize(element);
|
|
29
|
+
const width = size.width > 0 ? size.width : ORIGIN.width;
|
|
30
|
+
const height = size.height > 0 ? size.height : ORIGIN.height;
|
|
31
|
+
const offset = readStudioPathOffset(element);
|
|
32
|
+
return {
|
|
33
|
+
x: ORIGIN_CENTER.x + (width - ORIGIN.width) / 2 + offset.x,
|
|
34
|
+
y: ORIGIN_CENTER.y + (height - ORIGIN.height) / 2 + offset.y,
|
|
35
|
+
width,
|
|
36
|
+
height,
|
|
37
|
+
};
|
|
38
|
+
};
|
|
39
|
+
return {
|
|
40
|
+
...actual,
|
|
41
|
+
elementCornerOverlayPoints: (_o: unknown, _i: unknown, element: HTMLElement) => {
|
|
42
|
+
const c = physicalCenter(element);
|
|
43
|
+
const hw = c.width / 2;
|
|
44
|
+
const hh = c.height / 2;
|
|
45
|
+
return {
|
|
46
|
+
nw: { x: c.x - hw, y: c.y - hh },
|
|
47
|
+
ne: { x: c.x + hw, y: c.y - hh },
|
|
48
|
+
sw: { x: c.x - hw, y: c.y + hh },
|
|
49
|
+
se: { x: c.x + hw, y: c.y + hh },
|
|
50
|
+
};
|
|
51
|
+
},
|
|
52
|
+
orientedOverlayRect: (_o: unknown, _i: unknown, element: HTMLElement) => {
|
|
53
|
+
const c = physicalCenter(element);
|
|
54
|
+
return {
|
|
55
|
+
left: c.x - c.width / 2,
|
|
56
|
+
top: c.y - c.height / 2,
|
|
57
|
+
width: c.width,
|
|
58
|
+
height: c.height,
|
|
59
|
+
editScaleX: 1,
|
|
60
|
+
editScaleY: 1,
|
|
61
|
+
angle: 0,
|
|
62
|
+
};
|
|
63
|
+
},
|
|
64
|
+
};
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
const { createDomEditOverlayGestureHandlers } = await import("./useDomEditOverlayGestures");
|
|
68
|
+
|
|
69
|
+
function ref<T>(current: T) {
|
|
70
|
+
return { current };
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
interface CommitCall {
|
|
74
|
+
size: { width: number; height: number };
|
|
75
|
+
offset: { x: number; y: number } | undefined;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function buildHarness() {
|
|
79
|
+
const element = document.createElement("div");
|
|
80
|
+
document.body.append(element);
|
|
81
|
+
|
|
82
|
+
const selection = {
|
|
83
|
+
element,
|
|
84
|
+
id: "box",
|
|
85
|
+
selector: "#box",
|
|
86
|
+
selectorIndex: 0,
|
|
87
|
+
sourceFile: "index.html",
|
|
88
|
+
tagName: "div",
|
|
89
|
+
label: "Box",
|
|
90
|
+
textContent: "",
|
|
91
|
+
textFields: [],
|
|
92
|
+
capabilities: {
|
|
93
|
+
canEditText: false,
|
|
94
|
+
canEditLayout: true,
|
|
95
|
+
canMove: true,
|
|
96
|
+
canApplyManualOffset: true,
|
|
97
|
+
canApplyManualSize: true,
|
|
98
|
+
canApplyManualRotation: false,
|
|
99
|
+
canAdjustOpacity: true,
|
|
100
|
+
canAdjustFill: true,
|
|
101
|
+
canAdjustBorderRadius: true,
|
|
102
|
+
canAdjustStroke: true,
|
|
103
|
+
canAdjustShadow: true,
|
|
104
|
+
canAdjustZIndex: true,
|
|
105
|
+
},
|
|
106
|
+
computedStyle: { display: "block", position: "absolute" },
|
|
107
|
+
} as unknown as DomEditSelection;
|
|
108
|
+
|
|
109
|
+
const commits: CommitCall[] = [];
|
|
110
|
+
const overlayEl = document.createElement("div");
|
|
111
|
+
const iframe = document.createElement("iframe");
|
|
112
|
+
|
|
113
|
+
const opts: UseDomEditOverlayGesturesOptions = {
|
|
114
|
+
overlayRef: ref<HTMLDivElement | null>(overlayEl),
|
|
115
|
+
iframeRef: ref<HTMLIFrameElement | null>(iframe),
|
|
116
|
+
boxRef: ref<HTMLDivElement | null>(document.createElement("div")),
|
|
117
|
+
selectionRef: ref<DomEditSelection | null>(selection),
|
|
118
|
+
hoverSelectionRef: ref<DomEditSelection | null>(null),
|
|
119
|
+
overlayRectRef: ref<OverlayRectLike | null>({
|
|
120
|
+
left: ORIGIN.left,
|
|
121
|
+
top: ORIGIN.top,
|
|
122
|
+
width: ORIGIN.width,
|
|
123
|
+
height: ORIGIN.height,
|
|
124
|
+
editScaleX: 1,
|
|
125
|
+
editScaleY: 1,
|
|
126
|
+
}) as never,
|
|
127
|
+
groupOverlayItemsRef: ref([]),
|
|
128
|
+
gestureRef: ref<GestureState | null>(null),
|
|
129
|
+
groupGestureRef: ref(null),
|
|
130
|
+
blockedMoveRef: ref(null),
|
|
131
|
+
rafPausedRef: ref(false),
|
|
132
|
+
suppressNextBoxClickRef: ref(false),
|
|
133
|
+
setOverlayRect: () => {},
|
|
134
|
+
setGroupOverlayItems: () => {},
|
|
135
|
+
onBlockedMoveRef: ref(() => {}),
|
|
136
|
+
onManualDragStartRef: ref(() => {}),
|
|
137
|
+
onPathOffsetCommitRef: ref(() => {}),
|
|
138
|
+
onGroupPathOffsetCommitRef: ref(() => {}),
|
|
139
|
+
onBoxSizeCommitRef: ref((_s, size, offset) => {
|
|
140
|
+
commits.push({ size, offset });
|
|
141
|
+
}),
|
|
142
|
+
onRotationCommitRef: ref(() => {}),
|
|
143
|
+
onCanvasPointerMoveRef: ref(() => Promise.resolve(null)),
|
|
144
|
+
onCanvasMouseDown: () => {},
|
|
145
|
+
snapGuidesRef: ref(null),
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
const handlers = createDomEditOverlayGestureHandlers(opts);
|
|
149
|
+
return { handlers, commits, selection };
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
type OverlayRectLike = {
|
|
153
|
+
left: number;
|
|
154
|
+
top: number;
|
|
155
|
+
width: number;
|
|
156
|
+
height: number;
|
|
157
|
+
editScaleX: number;
|
|
158
|
+
editScaleY: number;
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
function evt(clientX: number, clientY: number) {
|
|
162
|
+
return {
|
|
163
|
+
clientX,
|
|
164
|
+
clientY,
|
|
165
|
+
pointerId: 1,
|
|
166
|
+
button: 0,
|
|
167
|
+
altKey: false,
|
|
168
|
+
shiftKey: false,
|
|
169
|
+
preventDefault() {},
|
|
170
|
+
stopPropagation() {},
|
|
171
|
+
currentTarget: { setPointerCapture() {} },
|
|
172
|
+
} as unknown as React.PointerEvent<HTMLDivElement>;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
afterEach(() => {
|
|
176
|
+
document.body.innerHTML = "";
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
describe("anchored corner resize — the release commit feeds the center-pin offset", () => {
|
|
180
|
+
it("onPointerUp passes a nonzero offset that keeps the center planted", () => {
|
|
181
|
+
const { handlers, commits } = buildHarness();
|
|
182
|
+
|
|
183
|
+
// Start an SE corner resize. Pointer starts 100px right of the center.
|
|
184
|
+
handlers.startGesture("resize", evt(ORIGIN_CENTER.x + 100, ORIGIN_CENTER.y), {
|
|
185
|
+
resizeHandle: "se",
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
// Drag outward to radial scale 1.5 (dist 150 / 100). Several frames so the
|
|
189
|
+
// per-frame center-pin anchor accumulates and converges into g.lastResizeAnchor.
|
|
190
|
+
for (let i = 0; i < 5; i++) {
|
|
191
|
+
handlers.onPointerMove(evt(ORIGIN_CENTER.x + 150, ORIGIN_CENTER.y));
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
handlers.onPointerUp(evt(ORIGIN_CENTER.x + 150, ORIGIN_CENTER.y));
|
|
195
|
+
|
|
196
|
+
expect(commits).toHaveLength(1);
|
|
197
|
+
const { size, offset } = commits[0]!;
|
|
198
|
+
|
|
199
|
+
// Proportional 1.5x growth of the 200x100 base.
|
|
200
|
+
expect(size.width).toBeCloseTo(300, 0);
|
|
201
|
+
expect(size.height).toBeCloseTo(150, 0);
|
|
202
|
+
|
|
203
|
+
// The committed offset must be present and nonzero — the open question.
|
|
204
|
+
expect(offset).toBeDefined();
|
|
205
|
+
if (!offset) return;
|
|
206
|
+
expect(offset.x).not.toBe(0);
|
|
207
|
+
expect(offset.y).not.toBe(0);
|
|
208
|
+
|
|
209
|
+
// And it must equal minus half the size growth, i.e. it re-pins the center to
|
|
210
|
+
// exactly the gesture-start center (offset = -(finalSize - origin)/2).
|
|
211
|
+
expect(offset.x).toBeCloseTo(-(size.width - ORIGIN.width) / 2, 0);
|
|
212
|
+
expect(offset.y).toBeCloseTo(-(size.height - ORIGIN.height) / 2, 0);
|
|
213
|
+
});
|
|
214
|
+
});
|
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
} from "./manualEditsDom";
|
|
9
9
|
import { buildBoxSizePatches, buildPathOffsetPatches } from "./manualEditsDomPatches";
|
|
10
10
|
import { createManualOffsetDragMember, applyManualOffsetDragCommit } from "./manualOffsetDrag";
|
|
11
|
+
import { computeNextResizeAnchor } from "./domEditResizeLocal";
|
|
11
12
|
import type { PatchOperation } from "../../utils/sourcePatcher";
|
|
12
13
|
import { splitTopLevelWhitespace } from "./manualEditsStyleHelpers";
|
|
13
14
|
|
|
@@ -16,15 +17,25 @@ import { splitTopLevelWhitespace } from "./manualEditsStyleHelpers";
|
|
|
16
17
|
* CENTER, which must stay planted across the whole gesture — including after
|
|
17
18
|
* release, on every corner and at any rotation.
|
|
18
19
|
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
* the
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
20
|
+
* Root cause of the original release "shift" (proved with a real-layout Chromium
|
|
21
|
+
* replay, see the anchor-loop test below): during a resize drag the per-frame
|
|
22
|
+
* anchor is derived from the element's LIVE measured center — which already carries
|
|
23
|
+
* the offset applied on the PREVIOUS frame — while `applyManualOffsetDragDraft`
|
|
24
|
+
* treats that anchor as the ABSOLUTE offset. So `fixedStart - centerNow` is really
|
|
25
|
+
* only the RESIDUAL correction, and using it as the absolute value makes the anchor
|
|
26
|
+
* OSCILLATE between the correct value and zero every frame:
|
|
27
|
+
* frame 0: offset 0 → center shifted by the resize → anchor = full amount → apply
|
|
28
|
+
* frame 1: offset applied → center back at fixedStart → anchor = 0 → apply 0 (un-pin!)
|
|
29
|
+
* frame 2: offset 0 again → anchor = full amount → ...
|
|
30
|
+
* Release commits `g.lastResizeAnchor` from whichever parity the last pointermove
|
|
31
|
+
* landed on, so the element lands EITHER pinned OR un-pinned — an unpredictable
|
|
32
|
+
* post-release "shift".
|
|
33
|
+
*
|
|
34
|
+
* Fix (useDomEditOverlayGestures pointermove, resize branch, fa4f39168): accumulate
|
|
35
|
+
* the residual onto the previously-applied anchor instead of using it as the
|
|
36
|
+
* absolute offset, so the loop converges to a stable value on every frame. The
|
|
37
|
+
* per-frame accumulation is the exported `computeNextResizeAnchor` helper (the one
|
|
38
|
+
* call site in the pointermove resize branch); tests 1 & 2 drive it directly.
|
|
28
39
|
*/
|
|
29
40
|
|
|
30
41
|
afterEach(() => {
|
|
@@ -66,10 +77,95 @@ function resolvedTranslatePx(el: HTMLElement): { x: number; y: number } {
|
|
|
66
77
|
}
|
|
67
78
|
|
|
68
79
|
describe("center-anchored corner resize — no shift after release", () => {
|
|
80
|
+
it("the per-frame center anchor converges (does NOT oscillate) — the release-shift root cause", () => {
|
|
81
|
+
// Model the pointermove anchor loop that pins the element's CENTER. The physical
|
|
82
|
+
// truth (confirmed in a real browser): the measured center sits at
|
|
83
|
+
// `fixedStart - appliedOffset` shifted by the resize — i.e. applying the offset
|
|
84
|
+
// moves the center back toward its gesture-start position. Here scale=1 so
|
|
85
|
+
// screen px == offset px. `trueAnchor` is the compensating offset that pins it.
|
|
86
|
+
const trueAnchor = { dx: -60, dy: -27 };
|
|
87
|
+
const fixedStart = { x: 500, y: 270 };
|
|
88
|
+
|
|
89
|
+
// `appliedOffset` mirrors what applyManualOffsetDragDraft set last frame.
|
|
90
|
+
let appliedOffset = { x: 0, y: 0 };
|
|
91
|
+
// `lastResizeAnchor` accumulator, exactly as g.lastResizeAnchor in the fix.
|
|
92
|
+
let lastResizeAnchor: { dx: number; dy: number } | undefined;
|
|
93
|
+
|
|
94
|
+
const anchorsSeen: Array<{ dx: number; dy: number }> = [];
|
|
95
|
+
for (let frame = 0; frame < 8; frame++) {
|
|
96
|
+
// Live measured center: the resize would put it at fixedStart + trueAnchor
|
|
97
|
+
// (un-anchored), and the currently-applied offset pulls it back by that
|
|
98
|
+
// offset. So centerNow = fixedStart - trueAnchor + appliedOffset.
|
|
99
|
+
const centerNow = {
|
|
100
|
+
x: fixedStart.x - trueAnchor.dx + appliedOffset.x,
|
|
101
|
+
y: fixedStart.y - trueAnchor.dy + appliedOffset.y,
|
|
102
|
+
};
|
|
103
|
+
// ── The fixed logic (accumulate residual onto the previous anchor) ──
|
|
104
|
+
const anchor = computeNextResizeAnchor(lastResizeAnchor, fixedStart, centerNow);
|
|
105
|
+
lastResizeAnchor = anchor;
|
|
106
|
+
anchorsSeen.push(anchor);
|
|
107
|
+
// applyManualOffsetDragDraft sets the absolute offset (scale 1) = anchor.
|
|
108
|
+
appliedOffset = { x: anchor.dx, y: anchor.dy };
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Every frame must report the same, correct anchor — no oscillation, so the
|
|
112
|
+
// committed value is parity-independent.
|
|
113
|
+
for (const a of anchorsSeen) {
|
|
114
|
+
expect(a).toEqual(trueAnchor);
|
|
115
|
+
}
|
|
116
|
+
// Guard against the OLD absolute formula regressing: with `anchor =
|
|
117
|
+
// fixedStart - centerNow` (no accumulation) the sequence would be
|
|
118
|
+
// [trueAnchor, 0, trueAnchor, 0, ...]; assert the last two frames agree.
|
|
119
|
+
expect(anchorsSeen.at(-1)).toEqual(anchorsSeen.at(-2));
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it("the center stays fixed for every corner at any rotation (loop converges)", () => {
|
|
123
|
+
// The pin loop is handle- and rotation-independent: it always measures the
|
|
124
|
+
// element CENTER and drives it back to fixedStart. Simulate the loop for all
|
|
125
|
+
// four corners across unrotated + rotated gestures; the resize's raw center
|
|
126
|
+
// shift varies with corner/rotation (modelled as `rawShift`), but the loop must
|
|
127
|
+
// converge the measured center onto fixedStart every time.
|
|
128
|
+
const fixedStart = { x: 640, y: 360 };
|
|
129
|
+
const HANDLES = ["nw", "ne", "sw", "se"] as const;
|
|
130
|
+
const DEGS = [0, 30, 90, 137];
|
|
131
|
+
for (const handle of HANDLES) {
|
|
132
|
+
for (const deg of DEGS) {
|
|
133
|
+
const t = (deg * Math.PI) / 180;
|
|
134
|
+
// Raw (un-pinned) center shift the size write would cause this frame —
|
|
135
|
+
// a corner/rotation-dependent vector. Its exact value is irrelevant; the
|
|
136
|
+
// loop only needs to cancel it.
|
|
137
|
+
const seed = (HANDLES.indexOf(handle) + 1) * 11;
|
|
138
|
+
const rawShift = {
|
|
139
|
+
dx: Math.cos(t) * seed - Math.sin(t) * (seed / 2),
|
|
140
|
+
dy: Math.sin(t) * seed + Math.cos(t) * (seed / 2),
|
|
141
|
+
};
|
|
142
|
+
let appliedOffset = { x: 0, y: 0 };
|
|
143
|
+
let lastResizeAnchor: { dx: number; dy: number } | undefined;
|
|
144
|
+
let centerNow = { x: fixedStart.x, y: fixedStart.y };
|
|
145
|
+
for (let frame = 0; frame < 6; frame++) {
|
|
146
|
+
centerNow = {
|
|
147
|
+
x: fixedStart.x + rawShift.dx + appliedOffset.x,
|
|
148
|
+
y: fixedStart.y + rawShift.dy + appliedOffset.y,
|
|
149
|
+
};
|
|
150
|
+
const anchor = computeNextResizeAnchor(lastResizeAnchor, fixedStart, centerNow);
|
|
151
|
+
lastResizeAnchor = anchor;
|
|
152
|
+
appliedOffset = { x: anchor.dx, y: anchor.dy };
|
|
153
|
+
}
|
|
154
|
+
// After convergence the pinned center equals the gesture-start center.
|
|
155
|
+
const pinnedCenter = {
|
|
156
|
+
x: fixedStart.x + rawShift.dx + appliedOffset.x,
|
|
157
|
+
y: fixedStart.y + rawShift.dy + appliedOffset.y,
|
|
158
|
+
};
|
|
159
|
+
expect(pinnedCenter.x).toBeCloseTo(fixedStart.x, 9);
|
|
160
|
+
expect(pinnedCenter.y).toBeCloseTo(fixedStart.y, 9);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
});
|
|
164
|
+
|
|
69
165
|
it("net translate after persist+reload equals the committed anchor offset (non-GSAP)", () => {
|
|
70
166
|
// The committed offset flows through the real apply → persist → reload chain
|
|
71
167
|
// unchanged (this hop was proved clean; the shift is upstream in the anchor
|
|
72
|
-
// loop
|
|
168
|
+
// loop above, not in persistence).
|
|
73
169
|
const el = document.createElement("div");
|
|
74
170
|
el.style.setProperty("width", "200px");
|
|
75
171
|
el.style.setProperty("height", "100px");
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
// @vitest-environment jsdom
|
|
2
2
|
import { describe, expect, it } from "vitest";
|
|
3
3
|
import {
|
|
4
|
+
isElementVisibleForZOrder,
|
|
4
5
|
isZOrderActionEnabled,
|
|
5
6
|
parseZIndex,
|
|
7
|
+
resolveCrossedNeighbor,
|
|
6
8
|
resolveZOrderChange,
|
|
7
9
|
type ZOrderAction,
|
|
8
10
|
type ZOrderPatch,
|
|
@@ -48,8 +50,12 @@ function makeFamily(
|
|
|
48
50
|
}
|
|
49
51
|
|
|
50
52
|
/** Resolve a z-order change and assert it produced patches (fails otherwise). */
|
|
51
|
-
function resolveZOrderPatches(
|
|
52
|
-
|
|
53
|
+
function resolveZOrderPatches(
|
|
54
|
+
target: HTMLElement,
|
|
55
|
+
action: ZOrderAction,
|
|
56
|
+
options?: { isVisible?: (el: HTMLElement) => boolean },
|
|
57
|
+
): ZOrderPatch[] {
|
|
58
|
+
const patches = resolveZOrderChange(target, action, options);
|
|
53
59
|
expect(patches).not.toBeNull();
|
|
54
60
|
if (!patches) throw new Error("expected z-order patches");
|
|
55
61
|
return patches;
|
|
@@ -375,6 +381,27 @@ describe("resolveZOrderChange – excludes non-painting siblings", () => {
|
|
|
375
381
|
expect(order.indexOf("target")).toBeLessThan(order.indexOf("a"));
|
|
376
382
|
});
|
|
377
383
|
|
|
384
|
+
it("ignores <template>/<noscript> siblings in the family", () => {
|
|
385
|
+
// A renumber fallback once wrote z-index/position into <template> source
|
|
386
|
+
// markup because templates entered the sibling family. They never paint —
|
|
387
|
+
// exclude them like audio/script/style.
|
|
388
|
+
const parent = document.createElement("div");
|
|
389
|
+
const a = makeEl("a", "0");
|
|
390
|
+
const template = document.createElement("template");
|
|
391
|
+
template.style.zIndex = "2";
|
|
392
|
+
const noscript = document.createElement("noscript");
|
|
393
|
+
const target = makeEl("target", "0");
|
|
394
|
+
parent.append(a, template, noscript, target);
|
|
395
|
+
|
|
396
|
+
const patches = resolveZOrderPatches(target, "send-to-back");
|
|
397
|
+
for (const p of patches) {
|
|
398
|
+
expect(p.element).not.toBe(template);
|
|
399
|
+
expect(p.element).not.toBe(noscript);
|
|
400
|
+
}
|
|
401
|
+
const order = renderOrderIds(parent, { a, target }, patches);
|
|
402
|
+
expect(order.indexOf("target")).toBeLessThan(order.indexOf("a"));
|
|
403
|
+
});
|
|
404
|
+
|
|
378
405
|
it("a lone painting element beside only non-painting siblings has no family → null", () => {
|
|
379
406
|
const parent = document.createElement("div");
|
|
380
407
|
const target = makeEl("target", "1");
|
|
@@ -392,6 +419,163 @@ describe("resolveZOrderChange – excludes non-painting siblings", () => {
|
|
|
392
419
|
});
|
|
393
420
|
});
|
|
394
421
|
|
|
422
|
+
// ── visibility scoping (forward/backward step over VISIBLE siblings only) ──────
|
|
423
|
+
//
|
|
424
|
+
// The visibility probe is injectable (ZOrderResolveOptions.isVisible) exactly
|
|
425
|
+
// like rect reading is stubbable — these tests drive the scoping logic with a
|
|
426
|
+
// stub, without a real style engine.
|
|
427
|
+
|
|
428
|
+
describe("resolveZOrderChange – visibility scoping (injectable stub)", () => {
|
|
429
|
+
/** Probe that hides exactly the given elements. */
|
|
430
|
+
function hiding(...hidden: HTMLElement[]) {
|
|
431
|
+
return { isVisible: (el: HTMLElement) => !hidden.includes(el) };
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
it("bring-forward steps over the next VISIBLE sibling, ignoring an invisible z-neighbor", () => {
|
|
435
|
+
// Render order: target(1), hidden(2), vis(3). The nearest z-neighbor above
|
|
436
|
+
// is invisible at the current frame — forward must land the target above
|
|
437
|
+
// `vis` (the next VISIBLE overlapping sibling), leaving `hidden` untouched.
|
|
438
|
+
const { target, byId } = makeFamily("1", [
|
|
439
|
+
["hidden", "2"],
|
|
440
|
+
["vis", "3"],
|
|
441
|
+
]);
|
|
442
|
+
const patches = resolveZOrderPatches(target, "bring-forward", hiding(byId.hidden!));
|
|
443
|
+
expect(patches).toHaveLength(1);
|
|
444
|
+
expect(patchFor(patches, byId, "target")?.zIndex).toBe(4);
|
|
445
|
+
expect(patchFor(patches, byId, "hidden")).toBeUndefined();
|
|
446
|
+
});
|
|
447
|
+
|
|
448
|
+
it("send-backward steps below the next VISIBLE sibling, ignoring an invisible z-neighbor", () => {
|
|
449
|
+
// Render order: vis(1), hidden(2), target(3). Backward must drop the target
|
|
450
|
+
// below `vis`, not merely below the invisible `hidden`.
|
|
451
|
+
const { target, byId } = makeFamily("3", [
|
|
452
|
+
["vis", "1"],
|
|
453
|
+
["hidden", "2"],
|
|
454
|
+
]);
|
|
455
|
+
const patches = resolveZOrderPatches(target, "send-backward", hiding(byId.hidden!));
|
|
456
|
+
expect(patches).toHaveLength(1);
|
|
457
|
+
expect(patchFor(patches, byId, "target")?.zIndex).toBe(0);
|
|
458
|
+
expect(patchFor(patches, byId, "hidden")).toBeUndefined();
|
|
459
|
+
});
|
|
460
|
+
|
|
461
|
+
it("forward/backward are no-ops when every overlapping sibling is invisible", () => {
|
|
462
|
+
const { target, byId } = makeFamily("1", [["hidden", "2"]]);
|
|
463
|
+
const opts = hiding(byId.hidden!);
|
|
464
|
+
expect(resolveZOrderChange(target, "bring-forward", opts)).toBeNull();
|
|
465
|
+
expect(resolveZOrderChange(target, "send-backward", opts)).toBeNull();
|
|
466
|
+
});
|
|
467
|
+
|
|
468
|
+
it("bring-to-front / send-to-back keep the FULL painting family (invisible siblings included)", () => {
|
|
469
|
+
// Unchanged semantics: front/back operate across all siblings, so an
|
|
470
|
+
// invisible sibling still counts and the actions stay meaningful.
|
|
471
|
+
const { target, byId } = makeFamily("1", [["hidden", "2"]]);
|
|
472
|
+
const opts = hiding(byId.hidden!);
|
|
473
|
+
const patches = resolveZOrderPatches(target, "bring-to-front", opts);
|
|
474
|
+
expect(patchFor(patches, byId, "target")?.zIndex).toBe(3);
|
|
475
|
+
expect(resolveZOrderChange(target, "send-to-back", opts)).toBeNull(); // already bottom
|
|
476
|
+
});
|
|
477
|
+
|
|
478
|
+
it("the target itself is retained even when the probe reports it invisible", () => {
|
|
479
|
+
const { target, byId } = makeFamily("1", [["vis", "2"]]);
|
|
480
|
+
const patches = resolveZOrderPatches(target, "bring-forward", hiding(target));
|
|
481
|
+
expect(patchFor(patches, byId, "target")?.zIndex).toBe(3);
|
|
482
|
+
});
|
|
483
|
+
|
|
484
|
+
it("isZOrderActionEnabled matches the resolver under the same visibility scope", () => {
|
|
485
|
+
const { target, byId } = makeFamily("1", [["hidden", "2"]]);
|
|
486
|
+
const opts = hiding(byId.hidden!);
|
|
487
|
+
// Forward/backward: scoped set collapses to the target alone → disabled.
|
|
488
|
+
expect(isZOrderActionEnabled(target, "bring-forward", opts)).toBe(false);
|
|
489
|
+
expect(isZOrderActionEnabled(target, "send-backward", opts)).toBe(false);
|
|
490
|
+
// Front/back: full family → enabled exactly where the resolver acts.
|
|
491
|
+
expect(isZOrderActionEnabled(target, "bring-to-front", opts)).toBe(true);
|
|
492
|
+
expect(isZOrderActionEnabled(target, "send-to-back", opts)).toBe(false);
|
|
493
|
+
});
|
|
494
|
+
});
|
|
495
|
+
|
|
496
|
+
// ── default visibility probe (element-level computed style) ───────────────────
|
|
497
|
+
|
|
498
|
+
describe("isElementVisibleForZOrder – default probe", () => {
|
|
499
|
+
function attachedEl(style: Partial<CSSStyleDeclaration> = {}): HTMLElement {
|
|
500
|
+
const el = document.createElement("div");
|
|
501
|
+
Object.assign(el.style, style);
|
|
502
|
+
document.body.appendChild(el);
|
|
503
|
+
return el;
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
it("treats display:none / visibility:hidden / opacity≈0 as invisible", () => {
|
|
507
|
+
expect(isElementVisibleForZOrder(attachedEl({ display: "none" }))).toBe(false);
|
|
508
|
+
expect(isElementVisibleForZOrder(attachedEl({ visibility: "hidden" }))).toBe(false);
|
|
509
|
+
expect(isElementVisibleForZOrder(attachedEl({ opacity: "0" }))).toBe(false);
|
|
510
|
+
expect(isElementVisibleForZOrder(attachedEl({ opacity: "0.005" }))).toBe(false);
|
|
511
|
+
});
|
|
512
|
+
|
|
513
|
+
it("treats normal, translucent, and unstyled elements as visible", () => {
|
|
514
|
+
expect(isElementVisibleForZOrder(attachedEl())).toBe(true);
|
|
515
|
+
expect(isElementVisibleForZOrder(attachedEl({ opacity: "0.5" }))).toBe(true);
|
|
516
|
+
expect(isElementVisibleForZOrder(attachedEl({ visibility: "visible" }))).toBe(true);
|
|
517
|
+
});
|
|
518
|
+
|
|
519
|
+
it("exempts a hidden color-grading source (its canvas paints in its place)", () => {
|
|
520
|
+
const el = attachedEl({ opacity: "0" });
|
|
521
|
+
el.setAttribute("data-hf-color-grading-source-hidden", "");
|
|
522
|
+
expect(isElementVisibleForZOrder(el)).toBe(true);
|
|
523
|
+
});
|
|
524
|
+
|
|
525
|
+
it("is the default probe: the runtime's inline visibility:hidden on a time-inactive clip is skipped", () => {
|
|
526
|
+
// End-to-end through resolveZOrderChange with NO injected probe: the
|
|
527
|
+
// runtime hides inactive clips with inline `visibility:hidden` (see core
|
|
528
|
+
// runtime syncTimedElementVisibility) — computed style picks that up.
|
|
529
|
+
const parent = document.createElement("div");
|
|
530
|
+
const target = makeEl("target", "1");
|
|
531
|
+
const hidden = makeEl("hidden", "2");
|
|
532
|
+
hidden.style.visibility = "hidden";
|
|
533
|
+
const vis = makeEl("vis", "3");
|
|
534
|
+
parent.append(target, hidden, vis);
|
|
535
|
+
document.body.appendChild(parent);
|
|
536
|
+
const patches = resolveZOrderPatches(target, "bring-forward");
|
|
537
|
+
expect(patchFor(patches, { target, hidden, vis }, "target")?.zIndex).toBe(4);
|
|
538
|
+
expect(patchFor(patches, { target, hidden, vis }, "hidden")).toBeUndefined();
|
|
539
|
+
});
|
|
540
|
+
});
|
|
541
|
+
|
|
542
|
+
// ── resolveCrossedNeighbor (the "show your work" flash target) ────────────────
|
|
543
|
+
|
|
544
|
+
describe("resolveCrossedNeighbor", () => {
|
|
545
|
+
it("returns the visible sibling directly above for bring-forward", () => {
|
|
546
|
+
const { target, byId } = makeFamily("1", [
|
|
547
|
+
["hidden", "2"],
|
|
548
|
+
["vis", "3"],
|
|
549
|
+
]);
|
|
550
|
+
const opts = { isVisible: (el: HTMLElement) => el !== byId.hidden };
|
|
551
|
+
expect(resolveCrossedNeighbor(target, "bring-forward", opts)).toBe(byId.vis);
|
|
552
|
+
});
|
|
553
|
+
|
|
554
|
+
it("returns the visible sibling directly below for send-backward", () => {
|
|
555
|
+
const { target, byId } = makeFamily("3", [
|
|
556
|
+
["vis", "1"],
|
|
557
|
+
["hidden", "2"],
|
|
558
|
+
]);
|
|
559
|
+
const opts = { isVisible: (el: HTMLElement) => el !== byId.hidden };
|
|
560
|
+
expect(resolveCrossedNeighbor(target, "send-backward", opts)).toBe(byId.vis);
|
|
561
|
+
});
|
|
562
|
+
|
|
563
|
+
it("returns null for front/back actions and for no-op steps", () => {
|
|
564
|
+
const { target } = makeFamily("1", [["a", "2"]]);
|
|
565
|
+
expect(resolveCrossedNeighbor(target, "bring-to-front")).toBeNull();
|
|
566
|
+
expect(resolveCrossedNeighbor(target, "send-to-back")).toBeNull();
|
|
567
|
+
expect(resolveCrossedNeighbor(target, "send-backward")).toBeNull(); // already bottom
|
|
568
|
+
const { target: top } = makeFamily("5", [["a", "2"]]);
|
|
569
|
+
expect(resolveCrossedNeighbor(top, "bring-forward")).toBeNull(); // already top
|
|
570
|
+
});
|
|
571
|
+
|
|
572
|
+
it("returns null when there are no siblings", () => {
|
|
573
|
+
const solo = makeEl("solo", "1");
|
|
574
|
+
document.createElement("div").appendChild(solo);
|
|
575
|
+
expect(resolveCrossedNeighbor(solo, "bring-forward")).toBeNull();
|
|
576
|
+
});
|
|
577
|
+
});
|
|
578
|
+
|
|
395
579
|
// ── isZOrderActionEnabled ─────────────────────────────────────────────────────
|
|
396
580
|
|
|
397
581
|
describe("isZOrderActionEnabled", () => {
|