@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.
Files changed (274) hide show
  1. package/dist/assets/hyperframes-player--Z69cEkE.js +459 -0
  2. package/dist/assets/{index-CPetwHFV.js → index-Bf1x1y8H.js} +1 -1
  3. package/dist/assets/index-DfmYkU44.js +423 -0
  4. package/dist/assets/{index-BXaqaVKt.js → index-KsfE1bUu.js} +1 -1
  5. package/dist/assets/index-_pqzyxB1.css +1 -0
  6. package/dist/{chunk-SOTCF4DF.js → chunk-5QSIMBEJ.js} +2 -1
  7. package/dist/chunk-5QSIMBEJ.js.map +1 -0
  8. package/dist/{domEditingLayers-2ECJK24D.js → domEditingLayers-6LQGKPOI.js} +2 -2
  9. package/dist/index.d.ts +172 -144
  10. package/dist/index.html +2 -2
  11. package/dist/index.js +43613 -40346
  12. package/dist/index.js.map +1 -1
  13. package/package.json +7 -7
  14. package/src/App.tsx +106 -96
  15. package/src/components/EditorShell.tsx +253 -0
  16. package/src/components/StudioGlobalDragOverlay.tsx +1 -3
  17. package/src/components/StudioLeftSidebar.tsx +13 -3
  18. package/src/components/StudioRightPanel.tsx +8 -3
  19. package/src/components/TimelineToolbar.test.tsx +2 -2
  20. package/src/components/TimelineToolbar.tsx +179 -121
  21. package/src/components/editor/CanvasContextMenu.test.tsx +152 -1
  22. package/src/components/editor/CanvasContextMenu.tsx +33 -18
  23. package/src/components/editor/DomEditCropHandles.test.tsx +74 -3
  24. package/src/components/editor/DomEditCropHandles.tsx +64 -29
  25. package/src/components/editor/DomEditOverlay.test.ts +245 -279
  26. package/src/components/editor/DomEditOverlay.tsx +126 -125
  27. package/src/components/editor/DomEditOverlayHover.test.tsx +90 -0
  28. package/src/components/editor/DomEditRotateHandle.tsx +38 -18
  29. package/src/components/editor/DomEditSelectionChrome.test.tsx +80 -0
  30. package/src/components/editor/DomEditSelectionChrome.tsx +257 -0
  31. package/src/components/editor/LayersPanel.test.ts +66 -2
  32. package/src/components/editor/LayersPanel.tsx +53 -2
  33. package/src/components/editor/OffCanvasIndicators.test.tsx +34 -0
  34. package/src/components/editor/OffCanvasIndicators.tsx +49 -10
  35. package/src/components/editor/SnapGuideOverlay.tsx +28 -23
  36. package/src/components/editor/SnapToolbar.test.tsx +0 -1
  37. package/src/components/editor/anchoredResizeCommitFeedsOffset.test.ts +214 -0
  38. package/src/components/editor/anchoredResizeReleaseShift.test.ts +106 -10
  39. package/src/components/editor/canvasContextMenuZOrder.test.ts +186 -2
  40. package/src/components/editor/canvasContextMenuZOrder.ts +144 -22
  41. package/src/components/editor/domEditNudge.test.ts +80 -0
  42. package/src/components/editor/domEditNudge.ts +44 -0
  43. package/src/components/editor/domEditOverlayGeometry.test.ts +167 -1
  44. package/src/components/editor/domEditOverlayGeometry.ts +280 -20
  45. package/src/components/editor/domEditOverlayGestures.ts +44 -41
  46. package/src/components/editor/domEditOverlayStartGesture.ts +62 -2
  47. package/src/components/editor/domEditResizeLocal.test.ts +131 -0
  48. package/src/components/editor/domEditResizeLocal.ts +125 -0
  49. package/src/components/editor/domEditingDom.ts +1 -1
  50. package/src/components/editor/manualEditsDomPatches.test.ts +47 -0
  51. package/src/components/editor/manualOffsetDrag.ts +55 -12
  52. package/src/components/editor/offCanvasIndicatorGeometry.ts +31 -10
  53. package/src/components/editor/offCanvasIndicatorRefresh.test.tsx +25 -1
  54. package/src/components/editor/propertyPanelColorGradingSection.tsx +15 -21
  55. package/src/components/editor/resizeDraft.ts +108 -0
  56. package/src/components/editor/snapEngine.test.ts +12 -78
  57. package/src/components/editor/snapEngine.ts +13 -53
  58. package/src/components/editor/useCanvasContextMenuState.ts +92 -0
  59. package/src/components/editor/useDomEditNudge.test.tsx +228 -0
  60. package/src/components/editor/useDomEditNudge.ts +254 -0
  61. package/src/components/editor/useDomEditOverlayGestures.ts +115 -116
  62. package/src/components/editor/useDomEditOverlayRects.ts +9 -2
  63. package/src/components/editor/useZOrderCrossedFlash.tsx +68 -0
  64. package/src/components/nle/AssetPreviewOverlay.test.tsx +106 -0
  65. package/src/components/nle/AssetPreviewOverlay.tsx +171 -0
  66. package/src/components/nle/NLEContext.test.ts +144 -0
  67. package/src/components/nle/NLEContext.tsx +328 -0
  68. package/src/components/nle/NLEPreview.tsx +1 -1
  69. package/src/components/nle/PreviewOverlays.tsx +260 -0
  70. package/src/components/nle/PreviewPane.tsx +163 -0
  71. package/src/components/nle/TimelinePane.test.ts +60 -0
  72. package/src/components/nle/TimelinePane.tsx +293 -0
  73. package/src/components/nle/TimelineResizeDivider.tsx +8 -2
  74. package/src/components/nle/useCompositionStack.test.tsx +44 -0
  75. package/src/components/nle/useCompositionStack.ts +17 -9
  76. package/src/components/nle/useTimelineEditCallbacks.ts +217 -0
  77. package/src/components/sidebar/AssetCard.test.tsx +106 -0
  78. package/src/components/sidebar/AssetCard.tsx +343 -0
  79. package/src/components/sidebar/AssetContextMenu.tsx +15 -30
  80. package/src/components/sidebar/AssetsTab.test.ts +96 -0
  81. package/src/components/sidebar/AssetsTab.tsx +71 -206
  82. package/src/components/sidebar/AudioRow.tsx +56 -2
  83. package/src/components/sidebar/BlocksTab.tsx +8 -1
  84. package/src/components/sidebar/LeftSidebar.tsx +4 -1
  85. package/src/components/sidebar/assetHelpers.ts +29 -0
  86. package/src/contexts/DomEditContext.tsx +9 -0
  87. package/src/contexts/StudioContext.tsx +9 -8
  88. package/src/contexts/TimelineEditContext.tsx +3 -7
  89. package/src/hooks/deleteSelectedKeyframes.ts +35 -0
  90. package/src/hooks/domEditCommitTypes.ts +23 -1
  91. package/src/hooks/domSelectionTestHarness.ts +14 -0
  92. package/src/hooks/gestureTransaction.test.ts +311 -0
  93. package/src/hooks/gestureTransaction.ts +199 -0
  94. package/src/hooks/gsapDragCommit.test.ts +141 -41
  95. package/src/hooks/gsapDragCommit.ts +64 -74
  96. package/src/hooks/gsapDragStaticSetHelpers.ts +9 -21
  97. package/src/hooks/gsapResizeIntercept.test.ts +69 -0
  98. package/src/hooks/gsapResizeIntercept.ts +38 -10
  99. package/src/hooks/gsapRuntimeBridge.test.ts +49 -13
  100. package/src/hooks/gsapRuntimeBridge.ts +4 -4
  101. package/src/hooks/gsapScriptCommitTypes.ts +16 -5
  102. package/src/hooks/gsapShared.test.ts +19 -1
  103. package/src/hooks/gsapShared.ts +14 -0
  104. package/src/hooks/patchDocumentRootDuration.test.ts +71 -0
  105. package/src/hooks/timelineEditingGsap.ts +14 -0
  106. package/src/hooks/timelineEditingHelpers.test.ts +125 -1
  107. package/src/hooks/timelineEditingHelpers.ts +53 -292
  108. package/src/hooks/timelineMoveAdapter.test.ts +81 -0
  109. package/src/hooks/timelineMoveAdapter.ts +39 -0
  110. package/src/hooks/timelineTimingSync.test.ts +150 -0
  111. package/src/hooks/timelineTimingSync.ts +412 -0
  112. package/src/hooks/useAnimatedPropertyCommit.test.tsx +36 -8
  113. package/src/hooks/useAnimatedPropertyCommit.ts +31 -26
  114. package/src/hooks/useAppHotkeys.ts +16 -25
  115. package/src/hooks/useBlockHandlers.ts +55 -27
  116. package/src/hooks/useCaptionDetection.ts +2 -0
  117. package/src/hooks/useCompositionDimensions.ts +32 -5
  118. package/src/hooks/useContextMenuDismiss.ts +31 -7
  119. package/src/hooks/useDomEditCommits.test.tsx +245 -1
  120. package/src/hooks/useDomEditCommits.ts +162 -3
  121. package/src/hooks/useDomEditPositionPatchCommit.ts +2 -0
  122. package/src/hooks/useDomEditSession.ts +2 -2
  123. package/src/hooks/useDomEditTextCommits.test.tsx +135 -0
  124. package/src/hooks/useDomEditTextCommits.ts +8 -3
  125. package/src/hooks/useDomEditWiring.ts +7 -9
  126. package/src/hooks/useDomGeometryCommits.ts +17 -2
  127. package/src/hooks/useDomSelection.test.ts +2 -36
  128. package/src/hooks/useDomSelection.ts +81 -51
  129. package/src/hooks/useDomSelectionSelectionGuards.test.ts +276 -0
  130. package/src/hooks/useElementLifecycleOps.test.tsx +423 -0
  131. package/src/hooks/useElementLifecycleOps.ts +53 -71
  132. package/src/hooks/useElementPicker.ts +7 -8
  133. package/src/hooks/useEnableKeyframes.test.ts +142 -1
  134. package/src/hooks/useEnableKeyframes.ts +73 -29
  135. package/src/hooks/useGestureCommit.test.tsx +130 -0
  136. package/src/hooks/useGestureCommit.ts +32 -8
  137. package/src/hooks/useGroupCommits.ts +7 -28
  138. package/src/hooks/useGsapAwareEditing.test.tsx +239 -0
  139. package/src/hooks/useGsapAwareEditing.ts +115 -20
  140. package/src/hooks/useGsapKeyframeOps.test.tsx +59 -0
  141. package/src/hooks/useGsapKeyframeOps.ts +38 -10
  142. package/src/hooks/useGsapScriptCommits.test.tsx +40 -1
  143. package/src/hooks/useGsapScriptCommits.ts +134 -50
  144. package/src/hooks/useGsapSelectionHandlers.ts +49 -16
  145. package/src/hooks/useMusicBeatAnalysis.ts +72 -36
  146. package/src/hooks/usePersistentEditHistory.test.ts +33 -0
  147. package/src/hooks/usePersistentEditHistory.ts +92 -58
  148. package/src/hooks/usePreviewPersistence.ts +25 -7
  149. package/src/hooks/useRazorSplit.history.test.tsx +148 -18
  150. package/src/hooks/useRazorSplit.test.ts +269 -0
  151. package/src/hooks/useRazorSplit.testHelpers.ts +64 -0
  152. package/src/hooks/useRazorSplit.ts +107 -36
  153. package/src/hooks/useRenderClipContent.ts +24 -6
  154. package/src/hooks/useStudioContextValue.ts +15 -5
  155. package/src/hooks/useStudioUrlState.ts +2 -4
  156. package/src/hooks/useTimelineAssetDropOps.ts +175 -0
  157. package/src/hooks/useTimelineEditing.test.tsx +398 -241
  158. package/src/hooks/useTimelineEditing.ts +181 -265
  159. package/src/hooks/useTimelineEditingTypes.ts +6 -0
  160. package/src/hooks/useTimelineGroupEditing.ts +167 -132
  161. package/src/index.ts +1 -1
  162. package/src/player/components/ImageThumbnail.test.tsx +173 -0
  163. package/src/player/components/ImageThumbnail.tsx +160 -0
  164. package/src/player/components/PlayerControls.tsx +5 -5
  165. package/src/player/components/PlayheadIndicator.tsx +55 -5
  166. package/src/player/components/Timeline.test.ts +183 -23
  167. package/src/player/components/Timeline.tsx +194 -194
  168. package/src/player/components/TimelineCanvas.tsx +218 -523
  169. package/src/player/components/TimelineClip.tsx +4 -1
  170. package/src/player/components/TimelineClipDiamonds.tsx +9 -40
  171. package/src/player/components/TimelineEmptyState.tsx +3 -1
  172. package/src/player/components/TimelineLanes.tsx +487 -0
  173. package/src/player/components/TimelineOverlays.tsx +122 -0
  174. package/src/player/components/TimelineRuler.tsx +58 -48
  175. package/src/player/components/VideoThumbnail.test.tsx +152 -0
  176. package/src/player/components/VideoThumbnail.tsx +28 -7
  177. package/src/player/components/thumbnailUtils.test.ts +127 -0
  178. package/src/player/components/thumbnailUtils.ts +54 -0
  179. package/src/player/components/timelineCallbacks.ts +18 -18
  180. package/src/player/components/timelineClipChildren.tsx +39 -0
  181. package/src/player/components/timelineClipDragCommit.test.ts +1026 -0
  182. package/src/player/components/timelineClipDragCommit.ts +498 -0
  183. package/src/player/components/timelineClipDragPreview.test.ts +144 -0
  184. package/src/player/components/timelineClipDragPreview.ts +323 -38
  185. package/src/player/components/timelineClipDragTypes.ts +65 -0
  186. package/src/player/components/timelineCollision.test.ts +42 -2
  187. package/src/player/components/timelineDragDrop.ts +69 -60
  188. package/src/player/components/timelineEditCapabilities.ts +52 -0
  189. package/src/player/components/timelineEditing.test.ts +1 -1
  190. package/src/player/components/timelineEditing.ts +76 -59
  191. package/src/player/components/timelineGroupEditing.test.ts +126 -0
  192. package/src/player/components/timelineGroupEditing.ts +133 -0
  193. package/src/player/components/timelineGroupResizeCommit.ts +54 -0
  194. package/src/player/components/timelineLayout.test.ts +95 -23
  195. package/src/player/components/timelineLayout.ts +207 -75
  196. package/src/player/components/timelineMarquee.test.ts +197 -0
  197. package/src/player/components/timelineMarquee.ts +112 -0
  198. package/src/player/components/timelineOptimisticRevision.ts +42 -0
  199. package/src/player/components/timelineRevealScroll.test.ts +133 -0
  200. package/src/player/components/timelineRevealScroll.ts +91 -0
  201. package/src/player/components/timelineStackingSync.test.ts +60 -0
  202. package/src/player/components/timelineStackingSync.ts +27 -2
  203. package/src/player/components/timelineTheme.ts +6 -1
  204. package/src/player/components/timelineTrackPersistPipeline.test.ts +168 -0
  205. package/src/player/components/timelineZones.test.ts +122 -310
  206. package/src/player/components/timelineZones.ts +82 -135
  207. package/src/player/components/useResolvedTimelineEditCallbacks.ts +25 -3
  208. package/src/player/components/useTimelineClipDrag.resize.test.tsx +241 -0
  209. package/src/player/components/useTimelineClipDrag.ts +380 -386
  210. package/src/player/components/useTimelineEditPinning.ts +121 -0
  211. package/src/player/components/useTimelineGeometry.ts +129 -0
  212. package/src/player/components/useTimelinePlayhead.ts +4 -16
  213. package/src/player/components/useTimelineRangeSelection.ts +341 -50
  214. package/src/player/components/useTimelineRevealClip.ts +56 -0
  215. package/src/player/components/useTimelineStackingSync.test.tsx +77 -0
  216. package/src/player/components/useTimelineStackingSync.ts +94 -0
  217. package/src/player/hooks/useExpandedTimelineElements.ts +7 -3
  218. package/src/player/hooks/usePlaybackKeyboard.ts +5 -0
  219. package/src/player/hooks/useTimelinePlayer.seek.test.ts +55 -0
  220. package/src/player/hooks/useTimelinePlayer.test.ts +36 -3
  221. package/src/player/hooks/useTimelinePlayer.ts +52 -53
  222. package/src/player/hooks/useTimelinePlayerLoop.ts +85 -0
  223. package/src/player/hooks/useTimelineSyncCallbacks.ts +9 -1
  224. package/src/player/index.ts +5 -2
  225. package/src/player/lib/runtimeProtocol.test.ts +48 -0
  226. package/src/player/lib/runtimeProtocol.ts +65 -0
  227. package/src/player/lib/time.test.ts +19 -19
  228. package/src/player/lib/time.ts +6 -2
  229. package/src/player/lib/timelineDOM.test.ts +25 -98
  230. package/src/player/lib/timelineDOM.ts +21 -81
  231. package/src/player/lib/timelineElementHelpers.ts +73 -5
  232. package/src/player/lib/timelineIframeHelpers.ts +4 -4
  233. package/src/player/store/playerStore.test.ts +38 -13
  234. package/src/player/store/playerStore.ts +99 -3
  235. package/src/styles/studio.css +27 -0
  236. package/src/utils/assetPreviewDismiss.test.ts +27 -0
  237. package/src/utils/assetPreviewDismiss.ts +29 -0
  238. package/src/utils/assetPreviewStore.ts +33 -0
  239. package/src/utils/blockInstaller.ts +10 -2
  240. package/src/utils/editHistory.test.ts +35 -0
  241. package/src/utils/gsapSoftReload.test.ts +119 -1
  242. package/src/utils/gsapSoftReload.ts +153 -0
  243. package/src/utils/mediaTypes.ts +3 -2
  244. package/src/utils/resizeDebug.ts +55 -0
  245. package/src/utils/rootDuration.test.ts +90 -1
  246. package/src/utils/rootDuration.ts +76 -13
  247. package/src/utils/sdkCutover.ts +4 -0
  248. package/src/utils/studioFileHistory.ts +37 -7
  249. package/src/utils/studioHelpers.ts +62 -0
  250. package/src/utils/studioPreviewHelpers.test.ts +65 -8
  251. package/src/utils/studioPreviewHelpers.ts +10 -0
  252. package/src/utils/studioTelemetry.ts +4 -1
  253. package/src/utils/studioUrlState.test.ts +0 -1
  254. package/src/utils/timelineAssetDrop.test.ts +144 -52
  255. package/src/utils/timelineAssetDrop.ts +67 -29
  256. package/src/utils/timelineDiscovery.ts +0 -17
  257. package/dist/assets/hyperframes-player-BBrKzTGd.js +0 -459
  258. package/dist/assets/index-BRwkMj0w.js +0 -423
  259. package/dist/assets/index-Dq7FEg0K.css +0 -1
  260. package/dist/chunk-SOTCF4DF.js.map +0 -1
  261. package/src/components/StudioPreviewArea.tsx +0 -500
  262. package/src/components/nle/NLELayout.test.ts +0 -12
  263. package/src/components/nle/NLELayout.tsx +0 -591
  264. package/src/player/components/TimelineLayerGutter.tsx +0 -61
  265. package/src/player/components/TimelineSelectionOverlays.tsx +0 -55
  266. package/src/player/components/timelineMarqueeSelection.test.ts +0 -87
  267. package/src/player/components/timelineSnapTargets.test.ts +0 -144
  268. package/src/player/components/timelineSnapTargets.ts +0 -164
  269. package/src/player/components/useTimelineClipDrag.test.tsx +0 -506
  270. package/src/player/components/useTimelineClipGroupDrag.ts +0 -417
  271. package/src/player/components/useTimelineMarqueeSelection.test.tsx +0 -231
  272. package/src/player/components/useTimelineMarqueeSelection.ts +0 -276
  273. package/src/utils/timelineDiscovery.test.ts +0 -90
  274. /package/dist/{domEditingLayers-2ECJK24D.js.map → domEditingLayers-6LQGKPOI.js.map} +0 -0
