@hyperframes/studio 0.7.55 → 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-BXaqaVKt.js → index-BWnOxAiH.js} +1 -1
- package/dist/assets/{index-CPetwHFV.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 +40414 -37680
- 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/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 +106 -10
- 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 +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/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 +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 +120 -61
- 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/timelineTheme.ts +6 -1
- 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/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/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.ts +62 -0
- package/src/utils/timelineDiscovery.ts +0 -17
- 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
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import { memo, useEffect, useMemo, useRef, useState, type RefObject } from "react";
|
|
2
|
-
import { getPreviewTargetFromPointer } from "../../utils/studioPreviewHelpers";
|
|
1
|
+
import { memo, useCallback, useEffect, useMemo, useRef, useState, type RefObject } from "react";
|
|
3
2
|
import { type DomEditSelection } from "./domEditing";
|
|
4
3
|
import type { PreviewMouseDownOptions } from "../../hooks/usePreviewInteraction";
|
|
5
4
|
import { useMarqueeGestures } from "./marqueeCommit";
|
|
@@ -16,17 +15,20 @@ import {
|
|
|
16
15
|
import { useDomEditOverlayRects } from "./useDomEditOverlayRects";
|
|
17
16
|
import { OffCanvasIndicators, type OffCanvasRect } from "./OffCanvasIndicators";
|
|
18
17
|
import { createDomEditOverlayGestureHandlers } from "./useDomEditOverlayGestures";
|
|
18
|
+
import { useDomEditNudge } from "./useDomEditNudge";
|
|
19
19
|
import { SnapGuideOverlay, type SnapGuidesState } from "./SnapGuideOverlay";
|
|
20
20
|
import { GridOverlay } from "./GridOverlay";
|
|
21
21
|
import type { GestureRecordingState } from "./GestureRecordControl";
|
|
22
|
-
import {
|
|
23
|
-
import { DomEditRotateHandle } from "./DomEditRotateHandle";
|
|
22
|
+
import { DomEditGroupChrome, DomEditSelectionChrome } from "./DomEditSelectionChrome";
|
|
24
23
|
import { hugRectForElement } from "./domEditOverlayCrop";
|
|
25
24
|
import { useCropOverlay } from "../../hooks/useCropOverlay";
|
|
26
25
|
import { readDomEditSelectionShapeStyles, resolveBoxChromeClass } from "./domEditOverlayShape";
|
|
27
26
|
import { useDomEditCompositionRect } from "./useDomEditCompositionRect";
|
|
28
27
|
import { useMountEffect } from "../../hooks/useMountEffect";
|
|
29
28
|
import { startOffCanvasIndicatorRefresh } from "./offCanvasIndicatorRefresh";
|
|
29
|
+
import { CanvasContextMenu } from "./CanvasContextMenu";
|
|
30
|
+
import type { ZOrderPatch } from "./canvasContextMenuZOrder";
|
|
31
|
+
import { getPreviewTargetFromPointer } from "../../utils/studioPreviewHelpers";
|
|
30
32
|
|
|
31
33
|
// Re-exports for external consumers — preserving existing import paths.
|
|
32
34
|
export {
|
|
@@ -37,7 +39,6 @@ export {
|
|
|
37
39
|
export {
|
|
38
40
|
focusDomEditOverlayElement,
|
|
39
41
|
hasDomEditRotationChanged,
|
|
40
|
-
resolveDomEditResizeGesture,
|
|
41
42
|
resolveDomEditRotationGesture,
|
|
42
43
|
} from "./domEditOverlayGestures";
|
|
43
44
|
export type { DomEditGroupPathOffsetCommit } from "./domEditOverlayGestures";
|
|
@@ -73,6 +74,8 @@ interface DomEditOverlayProps {
|
|
|
73
74
|
onBoxSizeCommit: (
|
|
74
75
|
selection: DomEditSelection,
|
|
75
76
|
next: { width: number; height: number },
|
|
77
|
+
offset?: { x: number; y: number },
|
|
78
|
+
restore?: () => void,
|
|
76
79
|
) => Promise<void> | void;
|
|
77
80
|
onRotationCommit: (selection: DomEditSelection, next: { angle: number }) => Promise<void> | void;
|
|
78
81
|
onStyleCommit?: (property: string, value: string) => Promise<void> | void;
|
|
@@ -81,6 +84,19 @@ interface DomEditOverlayProps {
|
|
|
81
84
|
recordingState?: GestureRecordingState;
|
|
82
85
|
onToggleRecording?: () => void;
|
|
83
86
|
onMarqueeSelect?: (selections: DomEditSelection[], additive: boolean) => void;
|
|
87
|
+
/**
|
|
88
|
+
* Delete the selected canvas element.
|
|
89
|
+
* Wire to handleDomEditElementDelete from useDomEditActionsContext —
|
|
90
|
+
* same handler the Delete/Backspace hotkey uses.
|
|
91
|
+
*/
|
|
92
|
+
onDeleteSelection?: (selection: DomEditSelection) => void;
|
|
93
|
+
/**
|
|
94
|
+
* Called with the resolved z-order patch list after an optimistic DOM update.
|
|
95
|
+
* The patch list is tie-aware and may include sibling elements (see
|
|
96
|
+
* canvasContextMenuZOrder). Wire to handleDomZIndexReorderCommit from
|
|
97
|
+
* useDomEditActionsContext. See CanvasContextMenu.tsx module comment.
|
|
98
|
+
*/
|
|
99
|
+
onApplyZIndex?: (selection: DomEditSelection, patches: ZOrderPatch[]) => void;
|
|
84
100
|
}
|
|
85
101
|
|
|
86
102
|
// fallow-ignore-next-line complexity
|
|
@@ -105,6 +121,8 @@ export const DomEditOverlay = memo(function DomEditOverlay({
|
|
|
105
121
|
onRotationCommit,
|
|
106
122
|
onStyleCommit,
|
|
107
123
|
onMarqueeSelect,
|
|
124
|
+
onDeleteSelection,
|
|
125
|
+
onApplyZIndex,
|
|
108
126
|
}: DomEditOverlayProps) {
|
|
109
127
|
const overlayRef = useRef<HTMLDivElement | null>(null);
|
|
110
128
|
const boxRef = useRef<HTMLDivElement | null>(null);
|
|
@@ -121,8 +139,31 @@ export const DomEditOverlay = memo(function DomEditOverlay({
|
|
|
121
139
|
const snapGuidesRef = useRef<SnapGuidesState | null>(null);
|
|
122
140
|
const rafPausedRef = useRef(false);
|
|
123
141
|
|
|
142
|
+
// Context menu state: position of the right-click that opened it.
|
|
143
|
+
// contextMenuSelection is the element the menu targets — captured at right-click
|
|
144
|
+
// time so the menu can open even before the React selection state settles.
|
|
145
|
+
const [contextMenu, setContextMenu] = useState<{
|
|
146
|
+
x: number;
|
|
147
|
+
y: number;
|
|
148
|
+
sel: DomEditSelection;
|
|
149
|
+
} | null>(null);
|
|
150
|
+
|
|
124
151
|
const selectionRef = useRef(selection);
|
|
125
152
|
selectionRef.current = selection;
|
|
153
|
+
|
|
154
|
+
// Close the context menu whenever the selection moves off the element the menu
|
|
155
|
+
// targets (a click that reselects elsewhere, a deselect, or a preview reload
|
|
156
|
+
// that rebuilds the selection). Without this the menu can linger — orphaned —
|
|
157
|
+
// over a stale target after the underlying element is gone. A right-click that
|
|
158
|
+
// OPENS the menu also selects its target, so the common open path keeps the
|
|
159
|
+
// menu (same element) rather than immediately dismissing it.
|
|
160
|
+
useEffect(() => {
|
|
161
|
+
if (!contextMenu) return;
|
|
162
|
+
if (!selection || selection.element !== contextMenu.sel.element) {
|
|
163
|
+
setContextMenu(null);
|
|
164
|
+
}
|
|
165
|
+
}, [selection, contextMenu]);
|
|
166
|
+
|
|
126
167
|
const activeCompositionPathRef = useRef(activeCompositionPath);
|
|
127
168
|
activeCompositionPathRef.current = activeCompositionPath;
|
|
128
169
|
const groupSelectionsRef = useRef(groupSelections);
|
|
@@ -241,6 +282,23 @@ export const DomEditOverlay = memo(function DomEditOverlay({
|
|
|
241
282
|
snapGuidesRef,
|
|
242
283
|
});
|
|
243
284
|
|
|
285
|
+
// Arrow-key nudge (1px, Shift = 10px) — commits through the same
|
|
286
|
+
// path-offset callbacks as a drag, one undo entry per key burst.
|
|
287
|
+
const { flushNudge } = useDomEditNudge({
|
|
288
|
+
selection,
|
|
289
|
+
groupSelections,
|
|
290
|
+
allowCanvasMovement,
|
|
291
|
+
selectionRef,
|
|
292
|
+
overlayRectRef,
|
|
293
|
+
groupOverlayItemsRef,
|
|
294
|
+
gestureRef,
|
|
295
|
+
groupGestureRef,
|
|
296
|
+
blockedMoveRef,
|
|
297
|
+
onManualDragStartRef,
|
|
298
|
+
onPathOffsetCommitRef,
|
|
299
|
+
onGroupPathOffsetCommitRef,
|
|
300
|
+
});
|
|
301
|
+
|
|
244
302
|
const marquee = useMarqueeGestures({
|
|
245
303
|
iframeRef,
|
|
246
304
|
overlayRef,
|
|
@@ -370,6 +428,38 @@ export const DomEditOverlay = memo(function DomEditOverlay({
|
|
|
370
428
|
e.stopPropagation();
|
|
371
429
|
};
|
|
372
430
|
|
|
431
|
+
// Right-click: select element first (if not already selected), then open menu.
|
|
432
|
+
const handleContextMenu = useCallback(
|
|
433
|
+
async (event: React.MouseEvent<HTMLDivElement>) => {
|
|
434
|
+
event.preventDefault();
|
|
435
|
+
|
|
436
|
+
// If no element is selected yet, resolve it from the pointer position first.
|
|
437
|
+
const currentSel = selectionRef.current;
|
|
438
|
+
let activeSel: DomEditSelection | null = currentSel;
|
|
439
|
+
if (!currentSel) {
|
|
440
|
+
const pointerEvent = event as unknown as React.PointerEvent<HTMLDivElement>;
|
|
441
|
+
const resolved = await onCanvasPointerMoveRef.current(pointerEvent);
|
|
442
|
+
if (!resolved) return; // Nothing under the cursor — skip menu.
|
|
443
|
+
onSelectionChangeRef.current(resolved, { revealPanel: true });
|
|
444
|
+
// Use `resolved` directly: React state (and therefore selectionRef) won't
|
|
445
|
+
// update synchronously after onSelectionChange — we'd be reading stale null.
|
|
446
|
+
activeSel = resolved;
|
|
447
|
+
} else {
|
|
448
|
+
// Check if the user right-clicked on an unselected element (hover target).
|
|
449
|
+
const hover = hoverSelectionRef.current;
|
|
450
|
+
if (hover && hover.element !== currentSel.element) {
|
|
451
|
+
onSelectionChangeRef.current(hover, { revealPanel: true });
|
|
452
|
+
activeSel = hover;
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
if (!activeSel) return;
|
|
457
|
+
setContextMenu({ x: event.clientX, y: event.clientY, sel: activeSel });
|
|
458
|
+
},
|
|
459
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
460
|
+
[],
|
|
461
|
+
);
|
|
462
|
+
|
|
373
463
|
return (
|
|
374
464
|
<div
|
|
375
465
|
ref={overlayRef}
|
|
@@ -378,142 +468,59 @@ export const DomEditOverlay = memo(function DomEditOverlay({
|
|
|
378
468
|
aria-label="Composition canvas"
|
|
379
469
|
// Cursor follows marquee rect *state* (re-renders), not the mutable ref.
|
|
380
470
|
style={marquee.marqueeRect ? { cursor: "crosshair" } : undefined}
|
|
381
|
-
onPointerDownCapture={(event) =>
|
|
382
|
-
|
|
383
|
-
|
|
471
|
+
onPointerDownCapture={(event) => {
|
|
472
|
+
// A pointer gesture supersedes a pending nudge burst — commit it first
|
|
473
|
+
// so the gesture's member snapshot starts from the nudged position.
|
|
474
|
+
flushNudge();
|
|
475
|
+
focusDomEditOverlayElement(event.currentTarget as FocusableDomEditOverlay);
|
|
476
|
+
}}
|
|
384
477
|
onPointerDown={handleOverlayPointerDown}
|
|
385
478
|
onMouseDown={handleOverlayMouseDown}
|
|
386
479
|
onPointerMove={marquee.onPointerMove}
|
|
387
480
|
onPointerLeave={() => onCanvasPointerLeaveRef.current()}
|
|
388
481
|
onPointerUp={marquee.onPointerUp}
|
|
389
482
|
onPointerCancel={marquee.onPointerCancel}
|
|
483
|
+
onContextMenu={handleContextMenu}
|
|
390
484
|
>
|
|
391
485
|
{hoverSelection && hoverRect && compRect.width > 0 && (
|
|
392
486
|
<div
|
|
393
487
|
aria-hidden="true"
|
|
394
488
|
data-dom-edit-hover-box="true"
|
|
395
489
|
className="pointer-events-none absolute rounded-md border border-studio-accent/80 shadow-[0_0_0_1px_rgba(60,230,172,0.25)]"
|
|
396
|
-
style={
|
|
490
|
+
style={{
|
|
491
|
+
...hugRectForElement(hoverRect, hoverSelection.element),
|
|
492
|
+
transform: hoverRect.angle ? `rotate(${hoverRect.angle}deg)` : undefined,
|
|
493
|
+
}}
|
|
397
494
|
/>
|
|
398
495
|
)}
|
|
399
496
|
{hasGroupSelection && groupOverlayItems.length > 1 && groupBounds && compRect.width > 0 && (
|
|
400
|
-
|
|
401
|
-
{groupOverlayItems
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
width: item.rect.width,
|
|
410
|
-
height: item.rect.height,
|
|
411
|
-
}}
|
|
412
|
-
/>
|
|
413
|
-
))}
|
|
414
|
-
<div
|
|
415
|
-
data-dom-edit-selection-box="true"
|
|
416
|
-
className="pointer-events-auto absolute rounded-xl border border-studio-accent shadow-[0_0_0_1px_rgba(60,230,172,0.3)]"
|
|
417
|
-
style={{
|
|
418
|
-
left: groupBounds.left,
|
|
419
|
-
top: groupBounds.top,
|
|
420
|
-
width: groupBounds.width,
|
|
421
|
-
height: groupBounds.height,
|
|
422
|
-
cursor: allowCanvasMovement && groupCanMove ? "move" : "default",
|
|
423
|
-
}}
|
|
424
|
-
onPointerDown={(e) => {
|
|
425
|
-
if (!allowCanvasMovement || !groupCanMove || e.shiftKey) return;
|
|
426
|
-
gestures.startGroupDrag(e);
|
|
427
|
-
}}
|
|
428
|
-
onMouseDown={suppressBoxMouseDown}
|
|
429
|
-
onClick={handleBoxClick}
|
|
430
|
-
/>
|
|
431
|
-
</>
|
|
497
|
+
<DomEditGroupChrome
|
|
498
|
+
groupOverlayItems={groupOverlayItems}
|
|
499
|
+
groupBounds={groupBounds}
|
|
500
|
+
allowCanvasMovement={allowCanvasMovement}
|
|
501
|
+
groupCanMove={groupCanMove}
|
|
502
|
+
gestures={gestures}
|
|
503
|
+
onBoxMouseDown={suppressBoxMouseDown}
|
|
504
|
+
onBoxClick={handleBoxClick}
|
|
505
|
+
/>
|
|
432
506
|
)}
|
|
433
507
|
{!hasGroupSelection && selection && overlayRect && compRect.width > 0 && (
|
|
434
|
-
|
|
435
|
-
{
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
style={{
|
|
451
|
-
left: overlayRect.left,
|
|
452
|
-
top: overlayRect.top,
|
|
453
|
-
width: overlayRect.width,
|
|
454
|
-
height: overlayRect.height,
|
|
455
|
-
clipPath: boxClipPath,
|
|
456
|
-
cursor:
|
|
457
|
-
allowCanvasMovement && selection.capabilities.canApplyManualOffset
|
|
458
|
-
? "move"
|
|
459
|
-
: "default",
|
|
460
|
-
}}
|
|
461
|
-
onPointerDown={(e) => {
|
|
462
|
-
if (!allowCanvasMovement || e.shiftKey) return;
|
|
463
|
-
if (selection.capabilities.canApplyManualOffset) {
|
|
464
|
-
gestures.startGesture("drag", e);
|
|
465
|
-
return;
|
|
466
|
-
}
|
|
467
|
-
e.preventDefault();
|
|
468
|
-
e.stopPropagation();
|
|
469
|
-
e.currentTarget.setPointerCapture(e.pointerId);
|
|
470
|
-
blockedMoveRef.current = {
|
|
471
|
-
pointerId: e.pointerId,
|
|
472
|
-
startX: e.clientX,
|
|
473
|
-
startY: e.clientY,
|
|
474
|
-
notified: false,
|
|
475
|
-
};
|
|
476
|
-
}}
|
|
477
|
-
onMouseDown={suppressBoxMouseDown}
|
|
478
|
-
onClick={handleBoxClick}
|
|
479
|
-
>
|
|
480
|
-
{cropOutlineInsetPx && (
|
|
481
|
-
<div
|
|
482
|
-
className="pointer-events-none absolute rounded-md border border-studio-accent/80 shadow-[0_0_0_1px_rgba(60,230,172,0.25)]"
|
|
483
|
-
style={{
|
|
484
|
-
left: cropOutlineInsetPx.left,
|
|
485
|
-
top: cropOutlineInsetPx.top,
|
|
486
|
-
right: cropOutlineInsetPx.right,
|
|
487
|
-
bottom: cropOutlineInsetPx.bottom,
|
|
488
|
-
}}
|
|
489
|
-
/>
|
|
490
|
-
)}
|
|
491
|
-
{allowCanvasMovement && selection.capabilities.canApplyManualSize && (
|
|
492
|
-
<div
|
|
493
|
-
className="absolute -right-1.5 -bottom-1.5 w-3 h-3 rounded-sm bg-studio-accent border border-studio-accent/60"
|
|
494
|
-
style={{
|
|
495
|
-
cursor: "se-resize",
|
|
496
|
-
touchAction: "none",
|
|
497
|
-
...(cropOutlineInsetPx && {
|
|
498
|
-
right: cropOutlineInsetPx.right - 6,
|
|
499
|
-
bottom: cropOutlineInsetPx.bottom - 6,
|
|
500
|
-
}),
|
|
501
|
-
}}
|
|
502
|
-
onPointerDown={(e) => {
|
|
503
|
-
e.stopPropagation();
|
|
504
|
-
gestures.startGesture("resize", e);
|
|
505
|
-
}}
|
|
506
|
-
/>
|
|
507
|
-
)}
|
|
508
|
-
</div>
|
|
509
|
-
{selection.capabilities.canCrop && groupSelections.length <= 1 && (
|
|
510
|
-
<DomEditCropHandles
|
|
511
|
-
selection={selection}
|
|
512
|
-
overlayRect={overlayRect}
|
|
513
|
-
onStyleCommit={onStyleCommitRef.current}
|
|
514
|
-
/>
|
|
515
|
-
)}
|
|
516
|
-
</>
|
|
508
|
+
<DomEditSelectionChrome
|
|
509
|
+
selection={selection}
|
|
510
|
+
overlayRect={overlayRect}
|
|
511
|
+
allowCanvasMovement={allowCanvasMovement}
|
|
512
|
+
cropOutlineInsetPx={cropOutlineInsetPx ?? undefined}
|
|
513
|
+
boxRef={boxRef}
|
|
514
|
+
boxChromeClass={boxChromeClass}
|
|
515
|
+
boxClipPath={boxClipPath}
|
|
516
|
+
selectionKey={selectionKey}
|
|
517
|
+
groupSelectionCount={groupSelections.length}
|
|
518
|
+
blockedMoveRef={blockedMoveRef}
|
|
519
|
+
gestures={gestures}
|
|
520
|
+
onStyleCommit={onStyleCommitRef.current}
|
|
521
|
+
onBoxMouseDown={suppressBoxMouseDown}
|
|
522
|
+
onBoxClick={handleBoxClick}
|
|
523
|
+
/>
|
|
517
524
|
)}
|
|
518
525
|
{childRects.length > 0 &&
|
|
519
526
|
compRect.width > 0 &&
|
|
@@ -539,6 +546,29 @@ export const DomEditOverlay = memo(function DomEditOverlay({
|
|
|
539
546
|
onSelectionChangeRef={onSelectionChangeRef}
|
|
540
547
|
/>
|
|
541
548
|
<MarqueeOverlay candidateRects={marquee.candidateRects} marqueeRect={marquee.marqueeRect} />
|
|
549
|
+
{contextMenu && (
|
|
550
|
+
<CanvasContextMenu
|
|
551
|
+
x={contextMenu.x}
|
|
552
|
+
y={contextMenu.y}
|
|
553
|
+
selection={contextMenu.sel}
|
|
554
|
+
onClose={() => setContextMenu(null)}
|
|
555
|
+
onDelete={
|
|
556
|
+
onDeleteSelection
|
|
557
|
+
? (sel) => {
|
|
558
|
+
setContextMenu(null);
|
|
559
|
+
onDeleteSelection(sel);
|
|
560
|
+
}
|
|
561
|
+
: undefined
|
|
562
|
+
}
|
|
563
|
+
onApplyZIndex={
|
|
564
|
+
onApplyZIndex
|
|
565
|
+
? (patches) => {
|
|
566
|
+
onApplyZIndex(contextMenu.sel, patches);
|
|
567
|
+
}
|
|
568
|
+
: undefined
|
|
569
|
+
}
|
|
570
|
+
/>
|
|
571
|
+
)}
|
|
542
572
|
<GridOverlay
|
|
543
573
|
visible={gridVisible}
|
|
544
574
|
spacing={gridSpacing}
|
|
@@ -551,8 +581,10 @@ export const DomEditOverlay = memo(function DomEditOverlay({
|
|
|
551
581
|
/>
|
|
552
582
|
<SnapGuideOverlay
|
|
553
583
|
snapGuidesRef={snapGuidesRef}
|
|
554
|
-
|
|
555
|
-
|
|
584
|
+
compositionLeft={compRect.left}
|
|
585
|
+
compositionTop={compRect.top}
|
|
586
|
+
compositionWidth={compRect.width}
|
|
587
|
+
compositionHeight={compRect.height}
|
|
556
588
|
/>
|
|
557
589
|
</div>
|
|
558
590
|
);
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
// @vitest-environment happy-dom
|
|
2
|
+
|
|
3
|
+
import React, { act } from "react";
|
|
4
|
+
import { createRoot } from "react-dom/client";
|
|
5
|
+
import { expect, it, vi } from "vitest";
|
|
6
|
+
import type { DomEditSelection } from "./domEditing";
|
|
7
|
+
import { DomEditOverlay } from "./DomEditOverlay";
|
|
8
|
+
|
|
9
|
+
(globalThis as unknown as { IS_REACT_ACT_ENVIRONMENT: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
|
|
10
|
+
|
|
11
|
+
const hoverState = vi.hoisted(() => ({ angle: 0 }));
|
|
12
|
+
|
|
13
|
+
vi.mock("./useDomEditOverlayRects", () => ({
|
|
14
|
+
useDomEditOverlayRects: ({ hoverSelectionRef }: { hoverSelectionRef: { current: unknown } }) => ({
|
|
15
|
+
overlayRect: null,
|
|
16
|
+
overlayRectRef: { current: null },
|
|
17
|
+
setOverlayRect: () => undefined,
|
|
18
|
+
hoverRect: hoverSelectionRef.current
|
|
19
|
+
? {
|
|
20
|
+
left: 20,
|
|
21
|
+
top: 30,
|
|
22
|
+
width: 100,
|
|
23
|
+
height: 40,
|
|
24
|
+
editScaleX: 1,
|
|
25
|
+
editScaleY: 1,
|
|
26
|
+
angle: hoverState.angle,
|
|
27
|
+
}
|
|
28
|
+
: null,
|
|
29
|
+
groupOverlayItems: [],
|
|
30
|
+
groupOverlayItemsRef: { current: [] },
|
|
31
|
+
setGroupOverlayItems: () => undefined,
|
|
32
|
+
childRects: [],
|
|
33
|
+
}),
|
|
34
|
+
}));
|
|
35
|
+
|
|
36
|
+
vi.mock("./useDomEditCompositionRect", () => ({
|
|
37
|
+
useDomEditCompositionRect: () => ({
|
|
38
|
+
left: 0,
|
|
39
|
+
top: 0,
|
|
40
|
+
width: 800,
|
|
41
|
+
height: 450,
|
|
42
|
+
scaleX: 1,
|
|
43
|
+
scaleY: 1,
|
|
44
|
+
}),
|
|
45
|
+
}));
|
|
46
|
+
|
|
47
|
+
vi.mock("./offCanvasIndicatorRefresh", () => ({
|
|
48
|
+
startOffCanvasIndicatorRefresh: () => () => undefined,
|
|
49
|
+
}));
|
|
50
|
+
|
|
51
|
+
function renderHover(angle: number): string {
|
|
52
|
+
hoverState.angle = angle;
|
|
53
|
+
const host = document.createElement("div");
|
|
54
|
+
document.body.append(host);
|
|
55
|
+
const root = createRoot(host);
|
|
56
|
+
const element = document.createElement("div");
|
|
57
|
+
const hoverSelection = { element } as unknown as DomEditSelection;
|
|
58
|
+
act(() => {
|
|
59
|
+
root.render(
|
|
60
|
+
<DomEditOverlay
|
|
61
|
+
iframeRef={{ current: document.createElement("iframe") }}
|
|
62
|
+
activeCompositionPath={null}
|
|
63
|
+
selection={null}
|
|
64
|
+
hoverSelection={hoverSelection}
|
|
65
|
+
onCanvasMouseDown={() => undefined}
|
|
66
|
+
onCanvasPointerMove={() => Promise.resolve(null)}
|
|
67
|
+
onCanvasPointerLeave={() => undefined}
|
|
68
|
+
onSelectionChange={() => undefined}
|
|
69
|
+
onBlockedMove={() => undefined}
|
|
70
|
+
onPathOffsetCommit={() => undefined}
|
|
71
|
+
onGroupPathOffsetCommit={() => undefined}
|
|
72
|
+
onBoxSizeCommit={() => undefined}
|
|
73
|
+
onRotationCommit={() => undefined}
|
|
74
|
+
/>,
|
|
75
|
+
);
|
|
76
|
+
});
|
|
77
|
+
const box = host.querySelector<HTMLElement>('[data-dom-edit-hover-box="true"]');
|
|
78
|
+
const transform = box?.style.transform ?? "";
|
|
79
|
+
act(() => root.unmount());
|
|
80
|
+
host.remove();
|
|
81
|
+
return transform;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
it("rotates the hover box with the element", () => {
|
|
85
|
+
expect(renderHover(30)).toBe("rotate(30deg)");
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it("leaves the hover box untransformed at angle zero", () => {
|
|
89
|
+
expect(renderHover(0)).toBe("");
|
|
90
|
+
});
|
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
import type { PointerEvent as ReactPointerEvent } from "react";
|
|
2
2
|
import type { OverlayRect } from "./domEditOverlayGeometry";
|
|
3
3
|
|
|
4
|
-
/** Rotate
|
|
5
|
-
*
|
|
4
|
+
/** Rotate handle below the selection: an attached circular-arrows icon chip
|
|
5
|
+
* (no connecting stem). Anchors to the crop outline when the element is
|
|
6
|
+
* cropped so it stays next to what's visible on screen. Presentation only —
|
|
7
|
+
* the rotation gesture measures pointer angles from the element CENTER
|
|
8
|
+
* (resolveDomEditRotationGesture), so the handle position doesn't affect the
|
|
9
|
+
* math. Sits 12px below the bbox, past the bottom crop handle's hit strip. */
|
|
6
10
|
export function DomEditRotateHandle({
|
|
7
11
|
overlayRect,
|
|
8
12
|
cropOutlineInsetPx,
|
|
@@ -15,27 +19,43 @@ export function DomEditRotateHandle({
|
|
|
15
19
|
const inset = cropOutlineInsetPx ?? { top: 0, right: 0, bottom: 0, left: 0 };
|
|
16
20
|
const visibleLeft = overlayRect.left + inset.left;
|
|
17
21
|
const visibleWidth = Math.max(0, overlayRect.width - inset.left - inset.right);
|
|
18
|
-
const
|
|
22
|
+
const visibleBottom = overlayRect.top + overlayRect.height - inset.bottom;
|
|
19
23
|
return (
|
|
20
|
-
<
|
|
21
|
-
|
|
24
|
+
<button
|
|
25
|
+
type="button"
|
|
26
|
+
className="pointer-events-auto absolute flex items-center justify-center border-0 bg-transparent p-0"
|
|
22
27
|
style={{
|
|
23
28
|
left: visibleLeft + visibleWidth / 2,
|
|
24
|
-
top:
|
|
25
|
-
width:
|
|
26
|
-
height:
|
|
29
|
+
top: visibleBottom + 12,
|
|
30
|
+
width: 22,
|
|
31
|
+
height: 22,
|
|
27
32
|
transform: "translateX(-50%)",
|
|
33
|
+
touchAction: "none",
|
|
34
|
+
// Closed-hand grab cursor: this handle is grabbed and dragged to rotate.
|
|
35
|
+
cursor: "grabbing",
|
|
28
36
|
}}
|
|
37
|
+
title="Rotate"
|
|
38
|
+
aria-label="Rotate selection"
|
|
39
|
+
onPointerDown={onStartRotate}
|
|
29
40
|
>
|
|
30
|
-
<
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
41
|
+
<span className="pointer-events-none flex h-[18px] w-[18px] items-center justify-center rounded-full border border-studio-accent/70 bg-studio-surface text-studio-accent shadow-[0_0_3px_rgba(0,0,0,0.45)]">
|
|
42
|
+
<svg
|
|
43
|
+
width="11"
|
|
44
|
+
height="11"
|
|
45
|
+
viewBox="0 0 24 24"
|
|
46
|
+
fill="none"
|
|
47
|
+
stroke="currentColor"
|
|
48
|
+
strokeWidth="2.5"
|
|
49
|
+
strokeLinecap="round"
|
|
50
|
+
strokeLinejoin="round"
|
|
51
|
+
aria-hidden="true"
|
|
52
|
+
>
|
|
53
|
+
<path d="M3 12a9 9 0 0 1 9-9 9.75 9.75 0 0 1 6.74 2.74L21 8" />
|
|
54
|
+
<path d="M21 3v5h-5" />
|
|
55
|
+
<path d="M21 12a9 9 0 0 1-9 9 9.75 9.75 0 0 1-6.74-2.74L3 16" />
|
|
56
|
+
<path d="M8 16H3v5" />
|
|
57
|
+
</svg>
|
|
58
|
+
</span>
|
|
59
|
+
</button>
|
|
40
60
|
);
|
|
41
61
|
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
// @vitest-environment happy-dom
|
|
2
|
+
|
|
3
|
+
import React, { act, createRef } from "react";
|
|
4
|
+
import { createRoot } from "react-dom/client";
|
|
5
|
+
import { describe, expect, it, vi } from "vitest";
|
|
6
|
+
import type { DomEditSelection } from "./domEditing";
|
|
7
|
+
import { DomEditSelectionChrome } from "./DomEditSelectionChrome";
|
|
8
|
+
|
|
9
|
+
(globalThis as unknown as { IS_REACT_ACT_ENVIRONMENT: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
|
|
10
|
+
|
|
11
|
+
describe("DomEditSelectionChrome crop composition", () => {
|
|
12
|
+
it("places rotated crop UI in exactly one oriented coordinate plane", () => {
|
|
13
|
+
const element = document.createElement("div");
|
|
14
|
+
element.id = "clip";
|
|
15
|
+
element.style.clipPath = "inset(10px)";
|
|
16
|
+
Object.defineProperties(element, {
|
|
17
|
+
offsetWidth: { value: 200 },
|
|
18
|
+
offsetHeight: { value: 100 },
|
|
19
|
+
});
|
|
20
|
+
document.body.append(element);
|
|
21
|
+
vi.spyOn(window, "getComputedStyle").mockReturnValue({
|
|
22
|
+
clipPath: "inset(10px)",
|
|
23
|
+
transform: "matrix(0.8660254, 0.5, -0.5, 0.8660254, 0, 0)",
|
|
24
|
+
} as CSSStyleDeclaration);
|
|
25
|
+
const selection = {
|
|
26
|
+
element,
|
|
27
|
+
id: "clip",
|
|
28
|
+
selector: "#clip",
|
|
29
|
+
capabilities: {
|
|
30
|
+
canCrop: true,
|
|
31
|
+
canApplyManualOffset: true,
|
|
32
|
+
canApplyManualSize: true,
|
|
33
|
+
canApplyManualRotation: true,
|
|
34
|
+
},
|
|
35
|
+
} as unknown as DomEditSelection;
|
|
36
|
+
const host = document.createElement("div");
|
|
37
|
+
document.body.append(host);
|
|
38
|
+
const root = createRoot(host);
|
|
39
|
+
act(() => {
|
|
40
|
+
root.render(
|
|
41
|
+
<DomEditSelectionChrome
|
|
42
|
+
selection={selection}
|
|
43
|
+
overlayRect={{
|
|
44
|
+
left: 100,
|
|
45
|
+
top: 50,
|
|
46
|
+
width: 220,
|
|
47
|
+
height: 130,
|
|
48
|
+
editScaleX: 1,
|
|
49
|
+
editScaleY: 1,
|
|
50
|
+
angle: 30,
|
|
51
|
+
}}
|
|
52
|
+
allowCanvasMovement={true}
|
|
53
|
+
boxRef={createRef()}
|
|
54
|
+
boxChromeClass=""
|
|
55
|
+
boxClipPath={undefined}
|
|
56
|
+
selectionKey="clip"
|
|
57
|
+
groupSelectionCount={0}
|
|
58
|
+
blockedMoveRef={createRef()}
|
|
59
|
+
gestures={{ startGesture: vi.fn() } as never}
|
|
60
|
+
onStyleCommit={vi.fn()}
|
|
61
|
+
onBoxMouseDown={vi.fn()}
|
|
62
|
+
onBoxClick={vi.fn()}
|
|
63
|
+
/>,
|
|
64
|
+
);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
const cropFrame = host.querySelector<HTMLElement>("[data-dom-edit-crop-frame]")!;
|
|
68
|
+
const rotations: string[] = [];
|
|
69
|
+
for (
|
|
70
|
+
let node: HTMLElement | null = cropFrame;
|
|
71
|
+
node && node !== host;
|
|
72
|
+
node = node.parentElement
|
|
73
|
+
) {
|
|
74
|
+
if (node.style.transform.includes("rotate(")) rotations.push(node.style.transform);
|
|
75
|
+
}
|
|
76
|
+
expect(rotations).toHaveLength(1);
|
|
77
|
+
expect(Number.parseFloat(rotations[0]!.slice("rotate(".length))).toBeCloseTo(30, 5);
|
|
78
|
+
act(() => root.unmount());
|
|
79
|
+
});
|
|
80
|
+
});
|