@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
@@ -1,184 +0,0 @@
1
- import { describe, it, expect, vi, afterEach } from 'vitest'
2
- import { render, screen, fireEvent, cleanup } from '@testing-library/react'
3
- import type { Project } from '../../../types'
4
- import type { Captions } from '../../../schema'
5
- import TranscriptPanel from '../TranscriptPanel'
6
-
7
- afterEach(() => cleanup())
8
-
9
- function makeProject(style: Captions['style'], extra: Partial<Captions> = {}): Project {
10
- return { id: 'p1', captions: { style, segments: [], ...extra } } as unknown as Project
11
- }
12
-
13
- function renderPanel(
14
- style: Captions['style'],
15
- extra: Partial<Captions> = {},
16
- opts: { selectedCaptionId?: string | null } = {},
17
- ) {
18
- const onCaptionEdit = vi.fn()
19
- const onProjectChange = vi.fn()
20
- const onCaptionSegmentChange = vi.fn()
21
- const project = makeProject(style, extra)
22
- render(
23
- <TranscriptPanel
24
- project={project}
25
- captionTrack={project.captions}
26
- currentTime={0}
27
- onCaptionEdit={onCaptionEdit}
28
- onProjectChange={onProjectChange}
29
- onCaptionSegmentChange={onCaptionSegmentChange}
30
- selectedCaptionId={opts.selectedCaptionId ?? null}
31
- onExpand={() => {}}
32
- />,
33
- )
34
- return { onCaptionEdit, onProjectChange, onCaptionSegmentChange }
35
- }
36
-
37
- describe('TranscriptPanel caption color controls', () => {
38
- it('previews live on change and commits on blur, writing captions.color', () => {
39
- const { onCaptionEdit, onProjectChange } = renderPanel('karaoke')
40
- const input = screen.getByLabelText('Caption text color') as HTMLInputElement
41
-
42
- fireEvent.change(input, { target: { value: '#76b900' } })
43
- expect(onProjectChange).toHaveBeenCalledTimes(1)
44
- expect(onProjectChange.mock.calls[0][0].captions.color).toBe('#76b900')
45
- expect(onCaptionEdit).not.toHaveBeenCalled() // live preview must not persist
46
-
47
- fireEvent.blur(input, { target: { value: '#76b900' } })
48
- expect(onCaptionEdit).toHaveBeenCalledTimes(1)
49
- expect(onCaptionEdit.mock.calls[0][0].captions.color).toBe('#76b900')
50
- })
51
-
52
- it('writes the accent to the field the active style actually reads', () => {
53
- // karaoke → highlightColor
54
- let h = renderPanel('karaoke')
55
- fireEvent.blur(screen.getByLabelText('Caption highlight color'), { target: { value: '#111111' } })
56
- expect(h.onCaptionEdit.mock.calls[0][0].captions.highlightColor).toBe('#111111')
57
- cleanup()
58
-
59
- // pop → activeColor
60
- h = renderPanel('pop')
61
- fireEvent.blur(screen.getByLabelText('Caption active color'), { target: { value: '#222222' } })
62
- expect(h.onCaptionEdit.mock.calls[0][0].captions.activeColor).toBe('#222222')
63
- cleanup()
64
-
65
- // subtitle → backgroundColor
66
- h = renderPanel('subtitle')
67
- fireEvent.blur(screen.getByLabelText('Caption box color'), { target: { value: '#333333' } })
68
- expect(h.onCaptionEdit.mock.calls[0][0].captions.backgroundColor).toBe('#333333')
69
- cleanup()
70
-
71
- // highlight-box → accentColor
72
- h = renderPanel('highlight-box')
73
- fireEvent.blur(screen.getByLabelText('Caption accent color'), { target: { value: '#444444' } })
74
- expect(h.onCaptionEdit.mock.calls[0][0].captions.accentColor).toBe('#444444')
75
- })
76
-
77
- it('hides the accent swatch for styles with no accent', () => {
78
- renderPanel('clean')
79
- expect(screen.getByLabelText('Caption text color')).toBeTruthy()
80
- expect(screen.queryByLabelText(/(accent|highlight|active|box) color/i)).toBeNull()
81
- cleanup()
82
-
83
- renderPanel('word-by-word')
84
- expect(screen.queryByLabelText(/(accent|highlight|active|box) color/i)).toBeNull()
85
- })
86
-
87
- it('reflects the stored hex value on the swatch input', () => {
88
- renderPanel('karaoke', { color: '#abcdef', highlightColor: '#00ff00' })
89
- expect((screen.getByLabelText('Caption text color') as HTMLInputElement).value).toBe('#abcdef')
90
- expect((screen.getByLabelText('Caption highlight color') as HTMLInputElement).value).toBe('#00ff00')
91
- })
92
- })
93
-
94
- describe('TranscriptPanel per-segment caption color', () => {
95
- it('no selection: base swatch is unchanged — still previews/commits the track-level color', () => {
96
- const { onCaptionEdit, onProjectChange } = renderPanel('karaoke', {
97
- segments: [{ id: 'cap-0', text: 'hi', start: 0, end: 2 }],
98
- })
99
- const input = screen.getByLabelText('Caption text color') as HTMLInputElement
100
-
101
- fireEvent.change(input, { target: { value: '#76b900' } })
102
- expect(onProjectChange).toHaveBeenCalledTimes(1)
103
- expect(onProjectChange.mock.calls[0][0].captions.color).toBe('#76b900')
104
-
105
- fireEvent.blur(input, { target: { value: '#76b900' } })
106
- expect(onCaptionEdit).toHaveBeenCalledTimes(1)
107
- expect(onCaptionEdit.mock.calls[0][0].captions.color).toBe('#76b900')
108
- })
109
-
110
- it('a selected segment: swatch previews live via onProjectChange (only that segment patched) and commits via onCaptionSegmentChange — never onCaptionEdit', () => {
111
- const { onCaptionEdit, onProjectChange, onCaptionSegmentChange } = renderPanel('karaoke', {
112
- color: '#ffffff',
113
- segments: [
114
- { id: 'cap-0', text: 'hi', start: 0, end: 2 },
115
- { id: 'cap-1', text: 'there', start: 2, end: 4 },
116
- ],
117
- }, { selectedCaptionId: 'cap-1' })
118
-
119
- const input = screen.getByLabelText('Selected segment text color') as HTMLInputElement
120
-
121
- // Live preview — onChange fires on every pick.
122
- fireEvent.change(input, { target: { value: '#123456' } })
123
- expect(onProjectChange).toHaveBeenCalledTimes(1)
124
- const previewed = onProjectChange.mock.calls[0][0]
125
- expect(previewed.captions.segments[1].color).toBe('#123456')
126
- expect(previewed.captions.segments[0].color).toBeUndefined() // only the selected segment changes
127
- expect(previewed.captions.color).toBe('#ffffff') // track-level color untouched
128
- expect(onCaptionSegmentChange).not.toHaveBeenCalled()
129
- expect(onCaptionEdit).not.toHaveBeenCalled()
130
-
131
- // Commit — onBlur fires once, through the segment-patch channel, not
132
- // the whole-track onCaptionEdit channel. One commit per gesture.
133
- fireEvent.blur(input, { target: { value: '#123456' } })
134
- expect(onCaptionSegmentChange).toHaveBeenCalledTimes(1)
135
- expect(onCaptionSegmentChange).toHaveBeenCalledWith('cap-1', { color: '#123456' })
136
- expect(onCaptionEdit).not.toHaveBeenCalled()
137
- })
138
-
139
- it("the swatch reflects the selected segment's own color when it has one", () => {
140
- renderPanel('karaoke', {
141
- color: '#abcdef',
142
- segments: [{ id: 'cap-0', text: 'hi', start: 0, end: 2, color: '#00ff00' }],
143
- }, { selectedCaptionId: 'cap-0' })
144
- expect((screen.getByLabelText('Selected segment text color') as HTMLInputElement).value).toBe('#00ff00')
145
- })
146
-
147
- it('the swatch falls back to the track color when the selected segment has none of its own', () => {
148
- renderPanel('karaoke', {
149
- color: '#abcdef',
150
- segments: [{ id: 'cap-0', text: 'hi', start: 0, end: 2 }],
151
- }, { selectedCaptionId: 'cap-0' })
152
- expect((screen.getByLabelText('Selected segment text color') as HTMLInputElement).value).toBe('#abcdef')
153
- })
154
-
155
- it('a selectedCaptionId that matches no segment falls back to track-level behavior', () => {
156
- renderPanel('karaoke', {
157
- segments: [{ id: 'cap-0', text: 'hi', start: 0, end: 2 }],
158
- }, { selectedCaptionId: 'cap-does-not-exist' })
159
- expect(screen.getByLabelText('Caption text color')).toBeTruthy()
160
- expect(screen.queryByLabelText('Selected segment text color')).toBeNull()
161
- })
162
- })
163
-
164
- describe('TranscriptPanel caption text edits', () => {
165
- // Regression: makeCaptionEdit fires every callback it's given with the same
166
- // updated project. TranscriptPanel used to pass BOTH onProjectChange and
167
- // onCaptionEdit into makeCaptionEdit for a text edit, and — at this
168
- // component's real call site (VideoEditor's ReviewSurface) — both land on
169
- // sync.mutate, so one text edit produced two undo entries and two queued
170
- // saves. Only onCaptionEdit (the commit channel) should fire.
171
- it('a completed text edit produces exactly one commit, not two', () => {
172
- const { onCaptionEdit, onProjectChange } = renderPanel('karaoke', {
173
- segments: [{ id: 'cap-0', text: 'hello world', start: 0, end: 2 }],
174
- })
175
-
176
- const span = screen.getByText('hello world')
177
- span.textContent = 'hello there'
178
- fireEvent.blur(span)
179
-
180
- expect(onCaptionEdit).toHaveBeenCalledTimes(1)
181
- expect(onCaptionEdit.mock.calls[0][0].captions.segments[0].text).toBe('hello there')
182
- expect(onProjectChange).not.toHaveBeenCalled()
183
- })
184
- })
@@ -1,72 +0,0 @@
1
- import { describe, it, expect, vi, afterEach } from 'vitest'
2
- import { renderHook } from '@testing-library/react'
3
- import { useItemDragDrop } from '../useItemDragDrop'
4
-
5
- // A press that drifts < 4px must stay a CLICK (so the timeline's click-to-select
6
- // fires); only a press that travels past the threshold becomes a drag. Regression
7
- // guard for the bug where any pixel of jitter suppressed clip selection.
8
-
9
- const RECT = {
10
- width: 1000, height: 100, left: 0, top: 0, right: 1000, bottom: 100, x: 0, y: 0,
11
- toJSON: () => ({}),
12
- } as DOMRect
13
-
14
- function setup() {
15
- const draggedFlagRef = { current: false }
16
- const scrollRef = { current: { getBoundingClientRect: () => RECT } }
17
- const { result } = renderHook(() =>
18
- useItemDragDrop({
19
- totalDuration: 100,
20
- snapBoundaries: [],
21
- scrollRef: scrollRef as unknown as React.RefObject<HTMLDivElement>,
22
- draggedFlagRef: draggedFlagRef as unknown as React.MutableRefObject<boolean>,
23
- }),
24
- )
25
- return { beginDrag: result.current.beginDrag, draggedFlagRef }
26
- }
27
-
28
- const ITEM = { id: 'a', start: 10, end: 20 }
29
-
30
- function press(beginDrag: ReturnType<typeof setup>['beginDrag']) {
31
- const onLivePreview = vi.fn()
32
- const onCommit = vi.fn()
33
- beginDrag(
34
- { stopPropagation: vi.fn(), clientX: 200, clientY: 200 } as unknown as React.MouseEvent,
35
- ITEM,
36
- { onLivePreview, onCommit },
37
- )
38
- return { onLivePreview, onCommit }
39
- }
40
-
41
- afterEach(() => {
42
- // ensure no stray listeners between tests
43
- document.dispatchEvent(new MouseEvent('mouseup'))
44
- vi.restoreAllMocks()
45
- })
46
-
47
- describe('useItemDragDrop — click vs drag threshold', () => {
48
- it('sub-threshold drift stays a click: no drag flag, no move, no commit', () => {
49
- const { beginDrag, draggedFlagRef } = setup()
50
- const { onLivePreview, onCommit } = press(beginDrag)
51
-
52
- // ~2.2px of jitter (hypot(2,1)) — under the 4px threshold
53
- document.dispatchEvent(new MouseEvent('mousemove', { clientX: 202, clientY: 201 }))
54
- document.dispatchEvent(new MouseEvent('mouseup'))
55
-
56
- expect(draggedFlagRef.current).toBe(false) // click-to-select is NOT suppressed
57
- expect(onLivePreview).not.toHaveBeenCalled()
58
- expect(onCommit).not.toHaveBeenCalled()
59
- })
60
-
61
- it('moving past the threshold becomes a drag: flag set, move + commit fire', () => {
62
- const { beginDrag, draggedFlagRef } = setup()
63
- const { onLivePreview, onCommit } = press(beginDrag)
64
-
65
- document.dispatchEvent(new MouseEvent('mousemove', { clientX: 240, clientY: 200 })) // 40px → drag
66
- expect(draggedFlagRef.current).toBe(true)
67
- expect(onLivePreview).toHaveBeenCalled()
68
-
69
- document.dispatchEvent(new MouseEvent('mouseup'))
70
- expect(onCommit).toHaveBeenCalled()
71
- })
72
- })