@@ -1,10 +1,11 @@
1
1
  import { memo, useEffect, useMemo, useRef, useState, type RefObject } from "react";
2
- import { getPreviewTargetFromPointer } from "../../utils/studioPreviewHelpers";
3
2
  import { type DomEditSelection } from "./domEditing";
4
3
  import type { PreviewMouseDownOptions } from "../../hooks/usePreviewInteraction";
5
4
  import { useMarqueeGestures } from "./marqueeCommit";
6
5
  import { MarqueeOverlay } from "./MarqueeOverlay";
7
6
  import { resolveDomEditGroupOverlayRect } from "./domEditOverlayGeometry";
7
+ import { useZOrderCrossedFlash, ZOrderCrossedFlash } from "./useZOrderCrossedFlash";
8
+ import { useCanvasContextMenuState } from "./useCanvasContextMenuState";
8
9
  import {
9
10
  type BlockedMoveState,
10
11
  type DomEditGroupPathOffsetCommit,
@@ -16,17 +17,20 @@ import {
16
17
  import { useDomEditOverlayRects } from "./useDomEditOverlayRects";
17
18
  import { OffCanvasIndicators, type OffCanvasRect } from "./OffCanvasIndicators";
18
19
  import { createDomEditOverlayGestureHandlers } from "./useDomEditOverlayGestures";
20
+ import { useDomEditNudge } from "./useDomEditNudge";
19
21
  import { SnapGuideOverlay, type SnapGuidesState } from "./SnapGuideOverlay";
20
22
  import { GridOverlay } from "./GridOverlay";
21
23
  import type { GestureRecordingState } from "./GestureRecordControl";
22
- import { DomEditCropHandles } from "./DomEditCropHandles";
23
- import { DomEditRotateHandle } from "./DomEditRotateHandle";
24
+ import { DomEditGroupChrome, DomEditSelectionChrome } from "./DomEditSelectionChrome";
24
25
  import { hugRectForElement } from "./domEditOverlayCrop";
25
26
  import { useCropOverlay } from "../../hooks/useCropOverlay";
26
27
  import { readDomEditSelectionShapeStyles, resolveBoxChromeClass } from "./domEditOverlayShape";
27
28
  import { useDomEditCompositionRect } from "./useDomEditCompositionRect";
28
29
  import { useMountEffect } from "../../hooks/useMountEffect";
29
30
  import { startOffCanvasIndicatorRefresh } from "./offCanvasIndicatorRefresh";
31
+ import { CanvasContextMenu } from "./CanvasContextMenu";
32
+ import type { ZOrderAction, ZOrderPatch } from "./canvasContextMenuZOrder";
33
+ import { getPreviewTargetFromPointer } from "../../utils/studioPreviewHelpers";
30
34
 
31
35
  // Re-exports for external consumers — preserving existing import paths.
32
36
  export {
@@ -37,7 +41,6 @@ export {
37
41
  export {
38
42
  focusDomEditOverlayElement,
39
43
  hasDomEditRotationChanged,
40
- resolveDomEditResizeGesture,
41
44
  resolveDomEditRotationGesture,
42
45
  } from "./domEditOverlayGestures";
43
46
  export type { DomEditGroupPathOffsetCommit } from "./domEditOverlayGestures";
@@ -73,6 +76,8 @@ interface DomEditOverlayProps {
73
76
  onBoxSizeCommit: (
74
77
  selection: DomEditSelection,
75
78
  next: { width: number; height: number },
79
+ offset?: { x: number; y: number },
80
+ restore?: () => void,
76
81
  ) => Promise<void> | void;
77
82
  onRotationCommit: (selection: DomEditSelection, next: { angle: number }) => Promise<void> | void;
78
83
  onStyleCommit?: (property: string, value: string) => Promise<void> | void;
@@ -81,6 +86,24 @@ interface DomEditOverlayProps {
81
86
  recordingState?: GestureRecordingState;
82
87
  onToggleRecording?: () => void;
83
88
  onMarqueeSelect?: (selections: DomEditSelection[], additive: boolean) => void;
89
+ /**
90
+ * Delete the selected canvas element.
91
+ * Wire to handleDomEditElementDelete from useDomEditActionsContext —
92
+ * same handler the Delete/Backspace hotkey uses.
93
+ */
94
+ onDeleteSelection?: (selection: DomEditSelection) => void;
95
+ /**
96
+ * Called with the resolved z-order patch list and the menu action that
97
+ * produced it (feeds the undo coalesce key). The patch list is tie-aware and
98
+ * may include sibling elements (see canvasContextMenuZOrder); the live DOM is
99
+ * NOT yet mutated. Wire to handleDomZIndexReorderCommit from
100
+ * useDomEditActionsContext. See CanvasContextMenu.tsx module comment.
101
+ */
102
+ onApplyZIndex?: (
103
+ selection: DomEditSelection,
104
+ patches: ZOrderPatch[],
105
+ action: ZOrderAction,
106
+ ) => void;
84
107
  }
85
108
 
86
109
  // fallow-ignore-next-line complexity
@@ -105,6 +128,8 @@ export const DomEditOverlay = memo(function DomEditOverlay({
105
128
  onRotationCommit,
106
129
  onStyleCommit,
107
130
  onMarqueeSelect,
131
+ onDeleteSelection,
132
+ onApplyZIndex,
108
133
  }: DomEditOverlayProps) {
109
134
  const overlayRef = useRef<HTMLDivElement | null>(null);
110
135
  const boxRef = useRef<HTMLDivElement | null>(null);
@@ -123,6 +148,11 @@ export const DomEditOverlay = memo(function DomEditOverlay({
123
148
 
124
149
  const selectionRef = useRef(selection);
125
150
  selectionRef.current = selection;
151
+
152
+ // Brief highlight on the sibling a forward/backward z step crossed — drawn
153
+ // in this studio overlay, never in the iframe DOM (see useZOrderCrossedFlash).
154
+ const { zOrderFlashRect, handleZOrderCrossed } = useZOrderCrossedFlash({ overlayRef, iframeRef });
155
+
126
156
  const activeCompositionPathRef = useRef(activeCompositionPath);
127
157
  activeCompositionPathRef.current = activeCompositionPath;
128
158
  const groupSelectionsRef = useRef(groupSelections);
@@ -241,6 +271,23 @@ export const DomEditOverlay = memo(function DomEditOverlay({
241
271
  snapGuidesRef,
242
272
  });
243
273
 
274
+ // Arrow-key nudge (1px, Shift = 10px) — commits through the same
275
+ // path-offset callbacks as a drag, one undo entry per key burst.
276
+ const { flushNudge } = useDomEditNudge({
277
+ selection,
278
+ groupSelections,
279
+ allowCanvasMovement,
280
+ selectionRef,
281
+ overlayRectRef,
282
+ groupOverlayItemsRef,
283
+ gestureRef,
284
+ groupGestureRef,
285
+ blockedMoveRef,
286
+ onManualDragStartRef,
287
+ onPathOffsetCommitRef,
288
+ onGroupPathOffsetCommitRef,
289
+ });
290
+
244
291
  const marquee = useMarqueeGestures({
245
292
  iframeRef,
246
293
  overlayRef,
@@ -370,6 +417,16 @@ export const DomEditOverlay = memo(function DomEditOverlay({
370
417
  e.stopPropagation();
371
418
  };
372
419
 
420
+ // Right-click state + handler: select the element under the pointer (if
421
+ // needed), then open the menu; closes when the selection moves off-target.
422
+ const { contextMenu, closeContextMenu, handleContextMenu } = useCanvasContextMenuState({
423
+ selection,
424
+ selectionRef,
425
+ hoverSelectionRef,
426
+ onCanvasPointerMoveRef,
427
+ onSelectionChangeRef,
428
+ });
429
+
373
430
  return (
374
431
  <div
375
432
  ref={overlayRef}
@@ -378,142 +435,59 @@ export const DomEditOverlay = memo(function DomEditOverlay({
378
435
  aria-label="Composition canvas"
379
436
  // Cursor follows marquee rect *state* (re-renders), not the mutable ref.
380
437
  style={marquee.marqueeRect ? { cursor: "crosshair" } : undefined}
381
- onPointerDownCapture={(event) =>
382
- focusDomEditOverlayElement(event.currentTarget as FocusableDomEditOverlay)
383
- }
438
+ onPointerDownCapture={(event) => {
439
+ // A pointer gesture supersedes a pending nudge burst — commit it first
440
+ // so the gesture's member snapshot starts from the nudged position.
441
+ flushNudge();
442
+ focusDomEditOverlayElement(event.currentTarget as FocusableDomEditOverlay);
443
+ }}
384
444
  onPointerDown={handleOverlayPointerDown}
385
445
  onMouseDown={handleOverlayMouseDown}
386
446
  onPointerMove={marquee.onPointerMove}
387
447
  onPointerLeave={() => onCanvasPointerLeaveRef.current()}
388
448
  onPointerUp={marquee.onPointerUp}
389
449
  onPointerCancel={marquee.onPointerCancel}
450
+ onContextMenu={handleContextMenu}
390
451
  >
391
452
  {hoverSelection && hoverRect && compRect.width > 0 && (
392
453
  <div
393
454
  aria-hidden="true"
394
455
  data-dom-edit-hover-box="true"
395
456
  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={hugRectForElement(hoverRect, hoverSelection.element)}
457
+ style={{
458
+ ...hugRectForElement(hoverRect, hoverSelection.element),
459
+ transform: hoverRect.angle ? `rotate(${hoverRect.angle}deg)` : undefined,
460
+ }}
397
461
  />
398
462
  )}
399
463
  {hasGroupSelection && groupOverlayItems.length > 1 && groupBounds && compRect.width > 0 && (
400
- <>
401
- {groupOverlayItems.map((item) => (
402
- <div
403
- key={item.key}
404
- aria-hidden="true"
405
- className="pointer-events-none absolute rounded-xl border border-studio-accent/70"
406
- style={{
407
- left: item.rect.left,
408
- top: item.rect.top,
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
- </>
464
+ <DomEditGroupChrome
465
+ groupOverlayItems={groupOverlayItems}
466
+ groupBounds={groupBounds}
467
+ allowCanvasMovement={allowCanvasMovement}
468
+ groupCanMove={groupCanMove}
469
+ gestures={gestures}
470
+ onBoxMouseDown={suppressBoxMouseDown}
471
+ onBoxClick={handleBoxClick}
472
+ />
432
473
  )}
433
474
  {!hasGroupSelection && selection && overlayRect && compRect.width > 0 && (
434
- <>
435
- {allowCanvasMovement && selection.capabilities.canApplyManualRotation && (
436
- <DomEditRotateHandle
437
- overlayRect={overlayRect}
438
- cropOutlineInsetPx={cropOutlineInsetPx}
439
- onStartRotate={(e) => {
440
- e.stopPropagation();
441
- gestures.startGesture("rotate", e);
442
- }}
443
- />
444
- )}
445
- <div
446
- key={selectionKey}
447
- ref={boxRef}
448
- data-dom-edit-selection-box="true"
449
- className={`pointer-events-auto absolute rounded-md ${boxChromeClass}`}
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
- </>
475
+ <DomEditSelectionChrome
476
+ selection={selection}
477
+ overlayRect={overlayRect}
478
+ allowCanvasMovement={allowCanvasMovement}
479
+ cropOutlineInsetPx={cropOutlineInsetPx ?? undefined}
480
+ boxRef={boxRef}
481
+ boxChromeClass={boxChromeClass}
482
+ boxClipPath={boxClipPath}
483
+ selectionKey={selectionKey}
484
+ groupSelectionCount={groupSelections.length}
485
+ blockedMoveRef={blockedMoveRef}
486
+ gestures={gestures}
487
+ onStyleCommit={onStyleCommitRef.current}
488
+ onBoxMouseDown={suppressBoxMouseDown}
489
+ onBoxClick={handleBoxClick}
490
+ />
517
491
  )}
518
492
  {childRects.length > 0 &&
519
493
  compRect.width > 0 &&
@@ -539,6 +513,31 @@ export const DomEditOverlay = memo(function DomEditOverlay({
539
513
  onSelectionChangeRef={onSelectionChangeRef}
540
514
  />
541
515
  <MarqueeOverlay candidateRects={marquee.candidateRects} marqueeRect={marquee.marqueeRect} />
516
+ {contextMenu && (
517
+ <CanvasContextMenu
518
+ x={contextMenu.x}
519
+ y={contextMenu.y}
520
+ selection={contextMenu.sel}
521
+ onClose={closeContextMenu}
522
+ onDelete={
523
+ onDeleteSelection
524
+ ? (sel) => {
525
+ closeContextMenu();
526
+ onDeleteSelection(sel);
527
+ }
528
+ : undefined
529
+ }
530
+ onApplyZIndex={
531
+ onApplyZIndex
532
+ ? (patches, action) => {
533
+ onApplyZIndex(contextMenu.sel, patches, action);
534
+ }
535
+ : undefined
536
+ }
537
+ onZOrderCrossed={handleZOrderCrossed}
538
+ />
539
+ )}
540
+ <ZOrderCrossedFlash rect={zOrderFlashRect} />
542
541
  <GridOverlay
543
542
  visible={gridVisible}
544
543
  spacing={gridSpacing}
@@ -551,8 +550,10 @@ export const DomEditOverlay = memo(function DomEditOverlay({
551
550
  />
552
551
  <SnapGuideOverlay
553
552
  snapGuidesRef={snapGuidesRef}
554
- overlayWidth={compRect.width}
555
- overlayHeight={compRect.height}
553
+ compositionLeft={compRect.left}
554
+ compositionTop={compRect.top}
555
+ compositionWidth={compRect.width}
556
+ compositionHeight={compRect.height}
556
557
  />
557
558
  </div>
558
559
  );
@@ -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 grab-handle above the selection. Anchors to the crop outline when
5
- * the element is cropped so it stays next to what's visible on screen. */
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 visibleTop = overlayRect.top + inset.top;
22
+ const visibleBottom = overlayRect.top + overlayRect.height - inset.bottom;
19
23
  return (
20
- <div
21
- className="pointer-events-none absolute"
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: visibleTop - 34,
25
- width: 28,
26
- height: 34,
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
- <div className="absolute left-1/2 top-3 bottom-0 w-px -translate-x-1/2 bg-studio-accent/60" />
31
- <button
32
- type="button"
33
- className="pointer-events-auto absolute left-1/2 top-0 h-3 w-3 -translate-x-1/2 rounded-full border border-studio-accent bg-studio-accent p-0 shadow-[0_0_0_2px_rgba(60,230,172,0.18)]"
34
- style={{ cursor: "grab", touchAction: "none" }}
35
- title="Rotate"
36
- aria-label="Rotate selection"
37
- onPointerDown={onStartRotate}
38
- />
39
- </div>
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
+ });