@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,210 @@
1
+ /// <reference types="vitest/globals" />
2
+ /**
3
+ * The pending-import ghost band — the placeholder drawn at the drop point
4
+ * while the host is still probing/ingesting a file dropped from the OS.
5
+ *
6
+ * Driven through a recording context (the same technique draw.test.ts uses:
7
+ * the rectangle a painter lands in IS its observable behaviour) rather than a
8
+ * real canvas, so every assertion is on a specific rect, colour or string.
9
+ *
10
+ * Three things here can only be shown at the draw level, and each one is a
11
+ * real bug if it regresses:
12
+ * - the band lands on the rect the caller asked for, at the viewport's own
13
+ * time→x mapping (a ghost drawn anywhere else points at the wrong second);
14
+ * - the dash is RESET before the painter returns — a leaked `setLineDash`
15
+ * would give this layer a dashed playhead;
16
+ * - an absent/empty `pendingDrops` paints nothing at all, which is what keeps
17
+ * the overlay layer byte-identical for every host that never passes one.
18
+ */
19
+ import { describe, it, expect } from 'vitest'
20
+ import {
21
+ DARK_TIMELINE_PALETTE,
22
+ LIGHT_TIMELINE_PALETTE,
23
+ PENDING_DROP_BORDER_PX,
24
+ PENDING_DROP_DASH,
25
+ PENDING_DROP_INSET_PX,
26
+ PLAYHEAD_WIDTH_PX,
27
+ drawPendingDropBand,
28
+ drawTimelineOverlay,
29
+ type DrawContext,
30
+ type PendingDropBand,
31
+ } from '../draw'
32
+ import type { Viewport } from '../viewport'
33
+
34
+ // ── Recording context (draw.test.ts's, kept local so this file stands alone) ──
35
+
36
+ interface RecordedCall { method: string; args: unknown[] }
37
+
38
+ interface Recorder {
39
+ ctx: DrawContext
40
+ calls: RecordedCall[]
41
+ of: (method: string) => RecordedCall[]
42
+ count: (method: string) => number
43
+ }
44
+
45
+ function recordingContext(): Recorder {
46
+ const calls: RecordedCall[] = []
47
+ const props: Record<string, unknown> = {}
48
+
49
+ const proxy = new Proxy({}, {
50
+ get(_target, prop: string) {
51
+ if (prop in props) return props[prop]
52
+ return (...args: unknown[]) => { calls.push({ method: prop, args }) }
53
+ },
54
+ set(_target, prop: string, value: unknown) {
55
+ props[prop] = value
56
+ calls.push({ method: `set:${prop}`, args: [value] })
57
+ return true
58
+ },
59
+ }) as unknown as DrawContext
60
+
61
+ return {
62
+ ctx: proxy,
63
+ calls,
64
+ of: (method: string) => calls.filter(c => c.method === method),
65
+ count: (method: string) => calls.filter(c => c.method === method).length,
66
+ }
67
+ }
68
+
69
+ function viewport(over: Partial<Viewport> = {}): Viewport {
70
+ return { pxPerSecond: 10, scrollSeconds: 0, widthPx: 1000, ...over }
71
+ }
72
+
73
+ function band(over: Partial<PendingDropBand> = {}): PendingDropBand {
74
+ return { start: 2, end: 5, y: 40, height: 120, ...over }
75
+ }
76
+
77
+ describe('drawPendingDropBand', () => {
78
+ it('fills the band at the viewport position, inset inside its row', () => {
79
+ const r = recordingContext()
80
+ // 2s..5s at 10px/s → x=20, width=30. The row is 120 tall at y=40, so the
81
+ // band takes 40+2 .. 158 (2px clear top and bottom).
82
+ drawPendingDropBand(r.ctx, band(), viewport(), 1000)
83
+ const fills = r.of('fill')
84
+ expect(fills).toHaveLength(1)
85
+ // roundRectPath starts at (x + radius, y).
86
+ const [move] = r.of('moveTo')
87
+ expect(move.args).toEqual([20 + 4, 40 + PENDING_DROP_INSET_PX])
88
+ expect(r.of('set:fillStyle')[0].args[0]).toBe(DARK_TIMELINE_PALETTE.colors.pendingDropFill)
89
+ })
90
+
91
+ it('strokes the outline DASHED — the one mark that says "not real yet"', () => {
92
+ const r = recordingContext()
93
+ drawPendingDropBand(r.ctx, band(), viewport(), 1000)
94
+ const dashes = r.of('setLineDash')
95
+ expect(dashes[0].args[0]).toEqual([...PENDING_DROP_DASH])
96
+ expect(r.of('set:strokeStyle')[0].args[0]).toBe(DARK_TIMELINE_PALETTE.colors.pendingDropStroke)
97
+ expect(r.of('set:lineWidth')[0].args[0]).toBe(PENDING_DROP_BORDER_PX)
98
+ expect(r.count('stroke')).toBe(1)
99
+ })
100
+
101
+ it('resets the dash before returning, so nothing painted later inherits it', () => {
102
+ const r = recordingContext()
103
+ drawPendingDropBand(r.ctx, band(), viewport(), 1000)
104
+ const dashes = r.of('setLineDash')
105
+ expect(dashes).toHaveLength(2)
106
+ expect(dashes[1].args[0]).toEqual([])
107
+ // And it happened after the stroke it was set for, not before it.
108
+ const dashIdx = r.calls.findIndex((c, i) => c.method === 'setLineDash' && i > r.calls.findIndex(x => x.method === 'stroke'))
109
+ expect(dashIdx).toBeGreaterThan(-1)
110
+ })
111
+
112
+ it('draws the label inside the band, clipped so a long filename cannot bleed out', () => {
113
+ const r = recordingContext()
114
+ drawPendingDropBand(r.ctx, band({ label: 'C0042.MP4' }), viewport(), 1000)
115
+ const text = r.of('fillText')
116
+ expect(text).toHaveLength(1)
117
+ expect(text[0].args[0]).toBe('C0042.MP4')
118
+ // Clipped to the band's own rect — the clip path is the plain rect, not
119
+ // the rounded one the fill used.
120
+ expect(r.of('rect')[0].args).toEqual([20, 42, 30, 116])
121
+ expect(r.count('clip')).toBe(1)
122
+ })
123
+
124
+ it('draws no label at all when the band carries none', () => {
125
+ const r = recordingContext()
126
+ drawPendingDropBand(r.ctx, band(), viewport(), 1000)
127
+ expect(r.count('fillText')).toBe(0)
128
+ expect(r.count('clip')).toBe(0)
129
+ })
130
+
131
+ it('skips a band scrolled entirely off-screen', () => {
132
+ const r = recordingContext()
133
+ // 2s..5s with the view starting at 400s is 3980px to the left of x=0.
134
+ drawPendingDropBand(r.ctx, band(), viewport({ scrollSeconds: 400 }), 1000)
135
+ expect(r.count('fill')).toBe(0)
136
+ expect(r.count('stroke')).toBe(0)
137
+ })
138
+
139
+ it('takes its colours from the palette it is handed, in both modes', () => {
140
+ const dark = recordingContext()
141
+ const light = recordingContext()
142
+ drawPendingDropBand(dark.ctx, band({ label: 'a.mov' }), viewport(), 1000, DARK_TIMELINE_PALETTE)
143
+ drawPendingDropBand(light.ctx, band({ label: 'a.mov' }), viewport(), 1000, LIGHT_TIMELINE_PALETTE)
144
+ expect(dark.of('set:fillStyle').map(c => c.args[0])).toEqual([
145
+ DARK_TIMELINE_PALETTE.colors.pendingDropFill,
146
+ DARK_TIMELINE_PALETTE.colors.pendingDropText,
147
+ ])
148
+ expect(light.of('set:fillStyle').map(c => c.args[0])).toEqual([
149
+ LIGHT_TIMELINE_PALETTE.colors.pendingDropFill,
150
+ LIGHT_TIMELINE_PALETTE.colors.pendingDropText,
151
+ ])
152
+ })
153
+ })
154
+
155
+ describe('drawTimelineOverlay — pending drops', () => {
156
+ it('paints a ghost band for each pending drop', () => {
157
+ const r = recordingContext()
158
+ drawTimelineOverlay(r.ctx, {
159
+ viewport: viewport(),
160
+ currentTime: 0,
161
+ surfaceWidth: 1000,
162
+ surfaceHeight: 200,
163
+ pendingDrops: [band(), band({ start: 20, end: 24, y: 164, height: 40 })],
164
+ })
165
+ // Two ghost fills, each starting its rounded path at its own rect. Each
166
+ // band lays TWO paths — the fill's, then the dashed outline's, inset by
167
+ // half the stroke — so the fill paths are the even entries.
168
+ const fillPaths = r.of('moveTo').filter((_, i) => i % 2 === 0).map(c => c.args)
169
+ expect(fillPaths).toEqual([
170
+ [24, 42],
171
+ [204, 166],
172
+ ])
173
+ expect(r.of('fill')).toHaveLength(2)
174
+ })
175
+
176
+ it('paints nothing extra when there are no pending drops', () => {
177
+ for (const pendingDrops of [undefined, []]) {
178
+ const r = recordingContext()
179
+ drawTimelineOverlay(r.ctx, {
180
+ viewport: viewport(),
181
+ currentTime: 5,
182
+ surfaceWidth: 1000,
183
+ surfaceHeight: 200,
184
+ pendingDrops,
185
+ })
186
+ // The playhead's `fillRect` and nothing else: no path, no dash, no
187
+ // stroke — this layer is byte-identical to what it was before ghosts.
188
+ expect(r.count('fill')).toBe(0)
189
+ expect(r.count('stroke')).toBe(0)
190
+ expect(r.count('setLineDash')).toBe(0)
191
+ expect(r.of('fillRect')).toHaveLength(1)
192
+ expect(r.of('fillRect')[0].args).toEqual([50 - PLAYHEAD_WIDTH_PX / 2, 0, PLAYHEAD_WIDTH_PX, 200])
193
+ }
194
+ })
195
+
196
+ it('draws the ghosts UNDER the playhead, which must stay legible over one', () => {
197
+ const r = recordingContext()
198
+ drawTimelineOverlay(r.ctx, {
199
+ viewport: viewport(),
200
+ currentTime: 3,
201
+ surfaceWidth: 1000,
202
+ surfaceHeight: 200,
203
+ pendingDrops: [band()],
204
+ })
205
+ const ghostFill = r.calls.findIndex(c => c.method === 'fill')
206
+ const playhead = r.calls.findIndex(c => c.method === 'fillRect')
207
+ expect(ghostFill).toBeGreaterThan(-1)
208
+ expect(playhead).toBeGreaterThan(ghostFill)
209
+ })
210
+ })