@editframe/elements 0.37.2-beta → 0.38.0

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 (321) hide show
  1. package/dist/EF_FRAMEGEN.js +17 -14
  2. package/dist/EF_FRAMEGEN.js.map +1 -1
  3. package/dist/EF_RENDERING.js.map +1 -1
  4. package/dist/canvas/EFCanvas.d.ts +9 -2
  5. package/dist/canvas/EFCanvas.js +14 -4
  6. package/dist/canvas/EFCanvas.js.map +1 -1
  7. package/dist/canvas/EFCanvasItem.d.ts +4 -4
  8. package/dist/canvas/overlays/SelectionOverlay.d.ts +10 -2
  9. package/dist/canvas/overlays/SelectionOverlay.js +5 -12
  10. package/dist/canvas/overlays/SelectionOverlay.js.map +1 -1
  11. package/dist/canvas/overlays/overlayState.js.map +1 -1
  12. package/dist/canvas/selection/SelectionController.js.map +1 -1
  13. package/dist/elements/EFAudio.d.ts +1 -11
  14. package/dist/elements/EFAudio.js +2 -10
  15. package/dist/elements/EFAudio.js.map +1 -1
  16. package/dist/elements/EFCaptions.d.ts +5 -9
  17. package/dist/elements/EFCaptions.js +34 -11
  18. package/dist/elements/EFCaptions.js.map +1 -1
  19. package/dist/elements/EFImage.d.ts +10 -8
  20. package/dist/elements/EFImage.js +117 -32
  21. package/dist/elements/EFImage.js.map +1 -1
  22. package/dist/elements/EFMedia/AssetMediaEngine.js +2 -2
  23. package/dist/elements/EFMedia/AssetMediaEngine.js.map +1 -1
  24. package/dist/elements/EFMedia/BaseMediaEngine.js +15 -92
  25. package/dist/elements/EFMedia/BaseMediaEngine.js.map +1 -1
  26. package/dist/elements/EFMedia/BufferedSeekingInput.js +10 -11
  27. package/dist/elements/EFMedia/BufferedSeekingInput.js.map +1 -1
  28. package/dist/elements/EFMedia/{AssetIdMediaEngine.js → FileMediaEngine.js} +44 -24
  29. package/dist/elements/EFMedia/FileMediaEngine.js.map +1 -0
  30. package/dist/elements/EFMedia/JitMediaEngine.js +14 -13
  31. package/dist/elements/EFMedia/JitMediaEngine.js.map +1 -1
  32. package/dist/elements/EFMedia/shared/AudioSpanUtils.js +3 -3
  33. package/dist/elements/EFMedia/shared/AudioSpanUtils.js.map +1 -1
  34. package/dist/elements/EFMedia/shared/ThumbnailExtractor.js +12 -7
  35. package/dist/elements/EFMedia/shared/ThumbnailExtractor.js.map +1 -1
  36. package/dist/elements/EFMedia/shared/timeoutUtils.js +44 -0
  37. package/dist/elements/EFMedia/shared/timeoutUtils.js.map +1 -0
  38. package/dist/elements/EFMedia/videoTasks/MainVideoInputCache.js +1 -1
  39. package/dist/elements/EFMedia/videoTasks/MainVideoInputCache.js.map +1 -1
  40. package/dist/elements/EFMedia/videoTasks/ScrubInputCache.js +4 -4
  41. package/dist/elements/EFMedia/videoTasks/ScrubInputCache.js.map +1 -1
  42. package/dist/elements/EFMedia.d.ts +14 -8
  43. package/dist/elements/EFMedia.js +52 -19
  44. package/dist/elements/EFMedia.js.map +1 -1
  45. package/dist/elements/EFPanZoom.d.ts +2 -2
  46. package/dist/elements/EFPanZoom.js +1 -1
  47. package/dist/elements/EFPanZoom.js.map +1 -1
  48. package/dist/elements/EFSourceMixin.js +16 -8
  49. package/dist/elements/EFSourceMixin.js.map +1 -1
  50. package/dist/elements/EFSurface.d.ts +7 -10
  51. package/dist/elements/EFSurface.js +4 -43
  52. package/dist/elements/EFSurface.js.map +1 -1
  53. package/dist/elements/EFTemporal.d.ts +33 -8
  54. package/dist/elements/EFTemporal.js +92 -40
  55. package/dist/elements/EFTemporal.js.map +1 -1
  56. package/dist/elements/EFText.d.ts +3 -0
  57. package/dist/elements/EFText.js +54 -21
  58. package/dist/elements/EFText.js.map +1 -1
  59. package/dist/elements/EFTextSegment.js +8 -4
  60. package/dist/elements/EFTextSegment.js.map +1 -1
  61. package/dist/elements/EFTimegroup.d.ts +26 -43
  62. package/dist/elements/EFTimegroup.js +295 -314
  63. package/dist/elements/EFTimegroup.js.map +1 -1
  64. package/dist/elements/EFVideo.d.ts +44 -42
  65. package/dist/elements/EFVideo.js +259 -172
  66. package/dist/elements/EFVideo.js.map +1 -1
  67. package/dist/elements/EFWaveform.d.ts +3 -8
  68. package/dist/elements/EFWaveform.js +18 -13
  69. package/dist/elements/EFWaveform.js.map +1 -1
  70. package/dist/elements/ElementPositionInfo.js.map +1 -1
  71. package/dist/elements/FetchMixin.js.map +1 -1
  72. package/dist/elements/TargetController.d.ts +0 -3
  73. package/dist/elements/TargetController.js +12 -35
  74. package/dist/elements/TargetController.js.map +1 -1
  75. package/dist/elements/TimegroupController.js.map +1 -1
  76. package/dist/elements/cloneFactoryRegistry.d.ts +14 -0
  77. package/dist/elements/cloneFactoryRegistry.js +15 -0
  78. package/dist/elements/cloneFactoryRegistry.js.map +1 -0
  79. package/dist/elements/renderTemporalAudio.js +8 -6
  80. package/dist/elements/renderTemporalAudio.js.map +1 -1
  81. package/dist/elements/setupTemporalHierarchy.js +62 -0
  82. package/dist/elements/setupTemporalHierarchy.js.map +1 -0
  83. package/dist/elements/updateAnimations.js +62 -87
  84. package/dist/elements/updateAnimations.js.map +1 -1
  85. package/dist/getRenderInfo.d.ts +3 -2
  86. package/dist/getRenderInfo.js +20 -4
  87. package/dist/getRenderInfo.js.map +1 -1
  88. package/dist/gui/ContextMixin.js +68 -12
  89. package/dist/gui/ContextMixin.js.map +1 -1
  90. package/dist/gui/Controllable.js +1 -1
  91. package/dist/gui/Controllable.js.map +1 -1
  92. package/dist/gui/EFActiveRootTemporal.d.ts +4 -4
  93. package/dist/gui/EFActiveRootTemporal.js.map +1 -1
  94. package/dist/gui/EFControls.d.ts +2 -2
  95. package/dist/gui/EFControls.js +2 -2
  96. package/dist/gui/EFControls.js.map +1 -1
  97. package/dist/gui/EFDial.d.ts +4 -4
  98. package/dist/gui/EFDial.js +12 -9
  99. package/dist/gui/EFDial.js.map +1 -1
  100. package/dist/gui/EFFilmstrip.d.ts +2 -0
  101. package/dist/gui/EFFilmstrip.js +18 -10
  102. package/dist/gui/EFFilmstrip.js.map +1 -1
  103. package/dist/gui/EFFitScale.d.ts +28 -4
  104. package/dist/gui/EFFitScale.js +88 -26
  105. package/dist/gui/EFFitScale.js.map +1 -1
  106. package/dist/gui/EFFocusOverlay.d.ts +4 -4
  107. package/dist/gui/EFFocusOverlay.js +3 -3
  108. package/dist/gui/EFFocusOverlay.js.map +1 -1
  109. package/dist/gui/EFOverlayItem.d.ts +4 -4
  110. package/dist/gui/EFOverlayLayer.d.ts +4 -4
  111. package/dist/gui/EFPause.d.ts +4 -4
  112. package/dist/gui/EFPause.js +1 -1
  113. package/dist/gui/EFPlay.d.ts +4 -4
  114. package/dist/gui/EFPlay.js +1 -1
  115. package/dist/gui/EFPreview.js +1 -1
  116. package/dist/gui/EFResizableBox.d.ts +4 -4
  117. package/dist/gui/EFResizableBox.js +5 -5
  118. package/dist/gui/EFResizableBox.js.map +1 -1
  119. package/dist/gui/EFScrubber.d.ts +4 -4
  120. package/dist/gui/EFScrubber.js +8 -13
  121. package/dist/gui/EFScrubber.js.map +1 -1
  122. package/dist/gui/EFTimeDisplay.d.ts +8 -4
  123. package/dist/gui/EFTimeDisplay.js +25 -7
  124. package/dist/gui/EFTimeDisplay.js.map +1 -1
  125. package/dist/gui/EFTimelineRuler.d.ts +4 -4
  126. package/dist/gui/EFTimelineRuler.js +3 -3
  127. package/dist/gui/EFTimelineRuler.js.map +1 -1
  128. package/dist/gui/EFToggleLoop.d.ts +4 -4
  129. package/dist/gui/EFToggleLoop.js +1 -1
  130. package/dist/gui/EFTogglePlay.d.ts +4 -4
  131. package/dist/gui/EFTogglePlay.js +1 -1
  132. package/dist/gui/EFTransformHandles.d.ts +4 -4
  133. package/dist/gui/EFTransformHandles.js +6 -6
  134. package/dist/gui/EFTransformHandles.js.map +1 -1
  135. package/dist/gui/EFWorkbench.d.ts +40 -36
  136. package/dist/gui/EFWorkbench.js +436 -822
  137. package/dist/gui/EFWorkbench.js.map +1 -1
  138. package/dist/gui/FitScaleHelpers.js.map +1 -1
  139. package/dist/gui/PlaybackController.d.ts +3 -8
  140. package/dist/gui/PlaybackController.js +59 -56
  141. package/dist/gui/PlaybackController.js.map +1 -1
  142. package/dist/gui/TWMixin.js +1 -1
  143. package/dist/gui/TWMixin.js.map +1 -1
  144. package/dist/gui/TargetOrContextMixin.js +43 -6
  145. package/dist/gui/TargetOrContextMixin.js.map +1 -1
  146. package/dist/gui/ef-theme.css +136 -0
  147. package/dist/gui/hierarchy/EFHierarchy.d.ts +2 -2
  148. package/dist/gui/hierarchy/EFHierarchy.js +14 -24
  149. package/dist/gui/hierarchy/EFHierarchy.js.map +1 -1
  150. package/dist/gui/hierarchy/EFHierarchyItem.d.ts +3 -3
  151. package/dist/gui/hierarchy/EFHierarchyItem.js +22 -10
  152. package/dist/gui/hierarchy/EFHierarchyItem.js.map +1 -1
  153. package/dist/gui/icons.js.map +1 -1
  154. package/dist/gui/previewSettingsContext.d.ts +18 -0
  155. package/dist/gui/previewSettingsContext.js.map +1 -1
  156. package/dist/gui/theme.js +34 -0
  157. package/dist/gui/theme.js.map +1 -0
  158. package/dist/gui/timeline/EFTimeline.d.ts +2 -2
  159. package/dist/gui/timeline/EFTimeline.js +70 -52
  160. package/dist/gui/timeline/EFTimeline.js.map +1 -1
  161. package/dist/gui/timeline/EFTimelineRow.d.ts +3 -1
  162. package/dist/gui/timeline/EFTimelineRow.js +55 -32
  163. package/dist/gui/timeline/EFTimelineRow.js.map +1 -1
  164. package/dist/gui/timeline/TrimHandles.d.ts +23 -9
  165. package/dist/gui/timeline/TrimHandles.js +224 -51
  166. package/dist/gui/timeline/TrimHandles.js.map +1 -1
  167. package/dist/gui/timeline/flattenHierarchy.js.map +1 -1
  168. package/dist/gui/timeline/timelineEditingContext.d.ts +34 -0
  169. package/dist/gui/timeline/timelineEditingContext.js +24 -0
  170. package/dist/gui/timeline/timelineEditingContext.js.map +1 -0
  171. package/dist/gui/timeline/timelineStateContext.js.map +1 -1
  172. package/dist/gui/timeline/tracks/AudioTrack.js +1 -1
  173. package/dist/gui/timeline/tracks/AudioTrack.js.map +1 -1
  174. package/dist/gui/timeline/tracks/CaptionsTrack.d.ts +2 -3
  175. package/dist/gui/timeline/tracks/CaptionsTrack.js +17 -75
  176. package/dist/gui/timeline/tracks/CaptionsTrack.js.map +1 -1
  177. package/dist/gui/timeline/tracks/EFThumbnailStrip.d.ts +52 -0
  178. package/dist/gui/timeline/tracks/EFThumbnailStrip.js +596 -0
  179. package/dist/gui/timeline/tracks/EFThumbnailStrip.js.map +1 -0
  180. package/dist/gui/timeline/tracks/HTMLTrack.js.map +1 -1
  181. package/dist/gui/timeline/tracks/ImageTrack.js.map +1 -1
  182. package/dist/gui/timeline/tracks/TextTrack.d.ts +3 -2
  183. package/dist/gui/timeline/tracks/TextTrack.js +17 -43
  184. package/dist/gui/timeline/tracks/TextTrack.js.map +1 -1
  185. package/dist/gui/timeline/tracks/TimegroupTrack.d.ts +5 -6
  186. package/dist/gui/timeline/tracks/TimegroupTrack.js +33 -23
  187. package/dist/gui/timeline/tracks/TimegroupTrack.js.map +1 -1
  188. package/dist/gui/timeline/tracks/TrackItem.d.ts +7 -9
  189. package/dist/gui/timeline/tracks/TrackItem.js +18 -17
  190. package/dist/gui/timeline/tracks/TrackItem.js.map +1 -1
  191. package/dist/gui/timeline/tracks/VideoTrack.d.ts +3 -3
  192. package/dist/gui/timeline/tracks/VideoTrack.js +11 -14
  193. package/dist/gui/timeline/tracks/VideoTrack.js.map +1 -1
  194. package/dist/gui/timeline/tracks/WaveformTrack.js.map +1 -1
  195. package/dist/gui/timeline/tracks/renderTrackChildren.js.map +1 -1
  196. package/dist/gui/timeline/tracks/waveformUtils.js +1 -1
  197. package/dist/gui/timeline/tracks/waveformUtils.js.map +1 -1
  198. package/dist/gui/tree/EFTree.d.ts +4 -4
  199. package/dist/gui/tree/EFTree.js +8 -14
  200. package/dist/gui/tree/EFTree.js.map +1 -1
  201. package/dist/gui/tree/EFTreeItem.d.ts +4 -4
  202. package/dist/gui/tree/EFTreeItem.js +3 -3
  203. package/dist/gui/tree/EFTreeItem.js.map +1 -1
  204. package/dist/gui/tree/treeContext.js.map +1 -1
  205. package/dist/index.d.ts +10 -8
  206. package/dist/index.js +6 -5
  207. package/dist/index.js.map +1 -1
  208. package/dist/node.d.ts +2 -2
  209. package/dist/node.js +2 -2
  210. package/dist/preview/AdaptiveResolutionTracker.js +3 -3
  211. package/dist/preview/AdaptiveResolutionTracker.js.map +1 -1
  212. package/dist/preview/FrameController.d.ts +2 -17
  213. package/dist/preview/FrameController.js +40 -63
  214. package/dist/preview/FrameController.js.map +1 -1
  215. package/dist/preview/QualityUpgradeScheduler.d.ts +76 -0
  216. package/dist/preview/QualityUpgradeScheduler.js +158 -0
  217. package/dist/preview/QualityUpgradeScheduler.js.map +1 -0
  218. package/dist/preview/RenderContext.d.ts +119 -1
  219. package/dist/preview/RenderContext.js +21 -3
  220. package/dist/preview/RenderContext.js.map +1 -1
  221. package/dist/preview/RenderProfiler.js.map +1 -1
  222. package/dist/preview/RenderStats.js +85 -0
  223. package/dist/preview/RenderStats.js.map +1 -0
  224. package/dist/preview/encoding/canvasEncoder.js +2 -52
  225. package/dist/preview/encoding/canvasEncoder.js.map +1 -1
  226. package/dist/preview/encoding/mainThreadEncoder.js.map +1 -1
  227. package/dist/preview/encoding/workerEncoder.js.map +1 -1
  228. package/dist/preview/logger.js.map +1 -1
  229. package/dist/preview/previewSettings.d.ts +34 -0
  230. package/dist/preview/previewSettings.js +29 -17
  231. package/dist/preview/previewSettings.js.map +1 -1
  232. package/dist/preview/previewTypes.js +4 -4
  233. package/dist/preview/previewTypes.js.map +1 -1
  234. package/dist/preview/renderElementToCanvas.d.ts +44 -0
  235. package/dist/preview/renderElementToCanvas.js +72 -0
  236. package/dist/preview/renderElementToCanvas.js.map +1 -0
  237. package/dist/preview/renderTimegroupToCanvas.js +267 -145
  238. package/dist/preview/renderTimegroupToCanvas.js.map +1 -1
  239. package/dist/preview/renderTimegroupToCanvas.types.d.ts +30 -0
  240. package/dist/preview/renderTimegroupToVideo.js +85 -105
  241. package/dist/preview/renderTimegroupToVideo.js.map +1 -1
  242. package/dist/preview/{renderTimegroupToVideo.d.ts → renderTimegroupToVideo.types.d.ts} +9 -9
  243. package/dist/preview/renderVideoToVideo.js +286 -0
  244. package/dist/preview/renderVideoToVideo.js.map +1 -0
  245. package/dist/preview/renderers.js.map +1 -1
  246. package/dist/preview/rendering/ScaleConfig.js +74 -0
  247. package/dist/preview/rendering/ScaleConfig.js.map +1 -0
  248. package/dist/preview/rendering/inlineImages.js +1 -44
  249. package/dist/preview/rendering/inlineImages.js.map +1 -1
  250. package/dist/preview/rendering/loadImage.js +22 -0
  251. package/dist/preview/rendering/loadImage.js.map +1 -0
  252. package/dist/preview/rendering/renderToImageNative.js +3 -3
  253. package/dist/preview/rendering/renderToImageNative.js.map +1 -1
  254. package/dist/preview/rendering/serializeTimelineDirect.js +224 -68
  255. package/dist/preview/rendering/serializeTimelineDirect.js.map +1 -1
  256. package/dist/preview/statsTrackingStrategy.js +1 -101
  257. package/dist/preview/statsTrackingStrategy.js.map +1 -1
  258. package/dist/preview/workers/WorkerPool.js +0 -1
  259. package/dist/preview/workers/WorkerPool.js.map +1 -1
  260. package/dist/preview/workers/encoderWorkerInline.js +21 -54
  261. package/dist/preview/workers/encoderWorkerInline.js.map +1 -1
  262. package/dist/render/EFRenderAPI.d.ts +2 -1
  263. package/dist/render/EFRenderAPI.js +12 -36
  264. package/dist/render/EFRenderAPI.js.map +1 -1
  265. package/dist/render/getRenderData.js +4 -4
  266. package/dist/render/getRenderData.js.map +1 -1
  267. package/dist/style.css +114 -163
  268. package/dist/transcoding/cache/RequestDeduplicator.js +1 -0
  269. package/dist/transcoding/cache/RequestDeduplicator.js.map +1 -1
  270. package/dist/transcoding/types/index.d.ts +1 -1
  271. package/dist/transcoding/utils/UrlGenerator.js +10 -3
  272. package/dist/transcoding/utils/UrlGenerator.js.map +1 -1
  273. package/dist/utils/LRUCache.js +1 -0
  274. package/dist/utils/LRUCache.js.map +1 -1
  275. package/dist/utils/frameTime.js +23 -1
  276. package/dist/utils/frameTime.js.map +1 -1
  277. package/package.json +21 -8
  278. package/scripts/build-css.js +8 -1
  279. package/test/setup.ts +0 -1
  280. package/test/useAssetMSW.ts +50 -0
  281. package/test/visualRegressionUtils.ts +23 -9
  282. package/dist/_virtual/rolldown_runtime.js +0 -27
  283. package/dist/elements/EFMedia/AssetIdMediaEngine.js.map +0 -1
  284. package/dist/elements/EFThumbnailStrip.d.ts +0 -167
  285. package/dist/elements/EFThumbnailStrip.js +0 -731
  286. package/dist/elements/EFThumbnailStrip.js.map +0 -1
  287. package/dist/elements/SessionThumbnailCache.js +0 -154
  288. package/dist/elements/SessionThumbnailCache.js.map +0 -1
  289. package/dist/node_modules/react/cjs/react-jsx-runtime.development.js +0 -688
  290. package/dist/node_modules/react/cjs/react-jsx-runtime.development.js.map +0 -1
  291. package/dist/node_modules/react/cjs/react.development.js +0 -1521
  292. package/dist/node_modules/react/cjs/react.development.js.map +0 -1
  293. package/dist/node_modules/react/index.js +0 -13
  294. package/dist/node_modules/react/index.js.map +0 -1
  295. package/dist/node_modules/react/jsx-runtime.js +0 -13
  296. package/dist/node_modules/react/jsx-runtime.js.map +0 -1
  297. package/dist/preview/encoding/types.d.ts +0 -1
  298. package/dist/preview/renderTimegroupPreview.js +0 -686
  299. package/dist/preview/renderTimegroupPreview.js.map +0 -1
  300. package/dist/preview/renderTimegroupToCanvas.d.ts +0 -42
  301. package/dist/preview/rendering/renderToImage.d.ts +0 -2
  302. package/dist/preview/rendering/renderToImage.js +0 -95
  303. package/dist/preview/rendering/renderToImage.js.map +0 -1
  304. package/dist/preview/rendering/renderToImageForeignObject.js +0 -163
  305. package/dist/preview/rendering/renderToImageForeignObject.js.map +0 -1
  306. package/dist/preview/rendering/renderToImageNative.d.ts +0 -1
  307. package/dist/preview/rendering/svgSerializer.js +0 -43
  308. package/dist/preview/rendering/svgSerializer.js.map +0 -1
  309. package/dist/preview/rendering/types.d.ts +0 -2
  310. package/dist/preview/thumbnailCacheSettings.js +0 -52
  311. package/dist/preview/thumbnailCacheSettings.js.map +0 -1
  312. package/dist/sandbox/PlaybackControls.d.ts +0 -1
  313. package/dist/sandbox/PlaybackControls.js +0 -10
  314. package/dist/sandbox/PlaybackControls.js.map +0 -1
  315. package/dist/sandbox/ScenarioRunner.d.ts +0 -1
  316. package/dist/sandbox/ScenarioRunner.js +0 -1
  317. package/dist/sandbox/defineSandbox.d.ts +0 -1
  318. package/dist/sandbox/index.d.ts +0 -3
  319. package/dist/sandbox/index.js +0 -2
  320. package/test/EFVideo.framegen.browsertest.ts +0 -80
  321. package/test/thumbnail-performance-test.html +0 -116
