@bycrux/editor 0.12.0 → 1.0.1

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 (266) hide show
  1. package/package.json +3 -1
  2. package/src/ControlsInfoModal.tsx +251 -61
  3. package/src/__tests__/ControlsInfoModal.test.tsx +43 -0
  4. package/src/__tests__/adapter.test.ts +59 -1
  5. package/src/__tests__/schema-assignability.test.ts +93 -0
  6. package/src/__tests__/schema.test.ts +15 -0
  7. package/src/__tests__/video-adapter-contract.test.ts +128 -5
  8. package/src/carousel/AddElementMenu.tsx +10 -4
  9. package/src/carousel/CarouselEditor.tsx +46 -8
  10. package/src/carousel/CarouselRenderModal.tsx +15 -10
  11. package/src/carousel/OverlayPicker.tsx +10 -3
  12. package/src/carousel/SlidePropertyPanel.tsx +43 -21
  13. package/src/components/FilmstripScrubber.tsx +277 -0
  14. package/src/components/__tests__/FilmstripScrubber.test.tsx +216 -0
  15. package/src/engine/__tests__/audio-clock.test.ts +655 -0
  16. package/src/engine/__tests__/audio-worklet-source.test.ts +316 -0
  17. package/src/engine/__tests__/batch-planner.test.ts +298 -0
  18. package/src/engine/__tests__/decode-worker-source.test.ts +249 -0
  19. package/src/engine/__tests__/demux-ranged.test.ts +495 -0
  20. package/src/engine/__tests__/demux-truncated.test.ts +136 -0
  21. package/src/engine/__tests__/demux.test.ts +305 -0
  22. package/src/engine/__tests__/eligibility.test.ts +146 -0
  23. package/src/engine/__tests__/engine-recovery.test.ts +263 -0
  24. package/src/engine/__tests__/engine.test.ts +326 -0
  25. package/src/engine/__tests__/frame-server-ranged.test.ts +261 -0
  26. package/src/engine/__tests__/frame-server.test.ts +326 -0
  27. package/src/engine/__tests__/media-loader-ranged.test.ts +289 -0
  28. package/src/engine/__tests__/media-loader.test.ts +100 -0
  29. package/src/engine/__tests__/scheduler-crop.test.ts +353 -0
  30. package/src/engine/__tests__/scheduler.test.ts +1310 -0
  31. package/src/engine/__tests__/scrub-resolve.test.ts +96 -0
  32. package/src/engine/__tests__/scrub-source.test.ts +195 -0
  33. package/src/engine/__tests__/source-host.test.ts +455 -0
  34. package/src/engine/__tests__/time-stretch.test.ts +182 -0
  35. package/src/engine/audio-clock.ts +1179 -0
  36. package/src/engine/audio-worklet-source.ts +160 -0
  37. package/src/engine/batch-planner.ts +308 -0
  38. package/src/engine/debug-hud.tsx +54 -0
  39. package/src/engine/decode-worker-source.ts +142 -0
  40. package/src/engine/demux.ts +900 -0
  41. package/src/engine/eligibility.ts +140 -0
  42. package/src/engine/frame-server.ts +644 -0
  43. package/src/engine/index.ts +950 -0
  44. package/src/engine/media-loader.ts +380 -0
  45. package/src/engine/mp4box.d.ts +214 -0
  46. package/src/engine/scheduler.ts +1324 -0
  47. package/src/engine/scrub-resolve.ts +66 -0
  48. package/src/engine/scrub-source.ts +496 -0
  49. package/src/engine/time-stretch.ts +224 -0
  50. package/src/index.ts +75 -1
  51. package/src/preview/OverlayPreview.tsx +2 -24
  52. package/src/schema.ts +134 -2
  53. package/src/state/__tests__/use-project-sync.test.tsx +56 -0
  54. package/src/state/use-project-sync.ts +17 -0
  55. package/src/test-setup.ts +32 -0
  56. package/src/text/FontPicker.tsx +85 -51
  57. package/src/text/TextFormattingToolbar.tsx +6 -1
  58. package/src/text/__tests__/FontPicker.options.test.ts +43 -0
  59. package/src/text/__tests__/TextFormattingToolbar.test.tsx +4 -4
  60. package/src/theme.ts +108 -3
  61. package/src/types.ts +586 -22
  62. package/src/ui/Loader.tsx +64 -0
  63. package/src/ui/NumberField.tsx +254 -0
  64. package/src/ui/Slider.tsx +128 -0
  65. package/src/ui/Tooltip.tsx +118 -0
  66. package/src/ui/__tests__/NumberField.test.tsx +298 -0
  67. package/src/ui/__tests__/Slider.test.tsx +150 -0
  68. package/src/ui/__tests__/Tooltip.test.tsx +105 -0
  69. package/src/ui/__tests__/usePersistentState.test.tsx +112 -0
  70. package/src/ui/badge.tsx +12 -4
  71. package/src/ui/index.ts +6 -0
  72. package/src/ui/input.tsx +1 -1
  73. package/src/ui/select.tsx +1 -1
  74. package/src/ui/switch.tsx +12 -2
  75. package/src/ui/textarea.tsx +1 -1
  76. package/src/ui/usePersistentState.ts +63 -0
  77. package/src/video/AudioPolishModal.tsx +983 -0
  78. package/src/video/CaptionListPanel.test.tsx +944 -0
  79. package/src/video/CaptionListPanel.tsx +1106 -0
  80. package/src/video/CaptionRegenModal.tsx +37 -9
  81. package/src/video/CaptionSpecimen.tsx +160 -0
  82. package/src/video/CaptionStyleGallery.tsx +466 -0
  83. package/src/video/CommandPalette.tsx +165 -0
  84. package/src/video/ImageToneMenu.tsx +6 -2
  85. package/src/video/OverlayInspector.tsx +977 -0
  86. package/src/video/RenderModal.tsx +1035 -62
  87. package/src/video/VersionCompare.tsx +258 -0
  88. package/src/video/VersionPanel.tsx +117 -42
  89. package/src/video/VideoEditor.tsx +2202 -251
  90. package/src/video/__tests__/AudioPolishModal.test.tsx +825 -0
  91. package/src/video/__tests__/CaptionListPanel.font.test.tsx +397 -0
  92. package/src/video/__tests__/CaptionListPanel.generate.test.tsx +153 -0
  93. package/src/video/__tests__/CaptionRegenModal.test.tsx +29 -0
  94. package/src/video/__tests__/CaptionSpecimen.test.tsx +213 -0
  95. package/src/video/__tests__/CaptionStyleGallery.test.tsx +362 -0
  96. package/src/video/__tests__/CommandPalette.test.tsx +119 -0
  97. package/src/video/__tests__/OverlayInspector.test.tsx +1367 -0
  98. package/src/video/__tests__/RenderModal.exportControls.test.tsx +319 -0
  99. package/src/video/__tests__/RenderModal.options.test.tsx +562 -0
  100. package/src/video/__tests__/RenderModal.progress.test.ts +65 -0
  101. package/src/video/__tests__/VersionCompare.test.tsx +182 -0
  102. package/src/video/__tests__/VersionPanel.test.tsx +279 -0
  103. package/src/video/__tests__/VideoEditor.audioPolish.test.tsx +241 -0
  104. package/src/video/__tests__/VideoEditor.captionDelete.test.tsx +147 -0
  105. package/src/video/__tests__/VideoEditor.captionGesture.test.tsx +170 -0
  106. package/src/video/__tests__/VideoEditor.captionSeam.test.tsx +134 -0
  107. package/src/video/__tests__/VideoEditor.clipKeyframes.test.tsx +59 -0
  108. package/src/video/__tests__/VideoEditor.context.test.tsx +44 -0
  109. package/src/video/__tests__/VideoEditor.editFocus.test.tsx +55 -0
  110. package/src/video/__tests__/VideoEditor.keymap.test.tsx +725 -0
  111. package/src/video/__tests__/VideoEditor.layout.test.tsx +338 -0
  112. package/src/video/__tests__/VideoEditor.propertiesPanel.test.tsx +765 -0
  113. package/src/video/__tests__/VideoEditor.rippleDeleteCaptions.test.tsx +159 -0
  114. package/src/video/__tests__/VideoEditor.sourcePreview.test.tsx +122 -0
  115. package/src/video/__tests__/VideoEditor.test.tsx +403 -50
  116. package/src/video/__tests__/audioMagnet.test.ts +158 -0
  117. package/src/video/__tests__/audioPolish.test.ts +1202 -0
  118. package/src/video/__tests__/captionActiveWord.test.ts +107 -0
  119. package/src/video/__tests__/captionLanes.test.ts +262 -0
  120. package/src/video/__tests__/captionPositioning.test.tsx +8 -3
  121. package/src/video/__tests__/captionWordFloor.test.ts +228 -0
  122. package/src/video/__tests__/clipboard-ops.test.ts +430 -0
  123. package/src/video/__tests__/cuts.insert.test.ts +244 -0
  124. package/src/video/__tests__/cuts.test.ts +1213 -34
  125. package/src/video/__tests__/export-limits.test.ts +195 -0
  126. package/src/video/__tests__/hover-scrub.test.ts +163 -0
  127. package/src/video/__tests__/keyframeOps.canKeyframeProp.test.ts +61 -0
  128. package/src/video/__tests__/keyframeOps.test.ts +643 -0
  129. package/src/video/__tests__/keymap.test.tsx +171 -0
  130. package/src/video/__tests__/render-progress.test.tsx +20 -4
  131. package/src/video/__tests__/shuttle.test.ts +210 -0
  132. package/src/video/__tests__/source-preview.test.ts +60 -0
  133. package/src/video/__tests__/timecode.test.ts +77 -0
  134. package/src/video/__tests__/use-report-context.test.tsx +101 -0
  135. package/src/video/audioMagnet.ts +72 -0
  136. package/src/video/audioPolish.ts +774 -0
  137. package/src/video/captionActiveWord.ts +74 -0
  138. package/src/video/captionLanes.ts +202 -0
  139. package/src/video/captionRepair.ts +9 -5
  140. package/src/video/captionStyleDefaults.ts +100 -0
  141. package/src/video/captionWordFloor.ts +83 -0
  142. package/src/video/clipboard-ops.ts +377 -0
  143. package/src/video/cuts.ts +713 -37
  144. package/src/video/export-limits.ts +102 -0
  145. package/src/video/hover-scrub.ts +102 -0
  146. package/src/video/keyframeOps.ts +384 -0
  147. package/src/video/keymap.ts +146 -0
  148. package/src/video/panels/ClipPropertiesPanel.tsx +706 -0
  149. package/src/video/panels/LeftPanelTabs.tsx +187 -0
  150. package/src/video/panels/OverlayContentPanel.tsx +351 -0
  151. package/src/video/panels/TabNav.tsx +60 -0
  152. package/src/video/panels/__tests__/ClipPropertiesPanel.test.tsx +645 -0
  153. package/src/video/panels/__tests__/LeftPanelTabs.test.tsx +189 -0
  154. package/src/video/panels/__tests__/OverlayContentPanel.test.tsx +222 -0
  155. package/src/video/panels/__tests__/TabNav.test.tsx +71 -0
  156. package/src/video/preview/CaptionPreview.tsx +57 -69
  157. package/src/video/preview/EngineSurface.tsx +103 -0
  158. package/src/video/preview/OverlayItemsLayer.tsx +315 -103
  159. package/src/video/preview/PreviewPlayer.tsx +337 -56
  160. package/src/video/preview/SocialPreviewMenu.tsx +214 -0
  161. package/src/video/preview/SocialSafeZoneOverlay.tsx +478 -0
  162. package/src/video/preview/__tests__/CaptionPreview.fonts.test.tsx +97 -0
  163. package/src/video/preview/__tests__/EngineSurface.test.tsx +114 -0
  164. package/src/video/preview/__tests__/OverlayItemsLayer.edit.test.tsx +4 -2
  165. package/src/video/preview/__tests__/OverlayItemsLayer.keyframes.test.tsx +363 -0
  166. package/src/video/preview/__tests__/OverlayItemsLayer.selection.test.tsx +329 -0
  167. package/src/video/preview/__tests__/PreviewPlayer.engine.test.tsx +139 -0
  168. package/src/video/preview/__tests__/SocialPreviewMenu.test.tsx +121 -0
  169. package/src/video/preview/__tests__/SocialSafeZoneOverlay.test.tsx +165 -0
  170. package/src/video/preview/__tests__/captionDragState.test.ts +120 -1
  171. package/src/video/preview/__tests__/latencyCompensation.test.tsx +451 -0
  172. package/src/video/preview/__tests__/proxySupport.test.ts +75 -0
  173. package/src/video/preview/__tests__/transformStyle.test.ts +29 -1
  174. package/src/video/preview/__tests__/useDragOverlay.perAxis.test.ts +197 -0
  175. package/src/video/preview/__tests__/useEnginePlayback.test.tsx +530 -0
  176. package/src/video/preview/__tests__/useVideoPlayback.corpus.test.ts +313 -0
  177. package/src/video/preview/__tests__/useVideoPlayback.test.ts +38 -5
  178. package/src/video/preview/__tests__/useVideoPlayback.trackAudio.test.ts +278 -0
  179. package/src/video/preview/audio-context.ts +111 -0
  180. package/src/video/preview/captionDragState.ts +91 -1
  181. package/src/video/preview/proxySupport.ts +86 -0
  182. package/src/video/preview/transformStyle.ts +22 -12
  183. package/src/video/preview/useDragOverlay.ts +92 -18
  184. package/src/video/preview/useEnginePlayback.ts +625 -0
  185. package/src/video/preview/useVideoPlayback.ts +211 -167
  186. package/src/video/sdrCurves.ts +56 -0
  187. package/src/video/shuttle.ts +159 -0
  188. package/src/video/source-preview.ts +66 -0
  189. package/src/video/timecode.ts +59 -0
  190. package/src/video/timeline/EditableSegment.tsx +1 -1
  191. package/src/video/timeline/Scrubber.tsx +46 -174
  192. package/src/video/timeline/SpeedControl.tsx +95 -0
  193. package/src/video/timeline/Timeline.tsx +1061 -328
  194. package/src/video/timeline/TimelineContext.ts +17 -10
  195. package/src/video/timeline/TrackGutter.tsx +560 -0
  196. package/src/video/timeline/TrackSettingsPopover.tsx +228 -0
  197. package/src/video/timeline/VolumeControl.tsx +113 -0
  198. package/src/video/timeline/__tests__/Timeline.backgroundClick.test.tsx +110 -0
  199. package/src/video/timeline/__tests__/Timeline.crossfade.test.tsx +104 -0
  200. package/src/video/timeline/__tests__/Timeline.fadeCurveMenu.test.tsx +174 -0
  201. package/src/video/timeline/__tests__/Timeline.keyframeDelete.test.tsx +273 -0
  202. package/src/video/timeline/__tests__/Timeline.keyframeFollow.test.tsx +215 -0
  203. package/src/video/timeline/__tests__/Timeline.keyframeMenu.test.tsx +253 -0
  204. package/src/video/timeline/__tests__/Timeline.keymap.test.tsx +372 -0
  205. package/src/video/timeline/__tests__/Timeline.subcutRegen.test.tsx +351 -0
  206. package/src/video/timeline/__tests__/TrackGutter.test.tsx +372 -0
  207. package/src/video/timeline/__tests__/_canvasSelect.test.tsx +273 -0
  208. package/src/video/timeline/__tests__/_canvasSelect.ts +414 -0
  209. package/src/video/timeline/__tests__/dragdrop-math.test.ts +135 -0
  210. package/src/video/timeline/__tests__/effectiveItemAudio.test.ts +50 -0
  211. package/src/video/timeline/__tests__/enabledTrackItems.test.ts +164 -0
  212. package/src/video/timeline/__tests__/moveItemAcrossTracks.test.ts +363 -0
  213. package/src/video/timeline/__tests__/multiSelectOps.test.ts +447 -0
  214. package/src/video/timeline/__tests__/placement.test.ts +278 -0
  215. package/src/video/timeline/__tests__/resizeWindowedItem.test.ts +140 -0
  216. package/src/video/timeline/__tests__/timeline-model.test.ts +576 -0
  217. package/src/video/timeline/__tests__/visualItemLabel.test.ts +69 -0
  218. package/src/video/timeline/canvas/TimelineCanvas.tsx +1447 -0
  219. package/src/video/timeline/canvas/__tests__/TimelineCanvas.drop.test.tsx +439 -0
  220. package/src/video/timeline/canvas/__tests__/TimelineCanvas.edgeScroll.test.tsx +346 -0
  221. package/src/video/timeline/canvas/__tests__/TimelineCanvas.panefill.test.tsx +122 -0
  222. package/src/video/timeline/canvas/__tests__/TimelineCanvas.pendingDrops.test.tsx +316 -0
  223. package/src/video/timeline/canvas/__tests__/TimelineCanvas.pointer.test.tsx +606 -0
  224. package/src/video/timeline/canvas/__tests__/TimelineCanvas.test.tsx +407 -0
  225. package/src/video/timeline/canvas/__tests__/clip-bands.test.ts +77 -0
  226. package/src/video/timeline/canvas/__tests__/draw.test.ts +2198 -0
  227. package/src/video/timeline/canvas/__tests__/fade-curve.test.ts +187 -0
  228. package/src/video/timeline/canvas/__tests__/filmstrips.test.ts +561 -0
  229. package/src/video/timeline/canvas/__tests__/hit-test.test.ts +818 -0
  230. package/src/video/timeline/canvas/__tests__/pending-drop.test.ts +210 -0
  231. package/src/video/timeline/canvas/__tests__/pointer-machine.test.ts +3358 -0
  232. package/src/video/timeline/canvas/__tests__/snap.test.ts +257 -0
  233. package/src/video/timeline/canvas/__tests__/viewport.test.ts +399 -0
  234. package/src/video/timeline/canvas/__tests__/waveforms.test.ts +946 -0
  235. package/src/video/timeline/canvas/clip-bands.ts +56 -0
  236. package/src/video/timeline/canvas/draw.ts +2187 -0
  237. package/src/video/timeline/canvas/fade-curve.ts +111 -0
  238. package/src/video/timeline/canvas/filmstrips.ts +418 -0
  239. package/src/video/timeline/canvas/hit-test.ts +501 -0
  240. package/src/video/timeline/canvas/keyframe-strip.ts +73 -0
  241. package/src/video/timeline/canvas/pointer-machine.ts +1828 -0
  242. package/src/video/timeline/canvas/snap.ts +232 -0
  243. package/src/video/timeline/canvas/viewport.ts +457 -0
  244. package/src/video/timeline/canvas/waveforms.ts +664 -0
  245. package/src/video/timeline/makeCaptionEdit.ts +5 -1
  246. package/src/video/timeline/multiSelectOps.ts +214 -55
  247. package/src/video/timeline/placement.ts +282 -0
  248. package/src/video/timeline/timeline-model.ts +919 -0
  249. package/src/video/timeline/useItemDragDrop.ts +151 -178
  250. package/src/video/timeline/useTimelineZoom.ts +25 -60
  251. package/src/video/timeline/utils.ts +0 -12
  252. package/src/video/use-report-context.ts +75 -0
  253. package/src/video/preview/OverlayPropsModal.tsx +0 -292
  254. package/src/video/preview/__tests__/OverlayPropsModal.test.tsx +0 -32
  255. package/src/video/timeline/AudioTrackRow.tsx +0 -404
  256. package/src/video/timeline/AudioWaveformLayer.tsx +0 -117
  257. package/src/video/timeline/CaptionTrackRow.tsx +0 -235
  258. package/src/video/timeline/PlayheadLine.tsx +0 -18
  259. package/src/video/timeline/TranscriptModal.tsx +0 -70
  260. package/src/video/timeline/TranscriptPanel.tsx +0 -273
  261. package/src/video/timeline/VisualTrackRow.tsx +0 -300
  262. package/src/video/timeline/__tests__/CaptionTrackRow.test.tsx +0 -241
  263. package/src/video/timeline/__tests__/PlayheadLine.test.tsx +0 -60
  264. package/src/video/timeline/__tests__/TranscriptModal.test.tsx +0 -41
  265. package/src/video/timeline/__tests__/TranscriptPanel.test.tsx +0 -184
  266. package/src/video/timeline/__tests__/useItemDragDrop.test.ts +0 -72
