@bycrux/editor 0.11.2 → 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 (268) 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 +83 -1
  51. package/src/preview/OverlayPreview.tsx +2 -24
  52. package/src/schema.ts +146 -3
  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 +601 -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 +137 -0
  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 +2229 -243
  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/imageTone.ts +58 -0
  147. package/src/video/imageToneExamples.ts +10 -0
  148. package/src/video/keyframeOps.ts +384 -0
  149. package/src/video/keymap.ts +146 -0
  150. package/src/video/panels/ClipPropertiesPanel.tsx +706 -0
  151. package/src/video/panels/LeftPanelTabs.tsx +187 -0
  152. package/src/video/panels/OverlayContentPanel.tsx +351 -0
  153. package/src/video/panels/TabNav.tsx +60 -0
  154. package/src/video/panels/__tests__/ClipPropertiesPanel.test.tsx +645 -0
  155. package/src/video/panels/__tests__/LeftPanelTabs.test.tsx +189 -0
  156. package/src/video/panels/__tests__/OverlayContentPanel.test.tsx +222 -0
  157. package/src/video/panels/__tests__/TabNav.test.tsx +71 -0
  158. package/src/video/preview/CaptionPreview.tsx +57 -69
  159. package/src/video/preview/EngineSurface.tsx +103 -0
  160. package/src/video/preview/OverlayItemsLayer.tsx +315 -103
  161. package/src/video/preview/PreviewPlayer.tsx +337 -56
  162. package/src/video/preview/SocialPreviewMenu.tsx +214 -0
  163. package/src/video/preview/SocialSafeZoneOverlay.tsx +478 -0
  164. package/src/video/preview/__tests__/CaptionPreview.fonts.test.tsx +97 -0
  165. package/src/video/preview/__tests__/EngineSurface.test.tsx +114 -0
  166. package/src/video/preview/__tests__/OverlayItemsLayer.edit.test.tsx +4 -2
  167. package/src/video/preview/__tests__/OverlayItemsLayer.keyframes.test.tsx +363 -0
  168. package/src/video/preview/__tests__/OverlayItemsLayer.selection.test.tsx +329 -0
  169. package/src/video/preview/__tests__/PreviewPlayer.engine.test.tsx +139 -0
  170. package/src/video/preview/__tests__/SocialPreviewMenu.test.tsx +121 -0
  171. package/src/video/preview/__tests__/SocialSafeZoneOverlay.test.tsx +165 -0
  172. package/src/video/preview/__tests__/captionDragState.test.ts +120 -1
  173. package/src/video/preview/__tests__/latencyCompensation.test.tsx +451 -0
  174. package/src/video/preview/__tests__/proxySupport.test.ts +75 -0
  175. package/src/video/preview/__tests__/transformStyle.test.ts +29 -1
  176. package/src/video/preview/__tests__/useDragOverlay.perAxis.test.ts +197 -0
  177. package/src/video/preview/__tests__/useEnginePlayback.test.tsx +530 -0
  178. package/src/video/preview/__tests__/useVideoPlayback.corpus.test.ts +313 -0
  179. package/src/video/preview/__tests__/useVideoPlayback.test.ts +38 -5
  180. package/src/video/preview/__tests__/useVideoPlayback.trackAudio.test.ts +278 -0
  181. package/src/video/preview/audio-context.ts +111 -0
  182. package/src/video/preview/captionDragState.ts +91 -1
  183. package/src/video/preview/proxySupport.ts +86 -0
  184. package/src/video/preview/transformStyle.ts +22 -12
  185. package/src/video/preview/useDragOverlay.ts +92 -18
  186. package/src/video/preview/useEnginePlayback.ts +625 -0
  187. package/src/video/preview/useVideoPlayback.ts +211 -167
  188. package/src/video/sdrCurves.ts +56 -0
  189. package/src/video/shuttle.ts +159 -0
  190. package/src/video/source-preview.ts +66 -0
  191. package/src/video/timecode.ts +59 -0
  192. package/src/video/timeline/EditableSegment.tsx +1 -1
  193. package/src/video/timeline/Scrubber.tsx +46 -174
  194. package/src/video/timeline/SpeedControl.tsx +95 -0
  195. package/src/video/timeline/Timeline.tsx +1061 -328
  196. package/src/video/timeline/TimelineContext.ts +17 -10
  197. package/src/video/timeline/TrackGutter.tsx +560 -0
  198. package/src/video/timeline/TrackSettingsPopover.tsx +228 -0
  199. package/src/video/timeline/VolumeControl.tsx +113 -0
  200. package/src/video/timeline/__tests__/Timeline.backgroundClick.test.tsx +110 -0
  201. package/src/video/timeline/__tests__/Timeline.crossfade.test.tsx +104 -0
  202. package/src/video/timeline/__tests__/Timeline.fadeCurveMenu.test.tsx +174 -0
  203. package/src/video/timeline/__tests__/Timeline.keyframeDelete.test.tsx +273 -0
  204. package/src/video/timeline/__tests__/Timeline.keyframeFollow.test.tsx +215 -0
  205. package/src/video/timeline/__tests__/Timeline.keyframeMenu.test.tsx +253 -0
  206. package/src/video/timeline/__tests__/Timeline.keymap.test.tsx +372 -0
  207. package/src/video/timeline/__tests__/Timeline.subcutRegen.test.tsx +351 -0
  208. package/src/video/timeline/__tests__/TrackGutter.test.tsx +372 -0
  209. package/src/video/timeline/__tests__/_canvasSelect.test.tsx +273 -0
  210. package/src/video/timeline/__tests__/_canvasSelect.ts +414 -0
  211. package/src/video/timeline/__tests__/dragdrop-math.test.ts +135 -0
  212. package/src/video/timeline/__tests__/effectiveItemAudio.test.ts +50 -0
  213. package/src/video/timeline/__tests__/enabledTrackItems.test.ts +164 -0
  214. package/src/video/timeline/__tests__/moveItemAcrossTracks.test.ts +363 -0
  215. package/src/video/timeline/__tests__/multiSelectOps.test.ts +447 -0
  216. package/src/video/timeline/__tests__/placement.test.ts +278 -0
  217. package/src/video/timeline/__tests__/resizeWindowedItem.test.ts +140 -0
  218. package/src/video/timeline/__tests__/timeline-model.test.ts +576 -0
  219. package/src/video/timeline/__tests__/visualItemLabel.test.ts +69 -0
  220. package/src/video/timeline/canvas/TimelineCanvas.tsx +1447 -0
  221. package/src/video/timeline/canvas/__tests__/TimelineCanvas.drop.test.tsx +439 -0
  222. package/src/video/timeline/canvas/__tests__/TimelineCanvas.edgeScroll.test.tsx +346 -0
  223. package/src/video/timeline/canvas/__tests__/TimelineCanvas.panefill.test.tsx +122 -0
  224. package/src/video/timeline/canvas/__tests__/TimelineCanvas.pendingDrops.test.tsx +316 -0
  225. package/src/video/timeline/canvas/__tests__/TimelineCanvas.pointer.test.tsx +606 -0
  226. package/src/video/timeline/canvas/__tests__/TimelineCanvas.test.tsx +407 -0
  227. package/src/video/timeline/canvas/__tests__/clip-bands.test.ts +77 -0
  228. package/src/video/timeline/canvas/__tests__/draw.test.ts +2198 -0
  229. package/src/video/timeline/canvas/__tests__/fade-curve.test.ts +187 -0
  230. package/src/video/timeline/canvas/__tests__/filmstrips.test.ts +561 -0
  231. package/src/video/timeline/canvas/__tests__/hit-test.test.ts +818 -0
  232. package/src/video/timeline/canvas/__tests__/pending-drop.test.ts +210 -0
  233. package/src/video/timeline/canvas/__tests__/pointer-machine.test.ts +3358 -0
  234. package/src/video/timeline/canvas/__tests__/snap.test.ts +257 -0
  235. package/src/video/timeline/canvas/__tests__/viewport.test.ts +399 -0
  236. package/src/video/timeline/canvas/__tests__/waveforms.test.ts +946 -0
  237. package/src/video/timeline/canvas/clip-bands.ts +56 -0
  238. package/src/video/timeline/canvas/draw.ts +2187 -0
  239. package/src/video/timeline/canvas/fade-curve.ts +111 -0
  240. package/src/video/timeline/canvas/filmstrips.ts +418 -0
  241. package/src/video/timeline/canvas/hit-test.ts +501 -0
  242. package/src/video/timeline/canvas/keyframe-strip.ts +73 -0
  243. package/src/video/timeline/canvas/pointer-machine.ts +1828 -0
  244. package/src/video/timeline/canvas/snap.ts +232 -0
  245. package/src/video/timeline/canvas/viewport.ts +457 -0
  246. package/src/video/timeline/canvas/waveforms.ts +664 -0
  247. package/src/video/timeline/makeCaptionEdit.ts +5 -1
  248. package/src/video/timeline/multiSelectOps.ts +214 -55
  249. package/src/video/timeline/placement.ts +282 -0
  250. package/src/video/timeline/timeline-model.ts +919 -0
  251. package/src/video/timeline/useItemDragDrop.ts +151 -178
  252. package/src/video/timeline/useTimelineZoom.ts +25 -60
  253. package/src/video/timeline/utils.ts +0 -12
  254. package/src/video/use-report-context.ts +75 -0
  255. package/src/video/preview/OverlayPropsModal.tsx +0 -292
  256. package/src/video/preview/__tests__/OverlayPropsModal.test.tsx +0 -32
  257. package/src/video/timeline/AudioTrackRow.tsx +0 -404
  258. package/src/video/timeline/AudioWaveformLayer.tsx +0 -117
  259. package/src/video/timeline/CaptionTrackRow.tsx +0 -235
  260. package/src/video/timeline/PlayheadLine.tsx +0 -18
  261. package/src/video/timeline/TranscriptModal.tsx +0 -70
  262. package/src/video/timeline/TranscriptPanel.tsx +0 -273
  263. package/src/video/timeline/VisualTrackRow.tsx +0 -300
  264. package/src/video/timeline/__tests__/CaptionTrackRow.test.tsx +0 -241
  265. package/src/video/timeline/__tests__/PlayheadLine.test.tsx +0 -60
  266. package/src/video/timeline/__tests__/TranscriptModal.test.tsx +0 -41
  267. package/src/video/timeline/__tests__/TranscriptPanel.test.tsx +0 -184
  268. 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
+ })