@@ -1,5 +1,7 @@
1
- import { __decorate } from "../../_virtual/_@oxc-project_runtime@0.95.0/helpers/decorate.js";
2
1
  import { TWMixin } from "../TWMixin2.js";
2
+ import { __decorate } from "../../_virtual/_@oxc-project_runtime@0.95.0/helpers/decorate.js";
3
+ import { timelineEditingContext } from "./timelineEditingContext.js";
4
+ import { consume } from "@lit/context";
3
5
  import { LitElement, css, html, nothing } from "lit";
4
6
  import { customElement, property, state } from "lit/decorators.js";
5
7
  import { styleMap } from "lit/directives/style-map.js";
@@ -8,36 +10,48 @@ import { styleMap } from "lit/directives/style-map.js";
8
10
  let EFTrimHandles = class EFTrimHandles$1 extends TWMixin(LitElement) {
9
11
  constructor(..._args) {
10
12
  super(..._args);
13
+ this.mode = "standalone";
11
14
  this.elementId = "";
12
- this.pixelsPerMs = .04;
13
- this.trimStartMs = 0;
14
- this.trimEndMs = 0;
15
+ this.pixelsPerMs = null;
16
+ this.value = {
17
+ startMs: 0,
18
+ endMs: 0
19
+ };
15
20
  this.intrinsicDurationMs = 0;
16
21
  this.showOverlays = true;
22
+ this.seekTarget = "";
17
23
  this.draggingHandle = null;
18
24
  this.dragStartX = 0;
19
25
  this.dragStartValue = 0;
20
26
  this.handlePointerMove = (e) => {
21
27
  if (!this.draggingHandle) return;
22
- const deltaMs = (e.clientX - this.dragStartX) / this.pixelsPerMs;
28
+ const pxPerMs = this.#effectivePixelsPerMs;
29
+ const deltaMs = (e.clientX - this.dragStartX) / pxPerMs;
30
+ if (this.draggingHandle === "region") {
31
+ const clampedDelta = Math.max(-this.#regionDragStartTrimStart, Math.min(this.#regionDragStartTrimEnd, deltaMs));
32
+ const newValue = {
33
+ startMs: this.#regionDragStartTrimStart + clampedDelta,
34
+ endMs: this.#regionDragStartTrimEnd - clampedDelta
35
+ };
36
+ this.#emitChange("region", newValue);
37
+ return;
38
+ }
23
39
  let newValueMs;
24
40
  if (this.draggingHandle === "start") {
25
41
  newValueMs = Math.max(0, this.dragStartValue + deltaMs);
26
42
  newValueMs = Math.min(newValueMs, this.intrinsicDurationMs - (this.trimEndMs || 0));
43
+ this.#emitChange("start", {
44
+ startMs: newValueMs,
45
+ endMs: this.trimEndMs
46
+ });
27
47
  } else {
28
48
  newValueMs = Math.max(0, this.dragStartValue - deltaMs);
29
49
  newValueMs = Math.min(newValueMs, this.intrinsicDurationMs - this.trimStartMs);
50
+ this.#emitChange("end", {
51
+ startMs: this.trimStartMs,
52
+ endMs: newValueMs
53
+ });
30
54
  }