@@ -0,0 +1,197 @@
1
+ import { describe, it, expect, vi } from 'vitest'
2
+ import { renderHook, act } from '@testing-library/react'
3
+
4
+ import { useDragOverlay, type OverlayChanges } from '../useDragOverlay'
5
+
6
+ // Per-axis resize (scaleX/scaleY) in the preview drag hook.
7
+ //
8
+ // Two things are load-bearing here and neither is visible from the component:
9
+ //
10
+ // 1. The RESIZE MATH. A corner drag scales both axes by one shared delta —
11
+ // which on a uniform item reduces to the exact pre-per-axis formula, so the
12
+ // existing feel is unchanged. An edge drag scales ONE axis: e/w the X axis,
13
+ // n/s the Y axis.
14
+ // 2. The COMMIT SHAPE. A corner drag of an item that has never carried
15
+ // scaleX/scaleY writes only `scale`, so nudging a legacy item does not
16
+ // silently migrate it to per-axis storage. An edge drag, or any drag of an
17
+ // item that already carries per-axis values, writes both axes.
18
+ //
19
+ // The hook listens on `document` for native mouse events, so these dispatch real
20
+ // MouseEvents rather than going through fireEvent (which drops clientX/clientY).
21
+
22
+ // 100×100 container makes a 1px move exactly 1% — every delta below reads
23
+ // directly as a percentage.
24
+ function makeContainer() {
25
+ return {
26
+ current: {
27
+ getBoundingClientRect: () => ({ width: 100, height: 100, left: 0, top: 0, right: 100, bottom: 100 }),
28
+ },
29
+ } as unknown as React.RefObject<HTMLDivElement | null>
30
+ }
31
+
32
+ interface StartOpts {
33
+ type: string
34
+ initScale?: number
35
+ initScaleX?: number
36
+ initScaleY?: number
37
+ initHasPerAxis?: boolean
38
+ initOffsetX?: number
39
+ initOffsetY?: number
40
+ }
41
+
42
+ /** Run one full gesture: mousedown state → mousemove(to) → mouseup. */
43
+ function drag(start: StartOpts, to: { x: number; y: number }) {
44
+ const onOverlayChange = vi.fn()
45
+ const hook = renderHook(() => useDragOverlay(makeContainer(), onOverlayChange))
46
+
47
+ act(() => {
48
+ hook.result.current.setDragState({
49
+ id: 'i1',
50
+ type: start.type as never,
51
+ initX: 0,
52
+ initY: 0,
53
+ initOffsetX: start.initOffsetX ?? 0,
54
+ initOffsetY: start.initOffsetY ?? 0,
55
+ initScale: start.initScale ?? 1,
56
+ initScaleX: start.initScaleX,
57
+ initScaleY: start.initScaleY,
58
+ initHasPerAxis: start.initHasPerAxis,
59
+ initRotation: 0,
60
+ })
61
+ })
62
+
63
+ act(() => {
64
+ document.dispatchEvent(new MouseEvent('mousemove', { clientX: to.x, clientY: to.y }))
65
+ })
66
+ const liveScale = hook.result.current.liveScale
67
+ const snapGuides = hook.result.current.snapGuides
68
+
69
+ act(() => { document.dispatchEvent(new MouseEvent('mouseup')) })
70
+
71
+ const changes: OverlayChanges | undefined = onOverlayChange.mock.calls[0]?.[1]
72
+ return { changes, liveScale, snapGuides, onOverlayChange }
73
+ }
74
+
75
+ describe('useDragOverlay — corner resize (uniform gesture, unchanged)', () => {
76
+ it('scales both axes by the same delta', () => {
77
+ // se: dirX=+1, dirY=+1 → delta = (10 + 10)/100 = 0.2
78
+ const { liveScale } = drag({ type: 'resize-se' }, { x: 10, y: 10 })
79
+ expect(liveScale!.scaleX).toBeCloseTo(1.2)
80
+ expect(liveScale!.scaleY).toBeCloseTo(1.2)
81
+ expect(liveScale!.scale).toBeCloseTo(1.2)
82
+ })
83
+
84
+ it('still reproduces the pre-per-axis formula exactly', () => {
85
+ const initScale = 1.7
86
+ const { liveScale } = drag({ type: 'resize-nw', initScale }, { x: -6, y: -4 })
87
+ // nw: dirX=-1, dirY=-1 → delta = ((-6)*-1 + (-4)*-1)/100 = 0.1
88
+ expect(liveScale!.scale).toBeCloseTo(initScale * 1.1)
89
+ expect(liveScale!.scaleX).toBeCloseTo(initScale * 1.1)
90
+ })
91
+
92
+ it('COMMITS ONLY `scale` for an item with no per-axis values (legacy stays legacy)', () => {
93
+ const { changes } = drag({ type: 'resize-se' }, { x: 10, y: 10 })
94
+ expect(Object.keys(changes!).sort()).toEqual(['scale'])
95
+ expect(changes!.scale).toBeCloseTo(1.2)
96
+ expect(changes!.scaleX).toBeUndefined()
97
+ expect(changes!.scaleY).toBeUndefined()
98
+ })
99
+
100
+ it('commits both axes for an item that ALREADY carries per-axis values', () => {
101
+ const { changes } = drag(
102
+ { type: 'resize-se', initScale: 1, initScaleX: 2, initScaleY: 0.5, initHasPerAxis: true },
103
+ { x: 10, y: 10 },
104
+ )
105
+ expect(changes!.scaleX).toBeCloseTo(2.4)
106
+ expect(changes!.scaleY).toBeCloseTo(0.6)
107
+ // Aspect ratio survives a corner drag of a non-uniform item.
108
+ expect(changes!.scaleX! / changes!.scaleY!).toBeCloseTo(4)
109
+ })
110
+ })
111
+
112
+ describe('useDragOverlay — edge resize (single axis)', () => {
113
+ it('`e` grows X only and leaves Y alone', () => {
114
+ const { changes } = drag({ type: 'resize-e' }, { x: 10, y: 40 })
115
+ expect(changes!.scaleX).toBeCloseTo(1.1)
116
+ expect(changes!.scaleY).toBe(1) // vertical mouse travel is ignored
117
+ })
118
+
119
+ it('`w` inverts the direction', () => {
120
+ const { changes } = drag({ type: 'resize-w' }, { x: 10, y: 0 })
121
+ expect(changes!.scaleX).toBeCloseTo(0.9)
122
+ expect(changes!.scaleY).toBe(1)
123
+ })
124
+
125
+ it('`s` grows Y only', () => {
126
+ const { changes } = drag({ type: 'resize-s' }, { x: 40, y: 10 })
127
+ expect(changes!.scaleX).toBe(1) // horizontal mouse travel is ignored
128
+ expect(changes!.scaleY).toBeCloseTo(1.1)
129
+ })
130
+
131
+ it('`n` inverts the direction', () => {
132
+ const { changes } = drag({ type: 'resize-n' }, { x: 0, y: 10 })
133
+ expect(changes!.scaleY).toBeCloseTo(0.9)
134
+ })
135
+
136
+ it('COMMITS BOTH AXES even on a legacy item — an edge drag is per-axis by definition', () => {
137
+ const { changes } = drag({ type: 'resize-e' }, { x: 10, y: 0 })
138
+ expect(Object.keys(changes!).sort()).toEqual(['scale', 'scaleX', 'scaleY'])
139
+ // The uniform knob is untouched: only a corner drag moves it.
140
+ expect(changes!.scale).toBe(1)
141
+ })
142
+
143
+ it('starts from the resolved per-axis base, not the uniform scale', () => {
144
+ const { changes } = drag(
145
+ { type: 'resize-e', initScale: 1, initScaleX: 3, initScaleY: 0.25, initHasPerAxis: true },
146
+ { x: 10, y: 0 },
147
+ )
148
+ expect(changes!.scaleX).toBeCloseTo(3.3)
149
+ expect(changes!.scaleY).toBeCloseTo(0.25)
150
+ })
151
+ })
152
+
153
+ describe('useDragOverlay — resize floor', () => {
154
+ it('floors each axis at 0.1 independently', () => {
155
+ const corner = drag({ type: 'resize-se' }, { x: -100, y: -100 })
156
+ expect(corner.changes!.scale).toBe(0.1)
157
+
158
+ const edge = drag({ type: 'resize-e' }, { x: -500, y: 0 })
159
+ expect(edge.changes!.scaleX).toBe(0.1)
160
+ expect(edge.changes!.scaleY).toBe(1) // the untouched axis is not floored
161
+ })
162
+ })
163
+
164
+ describe('useDragOverlay — per-axis edge snap while moving', () => {
165
+ // Element is inset-0 then scaled from center, so its edge meets the frame's at
166
+ // offset ±(0.5 - s/2)*100. At s=1 that is 0 — indistinguishable from the center
167
+ // snap — so an axis at scale 1 has no edge snap at all. Per axis, that means a
168
+ // half-width/full-height item snaps left/right but never top/bottom.
169
+ it('a narrow item snaps on X and not on Y', () => {
170
+ const { changes, snapGuides } = drag(
171
+ { type: 'move', initScale: 1, initScaleX: 0.5, initScaleY: 1 },
172
+ { x: -24, y: 40 },
173
+ )
174
+ expect(snapGuides.left).toBe(true)
175
+ expect(changes!.offsetX).toBeCloseTo(-25) // snapped to the left edge
176
+ expect(snapGuides.top).toBe(false)
177
+ expect(snapGuides.bottom).toBe(false)
178
+ expect(changes!.offsetY).toBeCloseTo(40) // untouched
179
+ })
180
+
181
+ it('a short item snaps on Y and not on X', () => {
182
+ const { changes, snapGuides } = drag(
183
+ { type: 'move', initScale: 1, initScaleX: 1, initScaleY: 0.5 },
184
+ { x: -24, y: -24 },
185
+ )
186
+ expect(snapGuides.top).toBe(true)
187
+ expect(changes!.offsetY).toBeCloseTo(-25)
188
+ expect(snapGuides.left).toBe(false)
189
+ expect(changes!.offsetX).toBeCloseTo(-24) // no X edge to snap to at scale 1
190
+ })
191
+
192
+ it('a uniform scaled-down item still snaps on both axes (no regression)', () => {
193
+ const { snapGuides } = drag({ type: 'move', initScale: 0.5 }, { x: -24, y: -24 })
194
+ expect(snapGuides.left).toBe(true)
195
+ expect(snapGuides.top).toBe(true)
196
+ })
197
+ })