31
- this.dispatchEvent(new CustomEvent("trim-change", {
32
- detail: {
33
- elementId: this.elementId,
34
- type: this.draggingHandle,
35
- deltaMs,
36
- newValueMs
37
- },
38
- bubbles: true,
39
- composed: true
40
- }));
41
55
  };
42
56
  this.handlePointerUp = (e) => {
43
57
  const target = e.currentTarget;
@@ -54,6 +68,7 @@ let EFTrimHandles = class EFTrimHandles$1 extends TWMixin(LitElement) {
54
68
  composed: true
55
69
  }));
56
70
  this.draggingHandle = null;
71
+ if (this.editingContext) this.editingContext.setState({ mode: "idle" });
57
72
  };
58
73
  }
59
74
  static {
@@ -72,45 +87,44 @@ let EFTrimHandles = class EFTrimHandles$1 extends TWMixin(LitElement) {
72
87
  position: absolute;
73
88
  top: 0;
74
89
  bottom: 0;
75
- width: 8px;
90
+ width: var(--trim-handle-width, 8px);
76
91
  cursor: ew-resize;
77
92
  pointer-events: auto;
78
93
  z-index: 10;
94
+ display: flex;
95
+ align-items: center;
96
+ justify-content: center;
79
97
  }
80
98
 
81
- .handle::before {
82
- content: "";
83
- position: absolute;
84
- top: 50%;
85
- transform: translateY(-50%);
86
- width: 4px;
87
- height: 60%;
88
- min-height: 12px;
89
- max-height: 24px;
99
+ .handle-inner {
100
+ width: 100%;
101
+ height: 100%;
102
+ display: flex;
103
+ align-items: center;
104
+ justify-content: center;
90
105
  background: var(--trim-handle-color, rgba(255, 255, 255, 0.7));
91
- border-radius: 2px;
92
106
  transition: background 0.15s ease;
93
107
  }
94
108
 
95
- .handle:hover::before,
96
- .handle.dragging::before {
109
+ .handle:hover .handle-inner,
110
+ .handle.dragging .handle-inner {
97
111
  background: var(--trim-handle-active-color, #3b82f6);
98
112
  }
99
113
 
100
- .handle-start {
101
- left: -4px;
114
+ .handle-start .handle-inner {
115
+ border-radius: var(--trim-handle-border-radius-start, 2px 0 0 2px);
102
116
  }
103
117
 
104
- .handle-start::before {
105
- left: 2px;
118
+ .handle-end .handle-inner {
119
+ border-radius: var(--trim-handle-border-radius-end, 0 2px 2px 0);
106
120
  }
107
121
 
108
- .handle-end {
109
- right: -4px;
122
+ /* Track mode: handles pinned at container edges */
123
+ :host([mode="track"]) .handle-start {
124
+ left: -4px;
110
125
  }
111
-
112
- .handle-end::before {
113
- right: 2px;
126
+ :host([mode="track"]) .handle-end {
127
+ right: -4px;
114
128
  }
115
129
 
116
130
  .handle.dragging {
@@ -132,14 +146,132 @@ let EFTrimHandles = class EFTrimHandles$1 extends TWMixin(LitElement) {
132
146
  .trim-overlay-end {
133
147
  right: 0;
134
148
  }
149
+
150
+ .region {
151
+ position: absolute;
152
+ top: 0;
153
+ bottom: 0;
154
+ cursor: grab;
155
+ pointer-events: auto;
156
+ z-index: 5;
157
+ }
158
+
159
+ .region.dragging {
160
+ cursor: grabbing;
161
+ }
162
+
163
+ .selected-border {
164
+ position: absolute;
165
+ left: 0;
166
+ right: 0;
167
+ height: var(--trim-selected-border-width, 0px);
168
+ background: var(--trim-selected-border-color, transparent);
169
+ pointer-events: none;
170
+ z-index: 15;
171
+ }
172
+
173
+ .selected-border-top {
174
+ top: 0;
175
+ }
176
+
177
+ .selected-border-bottom {
178
+ bottom: 0;
179
+ }
135
180
  `];
136
181
  }
182
+ get trimStartMs() {
183
+ return this.value.startMs;
184
+ }
185
+ set trimStartMs(v) {
186
+ this.value = {
187
+ ...this.value,
188
+ startMs: v
189
+ };
190
+ }
191
+ get trimEndMs() {
192
+ return this.value.endMs;
193
+ }
194
+ set trimEndMs(v) {
195
+ this.value = {
196
+ ...this.value,
197
+ endMs: v
198
+ };
199
+ }
200
+ #regionDragStartTrimStart = 0;
201
+ #regionDragStartTrimEnd = 0;
202
+ #resizeObserver = null;
203
+ #hostWidth = 0;
204
+ #emitChange(type, value) {
205
+ this.dispatchEvent(new CustomEvent("trim-change", {
206
+ detail: {
207
+ elementId: this.elementId,
208
+ type,
209
+ value
210
+ },
211
+ bubbles: true,
212
+ composed: true
213
+ }));
214
+ this.#seekToTarget(type, value);
215
+ }
216
+ #seekToTarget(type, value) {
217
+ if (!this.seekTarget) return;
218
+ const target = this.getRootNode().getElementById(this.seekTarget);
219
+ if (!target || !("currentTimeMs" in target)) return;
220
+ if (type === "end") target.currentTimeMs = this.intrinsicDurationMs - value.startMs - value.endMs;
221
+ else target.currentTimeMs = 0;
222
+ }
223
+ get #effectivePixelsPerMs() {
224
+ if (this.pixelsPerMs != null) return this.pixelsPerMs;
225
+ if (this.#hostWidth > 0 && this.intrinsicDurationMs > 0) return this.#hostWidth / this.intrinsicDurationMs;
226
+ return .04;
227
+ }
228
+ connectedCallback() {
229
+ super.connectedCallback();
230
+ this.#resizeObserver = new ResizeObserver((entries) => {
231
+ const entry = entries[0];
232
+ if (!entry) return;
233
+ const width = entry.contentRect.width;
234
+ if (width !== this.#hostWidth) {
235
+ this.#hostWidth = width;
236
+ this.requestUpdate();
237
+ }
238
+ });
239
+ this.#resizeObserver.observe(this);
240
+ }
241
+ disconnectedCallback() {
242
+ super.disconnectedCallback();
243
+ this.#resizeObserver?.disconnect();
244
+ this.#resizeObserver = null;
245
+ }
137
246
  handlePointerDown(e, type) {
138
247
  e.preventDefault();
139
248
  e.stopPropagation();
140
249
  this.draggingHandle = type;
141
250
  this.dragStartX = e.clientX;
142
251
  this.dragStartValue = type === "start" ? this.trimStartMs : this.trimEndMs;
252
+ if (this.editingContext) this.editingContext.setState({
253
+ mode: "trimming",
254
+ elementId: this.elementId,
255
+ handle: type
256
+ });
257
+ const target = e.currentTarget;
258
+ target.setPointerCapture(e.pointerId);
259
+ target.addEventListener("pointermove", this.handlePointerMove);
260
+ target.addEventListener("pointerup", this.handlePointerUp);
261
+ target.addEventListener("pointercancel", this.handlePointerUp);
262
+ }
263
+ handleRegionPointerDown(e) {
264
+ e.preventDefault();
265
+ e.stopPropagation();
266
+ this.draggingHandle = "region";
267
+ this.dragStartX = e.clientX;
268
+ this.#regionDragStartTrimStart = this.trimStartMs;
269
+ this.#regionDragStartTrimEnd = this.trimEndMs;
270
+ if (this.editingContext) this.editingContext.setState({
271
+ mode: "trimming",
272
+ elementId: this.elementId,
273
+ handle: "start"
274
+ });
143
275
  const target = e.currentTarget;
144
276
  target.setPointerCapture(e.pointerId);
145
277
  target.addEventListener("pointermove", this.handlePointerMove);
@@ -147,29 +279,69 @@ let EFTrimHandles = class EFTrimHandles$1 extends TWMixin(LitElement) {
147
279
  target.addEventListener("pointercancel", this.handlePointerUp);
148
280
  }
149
281
  render() {
150
- const trimStartWidth = this.trimStartMs * this.pixelsPerMs;
151
- const trimEndWidth = (this.trimEndMs || 0) * this.pixelsPerMs;
282
+ const pxPerMs = this.#effectivePixelsPerMs;
283
+ const trimStartPx = this.trimStartMs * pxPerMs;
284
+ const trimEndPx = (this.trimEndMs || 0) * pxPerMs;
285
+ const isStandalone = this.mode === "standalone";
286
+ const handleWidthPx = parseFloat(getComputedStyle(this).getPropertyValue("--trim-handle-width")) || 8;
152
287
  return html`
153
288
  ${this.showOverlays && this.trimStartMs > 0 ? html`<div
154
289
  class="trim-overlay trim-overlay-start"
155
- style=${styleMap({ width: `${trimStartWidth}px` })}
290
+ style=${styleMap({ width: `${trimStartPx}px` })}
156
291
  ></div>` : nothing}
157
292
  ${this.showOverlays && this.trimEndMs > 0 ? html`<div
158
293
  class="trim-overlay trim-overlay-end"
159
- style=${styleMap({ width: `${trimEndWidth}px` })}
294
+ style=${styleMap({ width: `${trimEndPx}px` })}
160
295
  ></div>` : nothing}
161
296
 
297
+ ${isStandalone ? html`
298
+ <div
299
+ class="region ${this.draggingHandle === "region" ? "dragging" : ""}"
300
+ style=${styleMap({
301
+ left: `${trimStartPx + handleWidthPx}px`,
302
+ right: `${trimEndPx + handleWidthPx}px`
303
+ })}
304
+ @pointerdown=${(e) => this.handleRegionPointerDown(e)}
305
+ ></div>
306
+ <div class="selected-border selected-border-top"
307
+ style=${styleMap({
308
+ left: `${trimStartPx}px`,
309
+ right: `${trimEndPx}px`
310
+ })}
311
+ ></div>
312
+ <div class="selected-border selected-border-bottom"
313
+ style=${styleMap({
314
+ left: `${trimStartPx}px`,
315
+ right: `${trimEndPx}px`
316
+ })}
317
+ ></div>
318
+ ` : nothing}
319
+
162
320
  <div
163
321
  class="handle handle-start ${this.draggingHandle === "start" ? "dragging" : ""}"
322
+ style=${isStandalone ? styleMap({ left: `${trimStartPx}px` }) : nothing}
164
323
  @pointerdown=${(e) => this.handlePointerDown(e, "start")}
165
- ></div>
324
+ >
325
+ <div class="handle-inner">
326
+ <slot name="handle-start"></slot>
327
+ </div>
328
+ </div>
166
329
  <div
167
330
  class="handle handle-end ${this.draggingHandle === "end" ? "dragging" : ""}"
331
+ style=${isStandalone ? styleMap({ right: `${trimEndPx}px` }) : nothing}
168
332
  @pointerdown=${(e) => this.handlePointerDown(e, "end")}
169
- ></div>
333
+ >
334
+ <div class="handle-inner">
335
+ <slot name="handle-end"></slot>
336
+ </div>
337
+ </div>
170
338
  `;
171
339
  }
172
340
  };
341
+ __decorate([property({
342
+ type: String,
343
+ reflect: true
344
+ })], EFTrimHandles.prototype, "mode", void 0);
173
345
  __decorate([property({
174
346
  type: String,
175
347
  attribute: "element-id"
@@ -178,14 +350,7 @@ __decorate([property({
178
350
  type: Number,
179
351
  attribute: "pixels-per-ms"
180
352
  })], EFTrimHandles.prototype, "pixelsPerMs", void 0);
181
- __decorate([property({
182
- type: Number,
183
- attribute: "trim-start-ms"
184
- })], EFTrimHandles.prototype, "trimStartMs", void 0);
185
- __decorate([property({
186
- type: Number,
187
- attribute: "trim-end-ms"
188
- })], EFTrimHandles.prototype, "trimEndMs", void 0);
353
+ __decorate([property({ attribute: false })], EFTrimHandles.prototype, "value", void 0);
189
354
  __decorate([property({
190
355
  type: Number,
191
356
  attribute: "intrinsic-duration-ms"
@@ -194,6 +359,14 @@ __decorate([property({
194
359
  type: Boolean,
195
360
  attribute: "show-overlays"
196
361
  })], EFTrimHandles.prototype, "showOverlays", void 0);
362
+ __decorate([property({
363
+ type: String,
364
+ attribute: "seek-target"
365
+ })], EFTrimHandles.prototype, "seekTarget", void 0);
366
+ __decorate([consume({
367
+ context: timelineEditingContext,
368
+ subscribe: true
369
+ })], EFTrimHandles.prototype, "editingContext", void 0);
197
370
  __decorate([state()], EFTrimHandles.prototype, "draggingHandle", void 0);
198
371
  __decorate([state()], EFTrimHandles.prototype, "dragStartX", void 0);
199
372
  __decorate([state()], EFTrimHandles.prototype, "dragStartValue", void 0);
@@ -1 +1 @@
1
- {"version":3,"file":"TrimHandles.js","names":["EFTrimHandles","newValueMs: number"],"sources":["../../../src/gui/timeline/TrimHandles.ts"],"sourcesContent":["import { css, html, LitElement, nothing } from \"lit\";\nimport { customElement, property, state } from \"lit/decorators.js\";\nimport { styleMap } from \"lit/directives/style-map.js\";\nimport { TWMixin } from \"../TWMixin.js\";\n\nexport interface TrimChangeDetail {\n elementId: string;\n type: \"start\" | \"end\";\n deltaMs: number;\n newValueMs: number;\n}\n\n@customElement(\"ef-trim-handles\")\nexport class EFTrimHandles extends TWMixin(LitElement) {\n static styles = [\n css`\n :host {\n display: block;\n position: absolute;\n top: 0;\n left: 0;\n right: 0;\n bottom: 0;\n pointer-events: none;\n }\n\n .handle {\n position: absolute;\n top: 0;\n bottom: 0;\n width: 8px;\n cursor: ew-resize;\n pointer-events: auto;\n z-index: 10;\n }\n\n .handle::before {\n content: \"\";\n position: absolute;\n top: 50%;\n transform: translateY(-50%);\n width: 4px;\n height: 60%;\n min-height: 12px;\n max-height: 24px;\n background: var(--trim-handle-color, rgba(255, 255, 255, 0.7));\n border-radius: 2px;\n transition: background 0.15s ease;\n }\n\n .handle:hover::before,\n .handle.dragging::before {\n background: var(--trim-handle-active-color, #3b82f6);\n }\n\n .handle-start {\n left: -4px;\n }\n\n .handle-start::before {\n left: 2px;\n }\n\n .handle-end {\n right: -4px;\n }\n\n .handle-end::before {\n right: 2px;\n }\n\n .handle.dragging {\n cursor: grabbing;\n }\n\n .trim-overlay {\n position: absolute;\n top: 0;\n bottom: 0;\n background: var(--trim-overlay-color, rgba(0, 0, 0, 0.4));\n pointer-events: none;\n }\n\n .trim-overlay-start {\n left: 0;\n }\n\n .trim-overlay-end {\n right: 0;\n }\n `,\n ];\n\n @property({ type: String, attribute: \"element-id\" })\n elementId = \"\";\n\n @property({ type: Number, attribute: \"pixels-per-ms\" })\n pixelsPerMs = 0.04;\n\n @property({ type: Number, attribute: \"trim-start-ms\" })\n trimStartMs = 0;\n\n @property({ type: Number, attribute: \"trim-end-ms\" })\n trimEndMs = 0;\n\n @property({ type: Number, attribute: \"intrinsic-duration-ms\" })\n intrinsicDurationMs = 0;\n\n @property({ type: Boolean, attribute: \"show-overlays\" })\n showOverlays = true;\n\n @state()\n private draggingHandle: \"start\" | \"end\" | null = null;\n\n @state()\n private dragStartX = 0;\n\n @state()\n private dragStartValue = 0;\n\n private handlePointerDown(e: PointerEvent, type: \"start\" | \"end\"): void {\n e.preventDefault();\n e.stopPropagation();\n\n this.draggingHandle = type;\n this.dragStartX = e.clientX;\n this.dragStartValue = type === \"start\" ? this.trimStartMs : this.trimEndMs;\n\n const target = e.currentTarget as HTMLElement;\n target.setPointerCapture(e.pointerId);\n\n target.addEventListener(\"pointermove\", this.handlePointerMove);\n target.addEventListener(\"pointerup\", this.handlePointerUp);\n target.addEventListener(\"pointercancel\", this.handlePointerUp);\n }\n\n private handlePointerMove = (e: PointerEvent): void => {\n if (!this.draggingHandle) return;\n\n const deltaX = e.clientX - this.dragStartX;\n const deltaMs = deltaX / this.pixelsPerMs;\n\n let newValueMs: number;\n\n if (this.draggingHandle === \"start\") {\n newValueMs = Math.max(0, this.dragStartValue + deltaMs);\n newValueMs = Math.min(\n newValueMs,\n this.intrinsicDurationMs - (this.trimEndMs || 0),\n );\n } else {\n newValueMs = Math.max(0, this.dragStartValue - deltaMs);\n newValueMs = Math.min(\n newValueMs,\n this.intrinsicDurationMs - this.trimStartMs,\n );\n }\n\n this.dispatchEvent(\n new CustomEvent<TrimChangeDetail>(\"trim-change\", {\n detail: {\n elementId: this.elementId,\n type: this.draggingHandle,\n deltaMs,\n newValueMs,\n },\n bubbles: true,\n composed: true,\n }),\n );\n };\n\n private handlePointerUp = (e: PointerEvent): void => {\n const target = e.currentTarget as HTMLElement;\n target.releasePointerCapture(e.pointerId);\n target.removeEventListener(\"pointermove\", this.handlePointerMove);\n target.removeEventListener(\"pointerup\", this.handlePointerUp);\n target.removeEventListener(\"pointercancel\", this.handlePointerUp);\n\n if (this.draggingHandle) {\n this.dispatchEvent(\n new CustomEvent(\"trim-change-end\", {\n detail: {\n elementId: this.elementId,\n type: this.draggingHandle,\n },\n bubbles: true,\n composed: true,\n }),\n );\n }\n\n this.draggingHandle = null;\n };\n\n render() {\n const trimStartWidth = this.trimStartMs * this.pixelsPerMs;\n const trimEndWidth = (this.trimEndMs || 0) * this.pixelsPerMs;\n\n return html`\n ${\n this.showOverlays && this.trimStartMs > 0\n ? html`<div\n class=\"trim-overlay trim-overlay-start\"\n style=${styleMap({ width: `${trimStartWidth}px` })}\n ></div>`\n : nothing\n }\n ${\n this.showOverlays && this.trimEndMs > 0\n ? html`<div\n class=\"trim-overlay trim-overlay-end\"\n style=${styleMap({ width: `${trimEndWidth}px` })}\n ></div>`\n : nothing\n }\n\n <div\n class=\"handle handle-start ${this.draggingHandle === \"start\" ? \"dragging\" : \"\"}\"\n @pointerdown=${(e: PointerEvent) => this.handlePointerDown(e, \"start\")}\n ></div>\n <div\n class=\"handle handle-end ${this.draggingHandle === \"end\" ? \"dragging\" : \"\"}\"\n @pointerdown=${(e: PointerEvent) => this.handlePointerDown(e, \"end\")}\n ></div>\n `;\n }\n}\n\ndeclare global {\n interface HTMLElementTagNameMap {\n \"ef-trim-handles\": EFTrimHandles;\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"TrimHandles.js","names":["EFTrimHandles","#effectivePixelsPerMs","#regionDragStartTrimStart","#regionDragStartTrimEnd","newValue: TrimValue","#emitChange","newValueMs: number","#seekToTarget","#hostWidth","#resizeObserver"],"sources":["../../../src/gui/timeline/TrimHandles.ts"],"sourcesContent":["import { consume } from \"@lit/context\";\nimport { css, html, LitElement, nothing } from \"lit\";\nimport { customElement, property, state } from \"lit/decorators.js\";\nimport { styleMap } from \"lit/directives/style-map.js\";\nimport { TWMixin } from \"../TWMixin.js\";\nimport {\n timelineEditingContext,\n type TimelineEditingContext,\n} from \"./timelineEditingContext.js\";\n\nexport interface TrimValue {\n startMs: number;\n endMs: number;\n}\n\nexport interface TrimChangeDetail {\n elementId: string;\n type: \"start\" | \"end\" | \"region\";\n value: TrimValue;\n}\n\n@customElement(\"ef-trim-handles\")\nexport class EFTrimHandles extends TWMixin(LitElement) {\n static styles = [\n css`\n :host {\n display: block;\n position: absolute;\n top: 0;\n left: 0;\n right: 0;\n bottom: 0;\n pointer-events: none;\n }\n\n .handle {\n position: absolute;\n top: 0;\n bottom: 0;\n width: var(--trim-handle-width, 8px);\n cursor: ew-resize;\n pointer-events: auto;\n z-index: 10;\n display: flex;\n align-items: center;\n justify-content: center;\n }\n\n .handle-inner {\n width: 100%;\n height: 100%;\n display: flex;\n align-items: center;\n justify-content: center;\n background: var(--trim-handle-color, rgba(255, 255, 255, 0.7));\n transition: background 0.15s ease;\n }\n\n .handle:hover .handle-inner,\n .handle.dragging .handle-inner {\n background: var(--trim-handle-active-color, #3b82f6);\n }\n\n .handle-start .handle-inner {\n border-radius: var(--trim-handle-border-radius-start, 2px 0 0 2px);\n }\n\n .handle-end .handle-inner {\n border-radius: var(--trim-handle-border-radius-end, 0 2px 2px 0);\n }\n\n /* Track mode: handles pinned at container edges */\n :host([mode=\"track\"]) .handle-start {\n left: -4px;\n }\n :host([mode=\"track\"]) .handle-end {\n right: -4px;\n }\n\n .handle.dragging {\n cursor: grabbing;\n }\n\n .trim-overlay {\n position: absolute;\n top: 0;\n bottom: 0;\n background: var(--trim-overlay-color, rgba(0, 0, 0, 0.4));\n pointer-events: none;\n }\n\n .trim-overlay-start {\n left: 0;\n }\n\n .trim-overlay-end {\n right: 0;\n }\n\n .region {\n position: absolute;\n top: 0;\n bottom: 0;\n cursor: grab;\n pointer-events: auto;\n z-index: 5;\n }\n\n .region.dragging {\n cursor: grabbing;\n }\n\n .selected-border {\n position: absolute;\n left: 0;\n right: 0;\n height: var(--trim-selected-border-width, 0px);\n background: var(--trim-selected-border-color, transparent);\n pointer-events: none;\n z-index: 15;\n }\n\n .selected-border-top {\n top: 0;\n }\n\n .selected-border-bottom {\n bottom: 0;\n }\n `,\n ];\n\n @property({ type: String, reflect: true })\n mode: \"standalone\" | \"track\" = \"standalone\";\n\n @property({ type: String, attribute: \"element-id\" })\n elementId = \"\";\n\n @property({ type: Number, attribute: \"pixels-per-ms\" })\n pixelsPerMs: number | null = null;\n\n @property({ attribute: false })\n value: TrimValue = { startMs: 0, endMs: 0 };\n\n @property({ type: Number, attribute: \"intrinsic-duration-ms\" })\n intrinsicDurationMs = 0;\n\n get trimStartMs(): number {\n return this.value.startMs;\n }\n set trimStartMs(v: number) {\n this.value = { ...this.value, startMs: v };\n }\n\n get trimEndMs(): number {\n return this.value.endMs;\n }\n set trimEndMs(v: number) {\n this.value = { ...this.value, endMs: v };\n }\n\n @property({ type: Boolean, attribute: \"show-overlays\" })\n showOverlays = true;\n\n @property({ type: String, attribute: \"seek-target\" })\n seekTarget = \"\";\n\n @consume({ context: timelineEditingContext, subscribe: true })\n editingContext?: TimelineEditingContext;\n\n @state()\n private draggingHandle: \"start\" | \"end\" | \"region\" | null = null;\n\n @state()\n private dragStartX = 0;\n\n @state()\n private dragStartValue = 0;\n\n #regionDragStartTrimStart = 0;\n #regionDragStartTrimEnd = 0;\n #resizeObserver: ResizeObserver | null = null;\n #hostWidth = 0;\n\n #emitChange(type: \"start\" | \"end\" | \"region\", value: TrimValue): void {\n this.dispatchEvent(\n new CustomEvent<TrimChangeDetail>(\"trim-change\", {\n detail: { elementId: this.elementId, type, value },\n bubbles: true,\n composed: true,\n }),\n );\n this.#seekToTarget(type, value);\n }\n\n #seekToTarget(type: \"start\" | \"end\" | \"region\", value: TrimValue): void {\n if (!this.seekTarget) return;\n const target = (this.getRootNode() as Document | ShadowRoot).getElementById(\n this.seekTarget,\n ) as any;\n if (!target || !(\"currentTimeMs\" in target)) return;\n\n if (type === \"end\") {\n target.currentTimeMs =\n this.intrinsicDurationMs - value.startMs - value.endMs;\n } else {\n target.currentTimeMs = 0;\n }\n }\n\n get #effectivePixelsPerMs(): number {\n if (this.pixelsPerMs != null) {\n return this.pixelsPerMs;\n }\n if (this.#hostWidth > 0 && this.intrinsicDurationMs > 0) {\n return this.#hostWidth / this.intrinsicDurationMs;\n }\n return 0.04;\n }\n\n connectedCallback(): void {\n super.connectedCallback();\n this.#resizeObserver = new ResizeObserver((entries) => {\n const entry = entries[0];\n if (!entry) return;\n const width = entry.contentRect.width;\n if (width !== this.#hostWidth) {\n this.#hostWidth = width;\n this.requestUpdate();\n }\n });\n this.#resizeObserver.observe(this);\n }\n\n disconnectedCallback(): void {\n super.disconnectedCallback();\n this.#resizeObserver?.disconnect();\n this.#resizeObserver = null;\n }\n\n private handlePointerDown(e: PointerEvent, type: \"start\" | \"end\"): void {\n e.preventDefault();\n e.stopPropagation();\n\n this.draggingHandle = type;\n this.dragStartX = e.clientX;\n this.dragStartValue = type === \"start\" ? this.trimStartMs : this.trimEndMs;\n\n if (this.editingContext) {\n this.editingContext.setState({\n mode: \"trimming\",\n elementId: this.elementId,\n handle: type,\n });\n }\n\n const target = e.currentTarget as HTMLElement;\n target.setPointerCapture(e.pointerId);\n\n target.addEventListener(\"pointermove\", this.handlePointerMove);\n target.addEventListener(\"pointerup\", this.handlePointerUp);\n target.addEventListener(\"pointercancel\", this.handlePointerUp);\n }\n\n private handleRegionPointerDown(e: PointerEvent): void {\n e.preventDefault();\n e.stopPropagation();\n\n this.draggingHandle = \"region\";\n this.dragStartX = e.clientX;\n this.#regionDragStartTrimStart = this.trimStartMs;\n this.#regionDragStartTrimEnd = this.trimEndMs;\n\n if (this.editingContext) {\n this.editingContext.setState({\n mode: \"trimming\",\n elementId: this.elementId,\n handle: \"start\",\n });\n }\n\n const target = e.currentTarget as HTMLElement;\n target.setPointerCapture(e.pointerId);\n\n target.addEventListener(\"pointermove\", this.handlePointerMove);\n target.addEventListener(\"pointerup\", this.handlePointerUp);\n target.addEventListener(\"pointercancel\", this.handlePointerUp);\n }\n\n private handlePointerMove = (e: PointerEvent): void => {\n if (!this.draggingHandle) return;\n\n const pxPerMs = this.#effectivePixelsPerMs;\n const deltaX = e.clientX - this.dragStartX;\n const deltaMs = deltaX / pxPerMs;\n\n if (this.draggingHandle === \"region\") {\n const clampedDelta = Math.max(\n -this.#regionDragStartTrimStart,\n Math.min(this.#regionDragStartTrimEnd, deltaMs),\n );\n\n const newValue: TrimValue = {\n startMs: this.#regionDragStartTrimStart + clampedDelta,\n endMs: this.#regionDragStartTrimEnd - clampedDelta,\n };\n\n this.#emitChange(\"region\", newValue);\n return;\n }\n\n let newValueMs: number;\n\n if (this.draggingHandle === \"start\") {\n newValueMs = Math.max(0, this.dragStartValue + deltaMs);\n newValueMs = Math.min(\n newValueMs,\n this.intrinsicDurationMs - (this.trimEndMs || 0),\n );\n\n this.#emitChange(\"start\", { startMs: newValueMs, endMs: this.trimEndMs });\n } else {\n newValueMs = Math.max(0, this.dragStartValue - deltaMs);\n newValueMs = Math.min(\n newValueMs,\n this.intrinsicDurationMs - this.trimStartMs,\n );\n\n this.#emitChange(\"end\", { startMs: this.trimStartMs, endMs: newValueMs });\n }\n };\n\n private handlePointerUp = (e: PointerEvent): void => {\n const target = e.currentTarget as HTMLElement;\n target.releasePointerCapture(e.pointerId);\n target.removeEventListener(\"pointermove\", this.handlePointerMove);\n target.removeEventListener(\"pointerup\", this.handlePointerUp);\n target.removeEventListener(\"pointercancel\", this.handlePointerUp);\n\n if (this.draggingHandle) {\n this.dispatchEvent(\n new CustomEvent(\"trim-change-end\", {\n detail: {\n elementId: this.elementId,\n type: this.draggingHandle,\n },\n bubbles: true,\n composed: true,\n }),\n );\n }\n\n this.draggingHandle = null;\n\n if (this.editingContext) {\n this.editingContext.setState({ mode: \"idle\" });\n }\n };\n\n render() {\n const pxPerMs = this.#effectivePixelsPerMs;\n const trimStartPx = this.trimStartMs * pxPerMs;\n const trimEndPx = (this.trimEndMs || 0) * pxPerMs;\n const isStandalone = this.mode === \"standalone\";\n const handleWidthPx =\n parseFloat(\n getComputedStyle(this).getPropertyValue(\"--trim-handle-width\"),\n ) || 8;\n\n return html`\n ${\n this.showOverlays && this.trimStartMs > 0\n ? html`<div\n class=\"trim-overlay trim-overlay-start\"\n style=${styleMap({ width: `${trimStartPx}px` })}\n ></div>`\n : nothing\n }\n ${\n this.showOverlays && this.trimEndMs > 0\n ? html`<div\n class=\"trim-overlay trim-overlay-end\"\n style=${styleMap({ width: `${trimEndPx}px` })}\n ></div>`\n : nothing\n }\n\n ${\n isStandalone\n ? html`\n <div\n class=\"region ${this.draggingHandle === \"region\" ? \"dragging\" : \"\"}\"\n style=${styleMap({\n left: `${trimStartPx + handleWidthPx}px`,\n right: `${trimEndPx + handleWidthPx}px`,\n })}\n @pointerdown=${(e: PointerEvent) => this.handleRegionPointerDown(e)}\n ></div>\n <div class=\"selected-border selected-border-top\"\n style=${styleMap({\n left: `${trimStartPx}px`,\n right: `${trimEndPx}px`,\n })}\n ></div>\n <div class=\"selected-border selected-border-bottom\"\n style=${styleMap({\n left: `${trimStartPx}px`,\n right: `${trimEndPx}px`,\n })}\n ></div>\n `\n : nothing\n }\n\n <div\n class=\"handle handle-start ${this.draggingHandle === \"start\" ? \"dragging\" : \"\"}\"\n style=${isStandalone ? styleMap({ left: `${trimStartPx}px` }) : nothing}\n @pointerdown=${(e: PointerEvent) => this.handlePointerDown(e, \"start\")}\n >\n <div class=\"handle-inner\">\n <slot name=\"handle-start\"></slot>\n </div>\n </div>\n <div\n class=\"handle handle-end ${this.draggingHandle === \"end\" ? \"dragging\" : \"\"}\"\n style=${isStandalone ? styleMap({ right: `${trimEndPx}px` }) : nothing}\n @pointerdown=${(e: PointerEvent) => this.handlePointerDown(e, \"end\")}\n >\n <div class=\"handle-inner\">\n <slot name=\"handle-end\"></slot>\n </div>\n </div>\n `;\n }\n}\n\ndeclare global {\n interface HTMLElementTagNameMap {\n \"ef-trim-handles\": EFTrimHandles;\n }\n}\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"flattenHierarchy.js","names":["rows: TimelineRowModel[]"],"sources":["../../../src/gui/timeline/flattenHierarchy.ts"],"sourcesContent":["import {\n isEFTemporal,\n type TemporalMixinInterface,\n} from \"../../elements/EFTemporal.js\";\nimport { EFTimegroup } from \"../../elements/EFTimegroup.js\";\n\nexport interface TimelineRowModel {\n element: TemporalMixinInterface & Element;\n depth: number;\n}\n\n/**\n * Flattens a hierarchical temporal element tree into a flat array of rows.\n * Each row contains the element and its depth in the hierarchy.\n *\n * @param root - The root temporal element to flatten\n * @param startDepth - Starting depth (default 0)\n * @returns Array of {element, depth} in depth-first order\n */\nexport function flattenHierarchy(\n root: TemporalMixinInterface & Element,\n startDepth = 0,\n): TimelineRowModel[] {\n const rows: TimelineRowModel[] = [{ element: root, depth: startDepth }];\n\n if (root instanceof EFTimegroup) {\n for (const child of root.children) {\n if (isEFTemporal(child)) {\n // Skip child elements that are consolidated into their parent track\n const tagName = (child as Element).tagName?.toUpperCase();\n \n // Skip captions child elements - they're shown inline in the captions track\n if (\n tagName === \"EF-CAPTIONS-ACTIVE-WORD\" ||\n tagName === \"EF-CAPTIONS-SEGMENT\" ||\n tagName === \"EF-CAPTIONS-BEFORE-ACTIVE-WORD\" ||\n tagName === \"EF-CAPTIONS-AFTER-ACTIVE-WORD\"\n ) {\n continue;\n }\n \n // Skip text segments - they're shown inline in the text track\n if (tagName === \"EF-TEXT-SEGMENT\") {\n continue;\n }\n \n rows.push(\n ...flattenHierarchy(\n child as TemporalMixinInterface & Element,\n startDepth + 1,\n ),\n );\n }\n }\n }\n\n return rows;\n}\n\n"],"mappings":";;;;;;;;;;;;AAmBA,SAAgB,iBACd,MACA,aAAa,GACO;CACpB,MAAMA,OAA2B,CAAC;EAAE,SAAS;EAAM,OAAO;EAAY,CAAC;AAEvE,KAAI,gBAAgB,aAClB;OAAK,MAAM,SAAS,KAAK,SACvB,KAAI,aAAa,MAAM,EAAE;GAEvB,MAAM,UAAW,MAAkB,SAAS,aAAa;AAGzD,OACE,YAAY,6BACZ,YAAY,yBACZ,YAAY,oCACZ,YAAY,gCAEZ;AAIF,OAAI,YAAY,kBACd;AAGF,QAAK,KACH,GAAG,iBACD,OACA,aAAa,EACd,CACF;;;AAKP,QAAO"}
1
+ {"version":3,"file":"flattenHierarchy.js","names":["rows: TimelineRowModel[]"],"sources":["../../../src/gui/timeline/flattenHierarchy.ts"],"sourcesContent":["import {\n isEFTemporal,\n type TemporalMixinInterface,\n} from \"../../elements/EFTemporal.js\";\nimport { EFTimegroup } from \"../../elements/EFTimegroup.js\";\n\nexport interface TimelineRowModel {\n element: TemporalMixinInterface & Element;\n depth: number;\n}\n\n/**\n * Flattens a hierarchical temporal element tree into a flat array of rows.\n * Each row contains the element and its depth in the hierarchy.\n *\n * @param root - The root temporal element to flatten\n * @param startDepth - Starting depth (default 0)\n * @returns Array of {element, depth} in depth-first order\n */\nexport function flattenHierarchy(\n root: TemporalMixinInterface & Element,\n startDepth = 0,\n): TimelineRowModel[] {\n const rows: TimelineRowModel[] = [{ element: root, depth: startDepth }];\n\n if (root instanceof EFTimegroup) {\n for (const child of root.children) {\n if (isEFTemporal(child)) {\n // Skip child elements that are consolidated into their parent track\n const tagName = (child as Element).tagName?.toUpperCase();\n\n // Skip captions child elements - they're shown inline in the captions track\n if (\n tagName === \"EF-CAPTIONS-ACTIVE-WORD\" ||\n tagName === \"EF-CAPTIONS-SEGMENT\" ||\n tagName === \"EF-CAPTIONS-BEFORE-ACTIVE-WORD\" ||\n tagName === \"EF-CAPTIONS-AFTER-ACTIVE-WORD\"\n ) {\n continue;\n }\n\n // Skip text segments - they're shown inline in the text track\n if (tagName === \"EF-TEXT-SEGMENT\") {\n continue;\n }\n\n rows.push(\n ...flattenHierarchy(\n child as TemporalMixinInterface & Element,\n startDepth + 1,\n ),\n );\n }\n }\n }\n\n return rows;\n}\n"],"mappings":";;;;;;;;;;;;AAmBA,SAAgB,iBACd,MACA,aAAa,GACO;CACpB,MAAMA,OAA2B,CAAC;EAAE,SAAS;EAAM,OAAO;EAAY,CAAC;AAEvE,KAAI,gBAAgB,aAClB;OAAK,MAAM,SAAS,KAAK,SACvB,KAAI,aAAa,MAAM,EAAE;GAEvB,MAAM,UAAW,MAAkB,SAAS,aAAa;AAGzD,OACE,YAAY,6BACZ,YAAY,yBACZ,YAAY,oCACZ,YAAY,gCAEZ;AAIF,OAAI,YAAY,kBACd;AAGF,QAAK,KACH,GAAG,iBACD,OACA,aAAa,EACd,CACF;;;AAKP,QAAO"}
@@ -0,0 +1,34 @@
1
+ //#region src/gui/timeline/timelineEditingContext.d.ts
2
+ type TimelineEditingState = {
3
+ mode: "idle";
4
+ } | {
5
+ mode: "scrubbing";
6
+ startTimeMs: number;
7
+ } | {
8
+ mode: "trimming";
9
+ elementId: string;
10
+ handle: "start" | "end";
11
+ } | {
12
+ mode: "dragging";
13
+ elementIds: string[];
14
+ startPositions: Map<string, number>;
15
+ } | {
16
+ mode: "selecting";
17
+ box: DOMRect;
18
+ };
19
+ interface TimelineEditingContext {
20
+ state: TimelineEditingState;
21
+ setState: (state: TimelineEditingState) => void;
22
+ /**
23
+ * Returns true if any editing operation is in progress (not idle).
24
+ */
25
+ isEditing: () => boolean;
26
+ /**
27
+ * Returns true if hover and other passive interactions should be allowed.
28
+ * False when an active editing operation would be disrupted by hover feedback.
29
+ */
30
+ canInteract: () => boolean;
31
+ }
32
+ //#endregion
33
+ export { TimelineEditingContext };
34
+ //# sourceMappingURL=timelineEditingContext.d.ts.map
@@ -0,0 +1,24 @@
1
+ import { createContext } from "@lit/context";
2
+
3
+ //#region src/gui/timeline/timelineEditingContext.ts
4
+ const timelineEditingContext = createContext(Symbol("timelineEditingContext"));
5
+ /**
6
+ * Create a timeline editing context with helper methods.
7
+ */
8
+ function createTimelineEditingContext() {
9
+ const context = {
10
+ state: { mode: "idle" },
11
+ setState: (state) => {
12
+ context.state = state;
13
+ },
14
+ isEditing: () => context.state.mode !== "idle",
15
+ canInteract: () => {
16
+ return context.state.mode === "idle";
17
+ }
18
+ };
19
+ return context;
20
+ }
21
+
22
+ //#endregion
23
+ export { createTimelineEditingContext, timelineEditingContext };
24
+ //# sourceMappingURL=timelineEditingContext.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"timelineEditingContext.js","names":["context: TimelineEditingContext"],"sources":["../../../src/gui/timeline/timelineEditingContext.ts"],"sourcesContent":["import { createContext } from \"@lit/context\";\n\nexport type TimelineEditingState =\n | { mode: \"idle\" }\n | { mode: \"scrubbing\"; startTimeMs: number }\n | { mode: \"trimming\"; elementId: string; handle: \"start\" | \"end\" }\n | {\n mode: \"dragging\";\n elementIds: string[];\n startPositions: Map<string, number>;\n }\n | { mode: \"selecting\"; box: DOMRect };\n\nexport interface TimelineEditingContext {\n state: TimelineEditingState;\n setState: (state: TimelineEditingState) => void;\n\n /**\n * Returns true if any editing operation is in progress (not idle).\n */\n isEditing: () => boolean;\n\n /**\n * Returns true if hover and other passive interactions should be allowed.\n * False when an active editing operation would be disrupted by hover feedback.\n */\n canInteract: () => boolean;\n}\n\nexport const timelineEditingContext = createContext<TimelineEditingContext>(\n Symbol(\"timelineEditingContext\"),\n);\n\n/**\n * Create a timeline editing context with helper methods.\n */\nexport function createTimelineEditingContext(): TimelineEditingContext {\n const context: TimelineEditingContext = {\n state: { mode: \"idle\" },\n setState: (state: TimelineEditingState) => {\n context.state = state;\n },\n isEditing: () => context.state.mode !== \"idle\",\n canInteract: () => {\n // Block interactions during active drag operations\n return context.state.mode === \"idle\";\n },\n };\n return context;\n}\n"],"mappings":";;;AA6BA,MAAa,yBAAyB,cACpC,OAAO,yBAAyB,CACjC;;;;AAKD,SAAgB,+BAAuD;CACrE,MAAMA,UAAkC;EACtC,OAAO,EAAE,MAAM,QAAQ;EACvB,WAAW,UAAgC;AACzC,WAAQ,QAAQ;;EAElB,iBAAiB,QAAQ,MAAM,SAAS;EACxC,mBAAmB;AAEjB,UAAO,QAAQ,MAAM,SAAS;;EAEjC;AACD,QAAO"}
@@ -1 +1 @@
1
- {"version":3,"file":"timelineStateContext.js","names":[],"sources":["../../../src/gui/timeline/timelineStateContext.ts"],"sourcesContent":["import { createContext } from \"@lit/context\";\n\n/**\n * The core invariant of the timeline system.\n * Everything else (ruler positions, track positions, playhead position) derives from this.\n */\nexport interface TimelineState {\n /** Pixels per millisecond - the single zoom value */\n pixelsPerMs: number;\n /** Current playhead position in milliseconds */\n currentTimeMs: number;\n /** Total duration in milliseconds */\n durationMs: number;\n /** Viewport scroll position in pixels - single source of truth for visible time range */\n viewportScrollLeft: number;\n /** Viewport width in pixels - for calculating visible time range */\n viewportWidth: number;\n /** Seek to a specific time */\n seek: (timeMs: number) => void;\n /** Zoom in */\n zoomIn: () => void;\n /** Zoom out */\n zoomOut: () => void;\n}\n\nexport const timelineStateContext =\n createContext<TimelineState>(\"timeline-state\");\n\n/**\n * Convert time to pixel position\n */\nexport function timeToPx(timeMs: number, pixelsPerMs: number): number {\n return timeMs * pixelsPerMs;\n}\n\n/**\n * Convert pixel position to time\n */\nexport function pxToTime(px: number, pixelsPerMs: number): number {\n return px / pixelsPerMs;\n}\n\n/**\n * Default pixels per ms at 100% zoom (100 pixels per second)\n */\nexport const DEFAULT_PIXELS_PER_MS = 0.1;\n\n/**\n * Timeline row height in pixels - must match --timeline-row-height CSS variable\n */\nexport const TIMELINE_ROW_HEIGHT = 28;\n\n/**\n * Timeline track content height in pixels - must match --timeline-track-height CSS variable\n */\nexport const TIMELINE_TRACK_HEIGHT = 22;\n\n/**\n * Vertical padding within a row (row height - track height) / 2\n */\nexport const TIMELINE_ROW_PADDING = (TIMELINE_ROW_HEIGHT - TIMELINE_TRACK_HEIGHT) / 2;\n\n/**\n * Calculate pixels per ms from a zoom scale\n */\nexport function zoomToPixelsPerMs(zoomScale: number): number {\n return DEFAULT_PIXELS_PER_MS * zoomScale;\n}\n\n/**\n * Calculate zoom scale from pixels per ms\n */\nexport function pixelsPerMsToZoom(pixelsPerMs: number): number {\n return pixelsPerMs / DEFAULT_PIXELS_PER_MS;\n}\n"],"mappings":";;;AAyBA,MAAa,uBACX,cAA6B,iBAAiB;;;;AAKhD,SAAgB,SAAS,QAAgB,aAA6B;AACpE,QAAO,SAAS;;;;;AAMlB,SAAgB,SAAS,IAAY,aAA6B;AAChE,QAAO,KAAK;;;;;AAMd,MAAa,wBAAwB;;;;AAKrC,MAAa,sBAAsB;;;;AAKnC,MAAa,wBAAwB;;;;AAKrC,MAAa,wBAAwB,sBAAsB,yBAAyB;;;;AAYpF,SAAgB,kBAAkB,aAA6B;AAC7D,QAAO,cAAc"}
1
+ {"version":3,"file":"timelineStateContext.js","names":[],"sources":["../../../src/gui/timeline/timelineStateContext.ts"],"sourcesContent":["import { createContext } from \"@lit/context\";\n\n/**\n * The core invariant of the timeline system.\n * Everything else (ruler positions, track positions, playhead position) derives from this.\n */\nexport interface TimelineState {\n /** Pixels per millisecond - the single zoom value */\n pixelsPerMs: number;\n /** Current playhead position in milliseconds */\n currentTimeMs: number;\n /** Total duration in milliseconds */\n durationMs: number;\n /** Viewport scroll position in pixels - single source of truth for visible time range */\n viewportScrollLeft: number;\n /** Viewport width in pixels - for calculating visible time range */\n viewportWidth: number;\n /** Seek to a specific time */\n seek: (timeMs: number) => void;\n /** Zoom in */\n zoomIn: () => void;\n /** Zoom out */\n zoomOut: () => void;\n}\n\nexport const timelineStateContext =\n createContext<TimelineState>(\"timeline-state\");\n\n/**\n * Convert time to pixel position\n */\nexport function timeToPx(timeMs: number, pixelsPerMs: number): number {\n return timeMs * pixelsPerMs;\n}\n\n/**\n * Convert pixel position to time\n */\nexport function pxToTime(px: number, pixelsPerMs: number): number {\n return px / pixelsPerMs;\n}\n\n/**\n * Default pixels per ms at 100% zoom (100 pixels per second)\n */\nexport const DEFAULT_PIXELS_PER_MS = 0.1;\n\n/**\n * Timeline row height in pixels - must match --timeline-row-height CSS variable\n */\nexport const TIMELINE_ROW_HEIGHT = 28;\n\n/**\n * Timeline track content height in pixels - must match --timeline-track-height CSS variable\n */\nexport const TIMELINE_TRACK_HEIGHT = 22;\n\n/**\n * Vertical padding within a row (row height - track height) / 2\n */\nexport const TIMELINE_ROW_PADDING =\n (TIMELINE_ROW_HEIGHT - TIMELINE_TRACK_HEIGHT) / 2;\n\n/**\n * Calculate pixels per ms from a zoom scale\n */\nexport function zoomToPixelsPerMs(zoomScale: number): number {\n return DEFAULT_PIXELS_PER_MS * zoomScale;\n}\n\n/**\n * Calculate zoom scale from pixels per ms\n */\nexport function pixelsPerMsToZoom(pixelsPerMs: number): number {\n return pixelsPerMs / DEFAULT_PIXELS_PER_MS;\n}\n"],"mappings":";;;AAyBA,MAAa,uBACX,cAA6B,iBAAiB;;;;AAKhD,SAAgB,SAAS,QAAgB,aAA6B;AACpE,QAAO,SAAS;;;;;AAMlB,SAAgB,SAAS,IAAY,aAA6B;AAChE,QAAO,KAAK;;;;;AAMd,MAAa,wBAAwB;;;;AAKrC,MAAa,sBAAsB;;;;AAKnC,MAAa,wBAAwB;;;;AAKrC,MAAa,wBACV,sBAAsB,yBAAyB;;;;AAYlD,SAAgB,kBAAkB,aAA6B;AAC7D,QAAO,cAAc"}
@@ -94,7 +94,7 @@ let EFAudioTrack = class EFAudioTrack$1 extends TrackItem {
94
94
  }
95
95
  /**
96
96
  * Render the waveform to canvas with virtual rendering.
97
- *
97
+ *
98
98
  * The approach:
99
99
  * 1. Calculate the visible portion of the track (intersection of track and viewport)
100
100
  * 2. Position the canvas at that visible portion within the track
@@ -1 +1 @@
1
- {"version":3,"file":"AudioTrack.js","names":["EFAudioTrack","#loadWaveformData","#lastSrc","#abortController","#scheduleRender","#renderRequested","#renderWaveform","#getTrackPositionInfo","#hostHeight","#drawWaveformRegion","#resizeObserver","#renderPlaceholder"],"sources":["../../../../src/gui/timeline/tracks/AudioTrack.ts"],"sourcesContent":["import { consume } from \"@lit/context\";\nimport { css, html, nothing } from \"lit\";\nimport { customElement, state } from \"lit/decorators.js\";\nimport { createRef, ref } from \"lit/directives/ref.js\";\nimport { EFAudio } from \"../../../elements/EFAudio.js\";\nimport { TrackItem } from \"./TrackItem.js\";\nimport {\n extractWaveformData,\n type WaveformData,\n} from \"./waveformUtils.js\";\nimport {\n timelineStateContext,\n type TimelineState,\n} from \"../timelineStateContext.js\";\n\n/** Padding in pixels to render beyond visible area (for smooth scrolling) */\nconst VIRTUAL_RENDER_PADDING_PX = 100;\n\n@customElement(\"ef-audio-track\")\nexport class EFAudioTrack extends TrackItem {\n static styles = [\n ...TrackItem.styles,\n css`\n .waveform-host {\n position: absolute;\n left: 0;\n top: 2px;\n right: 0;\n bottom: 2px;\n overflow: hidden;\n }\n .waveform-canvas {\n display: block;\n position: absolute;\n top: 0;\n height: 100%;\n pointer-events: none;\n }\n `,\n ];\n\n canvasRef = createRef<HTMLCanvasElement>();\n\n /** Timeline state context for viewport info */\n @consume({ context: timelineStateContext, subscribe: true })\n @state()\n private _timelineState?: TimelineState;\n\n @state()\n private _waveformData: WaveformData | null = null;\n\n @state()\n private _isLoading = false;\n\n #lastSrc: string | null = null;\n #abortController: AbortController | null = null;\n #resizeObserver?: ResizeObserver;\n #renderRequested = false;\n #hostHeight = 0;\n\n /**\n * Load waveform data when the audio source changes\n */\n async #loadWaveformData(): Promise<void> {\n const audio = this.element as EFAudio;\n const src = audio?.src;\n\n // Skip if no source or same source already loaded\n if (!src || src === this.#lastSrc) {\n return;\n }\n\n this.#lastSrc = src;\n\n // Cancel any in-progress load\n this.#abortController?.abort();\n this.#abortController = new AbortController();\n\n this._isLoading = true;\n\n try {\n const waveformData = await extractWaveformData(\n src,\n this.#abortController.signal,\n );\n\n if (waveformData) {\n this._waveformData = waveformData;\n this.#scheduleRender();\n }\n } catch (error) {\n if (!(error instanceof DOMException && error.name === \"AbortError\")) {\n console.warn(\"Failed to load waveform data:\", error);\n }\n } finally {\n this._isLoading = false;\n }\n }\n\n /**\n * Schedule a canvas render on the next animation frame\n */\n #scheduleRender(): void {\n if (this.#renderRequested) return;\n this.#renderRequested = true;\n\n requestAnimationFrame(() => {\n this.#renderRequested = false;\n this.#renderWaveform();\n });\n }\n\n /**\n * Get the track's position info relative to timeline scroll\n */\n #getTrackPositionInfo(): {\n trackStartPx: number;\n trackWidthPx: number;\n viewportScrollLeft: number;\n viewportWidth: number;\n pixelsPerMs: number;\n } | null {\n const audio = this.element as EFAudio;\n const durationMs = audio.durationMs ?? 0;\n if (durationMs === 0) return null;\n\n const pixelsPerMs = this._timelineState?.pixelsPerMs ?? this.pixelsPerMs;\n const trackWidthPx = durationMs * pixelsPerMs;\n\n // Get track's absolute position from startTimeMs\n const trackStartMs = audio.startTimeMs ?? 0;\n const trackStartPx = trackStartMs * pixelsPerMs;\n\n // Get viewport info from context\n const viewportScrollLeft = this._timelineState?.viewportScrollLeft ?? 0;\n const viewportWidth = this._timelineState?.viewportWidth ?? 800;\n\n return {\n trackStartPx,\n trackWidthPx,\n viewportScrollLeft,\n viewportWidth,\n pixelsPerMs,\n };\n }\n\n /**\n * Render the waveform to canvas with virtual rendering.\n * \n * The approach:\n * 1. Calculate the visible portion of the track (intersection of track and viewport)\n * 2. Position the canvas at that visible portion within the track\n * 3. Draw only the waveform data for that visible time range\n * 4. Update position and content as scroll/zoom changes\n */\n #renderWaveform(): void {\n const canvas = this.canvasRef.value;\n const waveformData = this._waveformData;\n\n if (!canvas || !waveformData) return;\n\n const positionInfo = this.#getTrackPositionInfo();\n if (!positionInfo) return;\n\n const { trackStartPx, trackWidthPx, viewportScrollLeft, viewportWidth, pixelsPerMs } =\n positionInfo;\n\n // Calculate visible region in absolute pixels (with padding for smooth scrolling)\n const visibleLeftPx = viewportScrollLeft - VIRTUAL_RENDER_PADDING_PX;\n const visibleRightPx = viewportScrollLeft + viewportWidth + VIRTUAL_RENDER_PADDING_PX;\n\n // Track boundaries in absolute pixels\n const trackEndPx = trackStartPx + trackWidthPx;\n\n // Check if track is visible at all\n if (trackEndPx < visibleLeftPx || trackStartPx > visibleRightPx) {\n // Track not visible, hide canvas\n canvas.style.display = \"none\";\n return;\n }\n canvas.style.display = \"block\";\n\n // Calculate the intersection: what part of the track is visible\n // All coordinates are now relative to the track's left edge (0 = track start)\n const visibleStartInTrack = Math.max(0, visibleLeftPx - trackStartPx);\n const visibleEndInTrack = Math.min(trackWidthPx, visibleRightPx - trackStartPx);\n const visibleWidthPx = visibleEndInTrack - visibleStartInTrack;\n\n if (visibleWidthPx <= 0) return;\n\n const height = this.#hostHeight || 18;\n\n // Set canvas size with DPR\n const dpr = window.devicePixelRatio || 1;\n const targetWidth = Math.ceil(visibleWidthPx * dpr);\n const targetHeight = Math.ceil(height * dpr);\n\n if (canvas.width !== targetWidth || canvas.height !== targetHeight) {\n canvas.width = targetWidth;\n canvas.height = targetHeight;\n }\n \n // Position canvas at the visible portion within the track\n canvas.style.left = `${visibleStartInTrack}px`;\n canvas.style.width = `${visibleWidthPx}px`;\n canvas.style.height = `${height}px`;\n\n const ctx = canvas.getContext(\"2d\");\n if (!ctx) return;\n\n ctx.setTransform(dpr, 0, 0, dpr, 0, 0);\n ctx.clearRect(0, 0, visibleWidthPx, height);\n\n // Calculate what time range to render\n const audio = this.element as EFAudio;\n const sourceInMs = audio.sourceStartMs ?? 0;\n \n // Convert visible pixel range to time range\n const timeStartMs = sourceInMs + visibleStartInTrack / pixelsPerMs;\n const timeEndMs = sourceInMs + visibleEndInTrack / pixelsPerMs;\n\n // Draw the waveform for the visible portion\n this.#drawWaveformRegion(\n ctx,\n waveformData,\n 0, // Start drawing at x=0 of canvas (canvas is already positioned)\n visibleWidthPx,\n height,\n timeStartMs,\n timeEndMs,\n );\n }\n\n /**\n * Draw a region of the waveform to canvas\n */\n #drawWaveformRegion(\n ctx: CanvasRenderingContext2D,\n waveformData: WaveformData,\n x: number,\n width: number,\n height: number,\n startMs: number,\n endMs: number,\n ): void {\n const { peaks, samplesPerSecond } = waveformData;\n\n // Calculate sample range\n const startSample = Math.floor((startMs / 1000) * samplesPerSecond);\n const endSample = Math.ceil((endMs / 1000) * samplesPerSecond);\n const sampleCount = endSample - startSample;\n\n if (sampleCount <= 0 || width <= 0) return;\n\n const centerY = height / 2;\n const halfHeight = (height / 2) - 2; // Leave 2px padding top/bottom\n const color = this.getElementTypeColor();\n\n ctx.fillStyle = color;\n ctx.globalAlpha = 0.8;\n ctx.beginPath();\n\n // Draw top half (max values) left to right\n const pixelsPerSample = width / sampleCount;\n\n for (let i = 0; i <= sampleCount; i++) {\n const sampleIndex = startSample + i;\n const peakIndex = sampleIndex * 2;\n\n // Clamp to valid range\n if (peakIndex + 1 >= peaks.length) break;\n\n const maxValue = peaks[peakIndex + 1] ?? 0;\n const px = x + i * pixelsPerSample;\n const py = centerY - maxValue * halfHeight;\n\n if (i === 0) {\n ctx.moveTo(px, py);\n } else {\n ctx.lineTo(px, py);\n }\n }\n\n // Draw bottom half (min values) right to left\n for (let i = sampleCount; i >= 0; i--) {\n const sampleIndex = startSample + i;\n const peakIndex = sampleIndex * 2;\n\n // Clamp to valid range\n if (peakIndex >= peaks.length) continue;\n\n const minValue = peaks[peakIndex] ?? 0;\n const px = x + i * pixelsPerSample;\n const py = centerY - minValue * halfHeight;\n\n ctx.lineTo(px, py);\n }\n\n ctx.closePath();\n ctx.fill();\n\n // Draw center line\n ctx.globalAlpha = 0.3;\n ctx.strokeStyle = color;\n ctx.lineWidth = 1;\n ctx.beginPath();\n ctx.moveTo(x, centerY);\n ctx.lineTo(x + width, centerY);\n ctx.stroke();\n\n ctx.globalAlpha = 1;\n }\n\n connectedCallback(): void {\n super.connectedCallback();\n\n // Start loading waveform data\n this.#loadWaveformData();\n\n // Observe size changes\n this.#resizeObserver = new ResizeObserver((entries) => {\n for (const entry of entries) {\n this.#hostHeight =\n entry.borderBoxSize?.[0]?.blockSize ?? entry.contentRect.height;\n this.#scheduleRender();\n }\n });\n }\n\n disconnectedCallback(): void {\n super.disconnectedCallback();\n this.#abortController?.abort();\n this.#resizeObserver?.disconnect();\n }\n\n updated(changedProperties: Map<string | number | symbol, unknown>): void {\n super.updated(changedProperties);\n\n // Check if we need to reload waveform data\n const audio = this.element as EFAudio;\n if (audio?.src !== this.#lastSrc) {\n this.#loadWaveformData();\n }\n\n // Re-render when timeline state changes (scroll, zoom)\n if (changedProperties.has(\"_timelineState\")) {\n this.#scheduleRender();\n }\n\n // Attach resize observer to track container once rendered\n if (this.canvasRef.value && this.#resizeObserver) {\n const container = this.canvasRef.value.parentElement;\n if (container) {\n this.#resizeObserver.disconnect();\n this.#resizeObserver.observe(container);\n }\n }\n\n // Always schedule render after update to catch any changes\n this.#scheduleRender();\n }\n\n contents() {\n const audio = this.element as EFAudio;\n if (!(audio instanceof EFAudio)) {\n return nothing;\n }\n\n const durationMs = audio.durationMs ?? 0;\n if (durationMs === 0) {\n return nothing;\n }\n\n // Show loading placeholder if no waveform data yet\n if (!this._waveformData) {\n return this.#renderPlaceholder();\n }\n\n // The host fills the track container, canvas is positioned within it\n return html`\n <div class=\"waveform-host\">\n <canvas ${ref(this.canvasRef)} class=\"waveform-canvas\"></canvas>\n </div>\n `;\n }\n\n /**\n * Render placeholder while loading\n */\n #renderPlaceholder() {\n return html`\n <div\n style=\"\n position: absolute;\n left: 0;\n top: 2px;\n bottom: 2px;\n right: 0;\n background: linear-gradient(90deg, \n ${this.getElementTypeColor()}22 0%, \n ${this.getElementTypeColor()}44 50%,\n ${this.getElementTypeColor()}22 100%\n );\n background-size: 200% 100%;\n animation: ${this._isLoading ? \"shimmer 1.5s infinite\" : \"none\"};\n border-radius: 2px;\n \"\n ></div>\n <style>\n @keyframes shimmer {\n 0% { background-position: 200% 0; }\n 100% { background-position: -200% 0; }\n }\n </style>\n `;\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"AudioTrack.js","names":["EFAudioTrack","#loadWaveformData","#lastSrc","#abortController","#scheduleRender","#renderRequested","#renderWaveform","#getTrackPositionInfo","#hostHeight","#drawWaveformRegion","#resizeObserver","#renderPlaceholder"],"sources":["../../../../src/gui/timeline/tracks/AudioTrack.ts"],"sourcesContent":["import { consume } from \"@lit/context\";\nimport { css, html, nothing } from \"lit\";\nimport { customElement, state } from \"lit/decorators.js\";\nimport { createRef, ref } from \"lit/directives/ref.js\";\nimport { EFAudio } from \"../../../elements/EFAudio.js\";\nimport { TrackItem } from \"./TrackItem.js\";\nimport { extractWaveformData, type WaveformData } from \"./waveformUtils.js\";\nimport {\n timelineStateContext,\n type TimelineState,\n} from \"../timelineStateContext.js\";\n\n/** Padding in pixels to render beyond visible area (for smooth scrolling) */\nconst VIRTUAL_RENDER_PADDING_PX = 100;\n\n@customElement(\"ef-audio-track\")\nexport class EFAudioTrack extends TrackItem {\n static styles = [\n ...TrackItem.styles,\n css`\n .waveform-host {\n position: absolute;\n left: 0;\n top: 2px;\n right: 0;\n bottom: 2px;\n overflow: hidden;\n }\n .waveform-canvas {\n display: block;\n position: absolute;\n top: 0;\n height: 100%;\n pointer-events: none;\n }\n `,\n ];\n\n canvasRef = createRef<HTMLCanvasElement>();\n\n /** Timeline state context for viewport info */\n @consume({ context: timelineStateContext, subscribe: true })\n @state()\n private _timelineState?: TimelineState;\n\n @state()\n private _waveformData: WaveformData | null = null;\n\n @state()\n private _isLoading = false;\n\n #lastSrc: string | null = null;\n #abortController: AbortController | null = null;\n #resizeObserver?: ResizeObserver;\n #renderRequested = false;\n #hostHeight = 0;\n\n /**\n * Load waveform data when the audio source changes\n */\n async #loadWaveformData(): Promise<void> {\n const audio = this.element as EFAudio;\n const src = audio?.src;\n\n // Skip if no source or same source already loaded\n if (!src || src === this.#lastSrc) {\n return;\n }\n\n this.#lastSrc = src;\n\n // Cancel any in-progress load\n this.#abortController?.abort();\n this.#abortController = new AbortController();\n\n this._isLoading = true;\n\n try {\n const waveformData = await extractWaveformData(\n src,\n this.#abortController.signal,\n );\n\n if (waveformData) {\n this._waveformData = waveformData;\n this.#scheduleRender();\n }\n } catch (error) {\n if (!(error instanceof DOMException && error.name === \"AbortError\")) {\n console.warn(\"Failed to load waveform data:\", error);\n }\n } finally {\n this._isLoading = false;\n }\n }\n\n /**\n * Schedule a canvas render on the next animation frame\n */\n #scheduleRender(): void {\n if (this.#renderRequested) return;\n this.#renderRequested = true;\n\n requestAnimationFrame(() => {\n this.#renderRequested = false;\n this.#renderWaveform();\n });\n }\n\n /**\n * Get the track's position info relative to timeline scroll\n */\n #getTrackPositionInfo(): {\n trackStartPx: number;\n trackWidthPx: number;\n viewportScrollLeft: number;\n viewportWidth: number;\n pixelsPerMs: number;\n } | null {\n const audio = this.element as EFAudio;\n const durationMs = audio.durationMs ?? 0;\n if (durationMs === 0) return null;\n\n const pixelsPerMs = this._timelineState?.pixelsPerMs ?? this.pixelsPerMs;\n const trackWidthPx = durationMs * pixelsPerMs;\n\n // Get track's absolute position from startTimeMs\n const trackStartMs = audio.startTimeMs ?? 0;\n const trackStartPx = trackStartMs * pixelsPerMs;\n\n // Get viewport info from context\n const viewportScrollLeft = this._timelineState?.viewportScrollLeft ?? 0;\n const viewportWidth = this._timelineState?.viewportWidth ?? 800;\n\n return {\n trackStartPx,\n trackWidthPx,\n viewportScrollLeft,\n viewportWidth,\n pixelsPerMs,\n };\n }\n\n /**\n * Render the waveform to canvas with virtual rendering.\n *\n * The approach:\n * 1. Calculate the visible portion of the track (intersection of track and viewport)\n * 2. Position the canvas at that visible portion within the track\n * 3. Draw only the waveform data for that visible time range\n * 4. Update position and content as scroll/zoom changes\n */\n #renderWaveform(): void {\n const canvas = this.canvasRef.value;\n const waveformData = this._waveformData;\n\n if (!canvas || !waveformData) return;\n\n const positionInfo = this.#getTrackPositionInfo();\n if (!positionInfo) return;\n\n const {\n trackStartPx,\n trackWidthPx,\n viewportScrollLeft,\n viewportWidth,\n pixelsPerMs,\n } = positionInfo;\n\n // Calculate visible region in absolute pixels (with padding for smooth scrolling)\n const visibleLeftPx = viewportScrollLeft - VIRTUAL_RENDER_PADDING_PX;\n const visibleRightPx =\n viewportScrollLeft + viewportWidth + VIRTUAL_RENDER_PADDING_PX;\n\n // Track boundaries in absolute pixels\n const trackEndPx = trackStartPx + trackWidthPx;\n\n // Check if track is visible at all\n if (trackEndPx < visibleLeftPx || trackStartPx > visibleRightPx) {\n // Track not visible, hide canvas\n canvas.style.display = \"none\";\n return;\n }\n canvas.style.display = \"block\";\n\n // Calculate the intersection: what part of the track is visible\n // All coordinates are now relative to the track's left edge (0 = track start)\n const visibleStartInTrack = Math.max(0, visibleLeftPx - trackStartPx);\n const visibleEndInTrack = Math.min(\n trackWidthPx,\n visibleRightPx - trackStartPx,\n );\n const visibleWidthPx = visibleEndInTrack - visibleStartInTrack;\n\n if (visibleWidthPx <= 0) return;\n\n const height = this.#hostHeight || 18;\n\n // Set canvas size with DPR\n const dpr = window.devicePixelRatio || 1;\n const targetWidth = Math.ceil(visibleWidthPx * dpr);\n const targetHeight = Math.ceil(height * dpr);\n\n if (canvas.width !== targetWidth || canvas.height !== targetHeight) {\n canvas.width = targetWidth;\n canvas.height = targetHeight;\n }\n\n // Position canvas at the visible portion within the track\n canvas.style.left = `${visibleStartInTrack}px`;\n canvas.style.width = `${visibleWidthPx}px`;\n canvas.style.height = `${height}px`;\n\n const ctx = canvas.getContext(\"2d\");\n if (!ctx) return;\n\n ctx.setTransform(dpr, 0, 0, dpr, 0, 0);\n ctx.clearRect(0, 0, visibleWidthPx, height);\n\n // Calculate what time range to render\n const audio = this.element as EFAudio;\n const sourceInMs = audio.sourceStartMs ?? 0;\n\n // Convert visible pixel range to time range\n const timeStartMs = sourceInMs + visibleStartInTrack / pixelsPerMs;\n const timeEndMs = sourceInMs + visibleEndInTrack / pixelsPerMs;\n\n // Draw the waveform for the visible portion\n this.#drawWaveformRegion(\n ctx,\n waveformData,\n 0, // Start drawing at x=0 of canvas (canvas is already positioned)\n visibleWidthPx,\n height,\n timeStartMs,\n timeEndMs,\n );\n }\n\n /**\n * Draw a region of the waveform to canvas\n */\n #drawWaveformRegion(\n ctx: CanvasRenderingContext2D,\n waveformData: WaveformData,\n x: number,\n width: number,\n height: number,\n startMs: number,\n endMs: number,\n ): void {\n const { peaks, samplesPerSecond } = waveformData;\n\n // Calculate sample range\n const startSample = Math.floor((startMs / 1000) * samplesPerSecond);\n const endSample = Math.ceil((endMs / 1000) * samplesPerSecond);\n const sampleCount = endSample - startSample;\n\n if (sampleCount <= 0 || width <= 0) return;\n\n const centerY = height / 2;\n const halfHeight = height / 2 - 2; // Leave 2px padding top/bottom\n const color = this.getElementTypeColor();\n\n ctx.fillStyle = color;\n ctx.globalAlpha = 0.8;\n ctx.beginPath();\n\n // Draw top half (max values) left to right\n const pixelsPerSample = width / sampleCount;\n\n for (let i = 0; i <= sampleCount; i++) {\n const sampleIndex = startSample + i;\n const peakIndex = sampleIndex * 2;\n\n // Clamp to valid range\n if (peakIndex + 1 >= peaks.length) break;\n\n const maxValue = peaks[peakIndex + 1] ?? 0;\n const px = x + i * pixelsPerSample;\n const py = centerY - maxValue * halfHeight;\n\n if (i === 0) {\n ctx.moveTo(px, py);\n } else {\n ctx.lineTo(px, py);\n }\n }\n\n // Draw bottom half (min values) right to left\n for (let i = sampleCount; i >= 0; i--) {\n const sampleIndex = startSample + i;\n const peakIndex = sampleIndex * 2;\n\n // Clamp to valid range\n if (peakIndex >= peaks.length) continue;\n\n const minValue = peaks[peakIndex] ?? 0;\n const px = x + i * pixelsPerSample;\n const py = centerY - minValue * halfHeight;\n\n ctx.lineTo(px, py);\n }\n\n ctx.closePath();\n ctx.fill();\n\n // Draw center line\n ctx.globalAlpha = 0.3;\n ctx.strokeStyle = color;\n ctx.lineWidth = 1;\n ctx.beginPath();\n ctx.moveTo(x, centerY);\n ctx.lineTo(x + width, centerY);\n ctx.stroke();\n\n ctx.globalAlpha = 1;\n }\n\n connectedCallback(): void {\n super.connectedCallback();\n\n // Start loading waveform data\n this.#loadWaveformData();\n\n // Observe size changes\n this.#resizeObserver = new ResizeObserver((entries) => {\n for (const entry of entries) {\n this.#hostHeight =\n entry.borderBoxSize?.[0]?.blockSize ?? entry.contentRect.height;\n this.#scheduleRender();\n }\n });\n }\n\n disconnectedCallback(): void {\n super.disconnectedCallback();\n this.#abortController?.abort();\n this.#resizeObserver?.disconnect();\n }\n\n updated(changedProperties: Map<string | number | symbol, unknown>): void {\n super.updated(changedProperties);\n\n // Check if we need to reload waveform data\n const audio = this.element as EFAudio;\n if (audio?.src !== this.#lastSrc) {\n this.#loadWaveformData();\n }\n\n // Re-render when timeline state changes (scroll, zoom)\n if (changedProperties.has(\"_timelineState\")) {\n this.#scheduleRender();\n }\n\n // Attach resize observer to track container once rendered\n if (this.canvasRef.value && this.#resizeObserver) {\n const container = this.canvasRef.value.parentElement;\n if (container) {\n this.#resizeObserver.disconnect();\n this.#resizeObserver.observe(container);\n }\n }\n\n // Always schedule render after update to catch any changes\n this.#scheduleRender();\n }\n\n contents() {\n const audio = this.element as EFAudio;\n if (!(audio instanceof EFAudio)) {\n return nothing;\n }\n\n const durationMs = audio.durationMs ?? 0;\n if (durationMs === 0) {\n return nothing;\n }\n\n // Show loading placeholder if no waveform data yet\n if (!this._waveformData) {\n return this.#renderPlaceholder();\n }\n\n // The host fills the track container, canvas is positioned within it\n return html`\n <div class=\"waveform-host\">\n <canvas ${ref(this.canvasRef)} class=\"waveform-canvas\"></canvas>\n </div>\n `;\n }\n\n /**\n * Render placeholder while loading\n */\n #renderPlaceholder() {\n return html`\n <div\n style=\"\n position: absolute;\n left: 0;\n top: 2px;\n bottom: 2px;\n right: 0;\n background: linear-gradient(90deg, \n ${this.getElementTypeColor()}22 0%, \n ${this.getElementTypeColor()}44 50%,\n ${this.getElementTypeColor()}22 100%\n );\n background-size: 200% 100%;\n animation: ${this._isLoading ? \"shimmer 1.5s infinite\" : \"none\"};\n border-radius: 2px;\n \"\n ></div>\n <style>\n @keyframes shimmer {\n 0% { background-position: 200% 0; }\n 100% { background-position: -200% 0; }\n }\n </style>\n `;\n }\n}\n"],"mappings":"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"}