@editframe/elements 0.37.3-beta → 0.38.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 (327) 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 +2 -2
  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 +5 -8
  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 +2 -2
  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 +2 -2
  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 +2 -2
  107. package/dist/gui/EFFocusOverlay.js +3 -3
  108. package/dist/gui/EFFocusOverlay.js.map +1 -1
  109. package/dist/gui/EFOverlayItem.d.ts +2 -2
  110. package/dist/gui/EFOverlayLayer.d.ts +2 -2
  111. package/dist/gui/EFPause.d.ts +2 -2
  112. package/dist/gui/EFPause.js +1 -1
  113. package/dist/gui/EFPlay.d.ts +2 -2
  114. package/dist/gui/EFPlay.js +1 -1
  115. package/dist/gui/EFPreview.js +1 -1
  116. package/dist/gui/EFResizableBox.d.ts +2 -2
  117. package/dist/gui/EFResizableBox.js +5 -5
  118. package/dist/gui/EFResizableBox.js.map +1 -1
  119. package/dist/gui/EFScrubber.d.ts +2 -2
  120. package/dist/gui/EFScrubber.js +8 -13
  121. package/dist/gui/EFScrubber.js.map +1 -1
  122. package/dist/gui/EFTimeDisplay.d.ts +6 -2
  123. package/dist/gui/EFTimeDisplay.js +25 -7
  124. package/dist/gui/EFTimeDisplay.js.map +1 -1
  125. package/dist/gui/EFTimelineRuler.d.ts +2 -2
  126. package/dist/gui/EFTimelineRuler.js +3 -3
  127. package/dist/gui/EFTimelineRuler.js.map +1 -1
  128. package/dist/gui/EFToggleLoop.d.ts +2 -2
  129. package/dist/gui/EFToggleLoop.js +1 -1
  130. package/dist/gui/EFTogglePlay.d.ts +2 -2
  131. package/dist/gui/EFTogglePlay.js +1 -1
  132. package/dist/gui/EFTransformHandles.d.ts +2 -2
  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 +5 -3
  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 +3 -4
  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 +2 -2
  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 +2 -2
  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.d.ts +134 -32
  238. package/dist/preview/renderTimegroupToCanvas.js +321 -146
  239. package/dist/preview/renderTimegroupToCanvas.js.map +1 -1
  240. package/dist/preview/renderTimegroupToCanvas.types.d.ts +51 -0
  241. package/dist/preview/renderTimegroupToVideo.d.ts +20 -35
  242. package/dist/preview/renderTimegroupToVideo.js +94 -106
  243. package/dist/preview/renderTimegroupToVideo.js.map +1 -1
  244. package/dist/preview/renderTimegroupToVideo.types.d.ts +42 -0
  245. package/dist/preview/renderVideoToVideo.js +286 -0
  246. package/dist/preview/renderVideoToVideo.js.map +1 -0
  247. package/dist/preview/renderers.d.ts +56 -0
  248. package/dist/preview/renderers.js +13 -1
  249. package/dist/preview/renderers.js.map +1 -1
  250. package/dist/preview/rendering/ScaleConfig.js +74 -0
  251. package/dist/preview/rendering/ScaleConfig.js.map +1 -0
  252. package/dist/preview/rendering/inlineImages.d.ts +13 -0
  253. package/dist/preview/rendering/inlineImages.js +7 -44
  254. package/dist/preview/rendering/inlineImages.js.map +1 -1
  255. package/dist/preview/rendering/loadImage.d.ts +8 -0
  256. package/dist/preview/rendering/loadImage.js +22 -0
  257. package/dist/preview/rendering/loadImage.js.map +1 -0
  258. package/dist/preview/rendering/renderToImageNative.js +3 -3
  259. package/dist/preview/rendering/renderToImageNative.js.map +1 -1
  260. package/dist/preview/rendering/serializeTimelineDirect.js +224 -68
  261. package/dist/preview/rendering/serializeTimelineDirect.js.map +1 -1
  262. package/dist/preview/statsTrackingStrategy.js +1 -101
  263. package/dist/preview/statsTrackingStrategy.js.map +1 -1
  264. package/dist/preview/workers/WorkerPool.js +0 -1
  265. package/dist/preview/workers/WorkerPool.js.map +1 -1
  266. package/dist/preview/workers/encoderWorkerInline.js +21 -54
  267. package/dist/preview/workers/encoderWorkerInline.js.map +1 -1
  268. package/dist/render/EFRenderAPI.d.ts +2 -1
  269. package/dist/render/EFRenderAPI.js +12 -36
  270. package/dist/render/EFRenderAPI.js.map +1 -1
  271. package/dist/render/getRenderData.js +4 -4
  272. package/dist/render/getRenderData.js.map +1 -1
  273. package/dist/style.css +114 -163
  274. package/dist/transcoding/cache/RequestDeduplicator.js +1 -0
  275. package/dist/transcoding/cache/RequestDeduplicator.js.map +1 -1
  276. package/dist/transcoding/types/index.d.ts +1 -1
  277. package/dist/transcoding/utils/UrlGenerator.js +10 -3
  278. package/dist/transcoding/utils/UrlGenerator.js.map +1 -1
  279. package/dist/utils/LRUCache.js +1 -0
  280. package/dist/utils/LRUCache.js.map +1 -1
  281. package/dist/utils/frameTime.js +23 -1
  282. package/dist/utils/frameTime.js.map +1 -1
  283. package/package.json +45 -8
  284. package/scripts/build-css.js +8 -1
  285. package/test/setup.ts +0 -1
  286. package/test/useAssetMSW.ts +50 -0
  287. package/test/visualRegressionUtils.ts +23 -9
  288. package/tsdown.config.ts +6 -1
  289. package/dist/_virtual/rolldown_runtime.js +0 -27
  290. package/dist/elements/EFMedia/AssetIdMediaEngine.js.map +0 -1
  291. package/dist/elements/EFThumbnailStrip.d.ts +0 -167
  292. package/dist/elements/EFThumbnailStrip.js +0 -731
  293. package/dist/elements/EFThumbnailStrip.js.map +0 -1
  294. package/dist/elements/SessionThumbnailCache.js +0 -154
  295. package/dist/elements/SessionThumbnailCache.js.map +0 -1
  296. package/dist/node_modules/react/cjs/react-jsx-runtime.development.js +0 -688
  297. package/dist/node_modules/react/cjs/react-jsx-runtime.development.js.map +0 -1
  298. package/dist/node_modules/react/cjs/react.development.js +0 -1521
  299. package/dist/node_modules/react/cjs/react.development.js.map +0 -1
  300. package/dist/node_modules/react/index.js +0 -13
  301. package/dist/node_modules/react/index.js.map +0 -1
  302. package/dist/node_modules/react/jsx-runtime.js +0 -13
  303. package/dist/node_modules/react/jsx-runtime.js.map +0 -1
  304. package/dist/preview/encoding/types.d.ts +0 -1
  305. package/dist/preview/renderTimegroupPreview.js +0 -686
  306. package/dist/preview/renderTimegroupPreview.js.map +0 -1
  307. package/dist/preview/rendering/renderToImage.d.ts +0 -2
  308. package/dist/preview/rendering/renderToImage.js +0 -95
  309. package/dist/preview/rendering/renderToImage.js.map +0 -1
  310. package/dist/preview/rendering/renderToImageForeignObject.js +0 -163
  311. package/dist/preview/rendering/renderToImageForeignObject.js.map +0 -1
  312. package/dist/preview/rendering/renderToImageNative.d.ts +0 -1
  313. package/dist/preview/rendering/svgSerializer.js +0 -43
  314. package/dist/preview/rendering/svgSerializer.js.map +0 -1
  315. package/dist/preview/rendering/types.d.ts +0 -2
  316. package/dist/preview/thumbnailCacheSettings.js +0 -52
  317. package/dist/preview/thumbnailCacheSettings.js.map +0 -1
  318. package/dist/sandbox/PlaybackControls.d.ts +0 -1
  319. package/dist/sandbox/PlaybackControls.js +0 -10
  320. package/dist/sandbox/PlaybackControls.js.map +0 -1
  321. package/dist/sandbox/ScenarioRunner.d.ts +0 -1
  322. package/dist/sandbox/ScenarioRunner.js +0 -1
  323. package/dist/sandbox/defineSandbox.d.ts +0 -1
  324. package/dist/sandbox/index.d.ts +0 -3
  325. package/dist/sandbox/index.js +0 -2
  326. package/test/EFVideo.framegen.browsertest.ts +0 -80
  327. package/test/thumbnail-performance-test.html +0 -116
@@ -1,12 +1,13 @@
1
+ import { TWMixin } from "../../TWMixin2.js";
1
2
  import { __decorate } from "../../../_virtual/_@oxc-project_runtime@0.95.0/helpers/decorate.js";
2
3
  import { focusContext } from "../../focusContext.js";
3
4
  import { focusedElementContext } from "../../focusedElementContext.js";
4
- import { TWMixin } from "../../TWMixin2.js";
5
5
  import { EFAudio } from "../../../elements/EFAudio.js";
6
6
  import { EFVideo } from "../../../elements/EFVideo.js";
7
7
  import { EFImage } from "../../../elements/EFImage.js";
8
8
  import { EFText } from "../../../elements/EFText.js";
9
9
  import { ICONS, phosphorIcon } from "../../icons.js";
10
+ import { getElementTypeColor } from "../../theme.js";
10
11
  import "../TrimHandles.js";
11
12
  import { EFTimegroup } from "../../../elements/EFTimegroup.js";
12
13
  import { consume } from "@lit/context";
@@ -50,7 +51,6 @@ let TrackItem = class TrackItem$1 extends TWMixin(LitElement) {
50
51
  this.element = new EFTimegroup();
51
52
  this.pixelsPerMs = .04;
52
53
  this.enableTrim = false;
53
- this.useAbsolutePosition = false;
54
54
  }
55
55
  static {
56
56
  this.styles = [css`
@@ -64,11 +64,11 @@ let TrackItem = class TrackItem$1 extends TWMixin(LitElement) {
64
64
  }
65
65
 
66
66
  :host(:hover) .trim-container {
67
- box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.15);
67
+ box-shadow: inset 0 0 0 1px var(--ef-color-hover);
68
68
  }
69
69
 
70
70
  :host([data-focused]) .trim-container {
71
- box-shadow: inset 0 0 0 1px rgba(59, 130, 246, 0.6);
71
+ box-shadow: inset 0 0 0 1px var(--ef-color-primary);
72
72
  }
73
73
  `];
74
74
  }
@@ -88,9 +88,15 @@ let TrackItem = class TrackItem$1 extends TWMixin(LitElement) {
88
88
  return "unknown";
89
89
  }
90
90
  /**
91
- * Get color for element type
91
+ * Get color for element type using shared theme utility
92
92
  */
93
93
  getElementTypeColor() {
94
+ return getElementTypeColor(this.getElementType(), this);
95
+ }
96
+ /**
97
+ * @deprecated Use getElementTypeColor() instead
98
+ */
99
+ getElementTypeColorOld() {
94
100
  switch (this.getElementType()) {
95
101
  case "video": return "rgb(59, 130, 246)";
96
102
  case "audio": return "rgb(34, 197, 94)";
@@ -142,8 +148,7 @@ let TrackItem = class TrackItem$1 extends TWMixin(LitElement) {
142
148
  return parts.join(" • ");
143
149
  }
144
150
  get gutterStyles() {
145
- const startMs = this.useAbsolutePosition ? this.element.startTimeMs : this.element.startTimeWithinParentMs;
146
- const leftOffset = this.useAbsolutePosition ? startMs : startMs - this.element.sourceStartMs;
151
+ const leftOffset = this.element.startTimeMs;
147
152
  return {
148
153
  position: "relative",
149
154
  left: `${this.pixelsPerMs * leftOffset}px`,
@@ -151,21 +156,20 @@ let TrackItem = class TrackItem$1 extends TWMixin(LitElement) {
151
156
  };
152
157
  }
153
158
  get trimPortionStyles() {
154
- const leftOffset = this.useAbsolutePosition ? 0 : this.element.sourceStartMs;
155
159
  return {
156
160
  width: `${this.pixelsPerMs * this.element.durationMs}px`,
157
- left: `${this.pixelsPerMs * leftOffset}px`
161
+ left: "0px"
158
162
  };
159
163
  }
160
164
  handleTrimChange(e) {
161
- const { type, newValueMs } = e.detail;
162
- if (type === "start") this.element.trimStartMs = newValueMs;
163
- else this.element.trimEndMs = newValueMs;
165
+ const { type, value } = e.detail;
166
+ this.element.trimStartMs = value.startMs;
167
+ this.element.trimEndMs = value.endMs;
164
168
  this.dispatchEvent(new CustomEvent("track-trim-change", {
165
169
  detail: {
166
170
  elementId: this.element.id || "",
167
171
  type,
168
- newValueMs
172
+ value
169
173
  },
170
174
  bubbles: true,
171
175
  composed: true
@@ -210,6 +214,7 @@ let TrackItem = class TrackItem$1 extends TWMixin(LitElement) {
210
214
  ${this.animations()}
211
215
  ${this.contents()}
212
216
  ${this.enableTrim ? html`<ef-trim-handles
217
+ mode="track"
213
218
  element-id=${elementId}
214
219
  pixels-per-ms=${this.pixelsPerMs}
215
220
  trim-start-ms=${trimStartMs}
@@ -258,10 +263,6 @@ __decorate([property({
258
263
  type: Array,
259
264
  attribute: false
260
265
  })], TrackItem.prototype, "showSelectors", void 0);
261
- __decorate([property({
262
- type: Boolean,
263
- attribute: "use-absolute-position"
264
- })], TrackItem.prototype, "useAbsolutePosition", void 0);
265
266
  TrackItem = __decorate([customElement("ef-track-item")], TrackItem);
266
267
 
267
268
  //#endregion
@@ -1 +1 @@
1
- {"version":3,"file":"TrackItem.js","names":["host: LitElement","track: TrackItem","TrackItem"],"sources":["../../../../src/gui/timeline/tracks/TrackItem.ts"],"sourcesContent":["import { consume } from \"@lit/context\";\nimport {\n css,\n html,\n LitElement,\n nothing,\n type PropertyValueMap,\n type ReactiveController,\n type TemplateResult,\n} from \"lit\";\nimport { customElement, property } from \"lit/decorators.js\";\nimport { styleMap } from \"lit/directives/style-map.js\";\n\nimport {\n isEFTemporal,\n type TemporalMixinInterface,\n} from \"../../../elements/EFTemporal.js\";\nimport { EFTimegroup } from \"../../../elements/EFTimegroup.js\";\nimport { EFVideo } from \"../../../elements/EFVideo.js\";\nimport { EFAudio } from \"../../../elements/EFAudio.js\";\nimport { EFImage } from \"../../../elements/EFImage.js\";\nimport { EFText } from \"../../../elements/EFText.js\";\nimport { type FocusContext, focusContext } from \"../../focusContext.js\";\nimport { focusedElementContext } from \"../../focusedElementContext.js\";\nimport { TWMixin } from \"../../TWMixin.js\";\nimport { phosphorIcon, ICONS } from \"../../icons.js\";\nimport \"../TrimHandles.js\";\nimport type { TrimChangeDetail } from \"../TrimHandles.js\";\n\nclass ElementTrackController implements ReactiveController {\n private lastDuration = 0;\n private durationCheckFrame?: number;\n\n constructor(\n private host: LitElement,\n private track: TrackItem,\n ) {\n this.host.addController(this);\n }\n\n remove() {\n this.host.removeController(this);\n if (this.durationCheckFrame) {\n cancelAnimationFrame(this.durationCheckFrame);\n }\n }\n\n hostDisconnected() {\n this.host.removeController(this);\n if (this.durationCheckFrame) {\n cancelAnimationFrame(this.durationCheckFrame);\n }\n }\n\n hostConnected(): void {\n // Start watching for duration changes\n this.lastDuration = (this.host as any).durationMs ?? 0;\n this.checkDuration();\n }\n\n private checkDuration = () => {\n const currentDuration = (this.host as any).durationMs ?? 0;\n if (currentDuration !== this.lastDuration) {\n this.lastDuration = currentDuration;\n // Duration changed - trigger re-render of the track\n this.track.requestUpdate();\n }\n // Keep checking if duration is still 0 (waiting for media to load)\n if (currentDuration === 0) {\n this.durationCheckFrame = requestAnimationFrame(this.checkDuration);\n }\n };\n\n hostUpdated(): void {\n // TEMPORARILY DISABLED: This causes every TrackItem to re-render on every frame\n // during playback, even though TrackItem doesn't display currentTimeMs.\n // Duration changes are now handled separately via checkDuration()\n }\n}\n\nconst CommonEffectKeys = new Set([\n \"offset\",\n \"easing\",\n \"composite\",\n \"computedOffset\",\n]);\n\n@customElement(\"ef-track-item\")\nexport class TrackItem extends TWMixin(LitElement) {\n static styles = [\n css`\n :host {\n display: block;\n }\n .trim-container {\n position: relative;\n border-radius: 3px;\n transition: background-color 0.15s ease, box-shadow 0.15s ease;\n }\n \n :host(:hover) .trim-container {\n box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.15);\n }\n \n :host([data-focused]) .trim-container {\n box-shadow: inset 0 0 0 1px rgba(59, 130, 246, 0.6);\n }\n `,\n ];\n\n @consume({ context: focusContext, subscribe: true })\n focusContext?: FocusContext;\n\n @consume({ context: focusedElementContext, subscribe: true })\n focusedElement?: HTMLElement | null;\n\n get isFocused() {\n return this.element && this.focusContext?.focusedElement === this.element;\n }\n\n /**\n * Get element type for styling and icons\n */\n protected getElementType(): \"video\" | \"audio\" | \"image\" | \"text\" | \"timegroup\" | \"captions\" | \"unknown\" {\n // Check for captions element\n if ((this.element as any).tagName === \"EF-CAPTIONS\" || \n (this.element as any).tagName?.toLowerCase() === \"ef-captions\") {\n return \"captions\";\n }\n if (this.element instanceof EFVideo) return \"video\";\n if (this.element instanceof EFAudio) return \"audio\";\n if (this.element instanceof EFImage) return \"image\";\n if (this.element instanceof EFText) return \"text\";\n if (this.element instanceof EFTimegroup) return \"timegroup\";\n return \"unknown\";\n }\n\n /**\n * Get color for element type\n */\n protected getElementTypeColor(): string {\n const type = this.getElementType();\n switch (type) {\n case \"video\":\n return \"rgb(59, 130, 246)\"; // Blue\n case \"audio\":\n return \"rgb(34, 197, 94)\"; // Green\n case \"image\":\n return \"rgb(168, 85, 247)\"; // Purple\n case \"text\":\n return \"rgb(249, 115, 22)\"; // Orange\n case \"captions\":\n return \"rgb(34, 197, 94)\"; // Green (same as audio, but distinct usage)\n case \"timegroup\":\n return \"rgb(148, 163, 184)\"; // Gray\n default:\n return \"rgb(148, 163, 184)\"; // Gray\n }\n }\n\n /**\n * Get icon for element type\n */\n protected getElementIcon(): TemplateResult | typeof nothing {\n const type = this.getElementType();\n switch (type) {\n case \"video\":\n return phosphorIcon(ICONS.filmStrip, 14);\n case \"audio\":\n return phosphorIcon(ICONS.speakerHigh, 14);\n case \"image\":\n return phosphorIcon(ICONS.image, 14);\n case \"text\":\n return phosphorIcon(ICONS.textT, 14);\n case \"captions\":\n return phosphorIcon(ICONS.subtitles, 14);\n case \"timegroup\":\n return phosphorIcon(ICONS.filmSlate, 14);\n default:\n return nothing;\n }\n }\n\n /**\n * Format duration for display\n */\n protected formatDuration(ms: number): string {\n if (ms < 1000) {\n return `${Math.round(ms)}ms`;\n }\n const seconds = (ms / 1000).toFixed(1);\n return `${seconds}s`;\n }\n\n /**\n * Get tooltip text with element info\n */\n protected getTooltipText(): string {\n const elementId = (this.element as HTMLElement)?.id || \"\";\n const type = this.getElementType();\n const duration = this.formatDuration(this.element.durationMs ?? 0);\n const startTime = this.formatDuration(this.element.startTimeMs ?? 0);\n const endTime = this.formatDuration((this.element.startTimeMs ?? 0) + (this.element.durationMs ?? 0));\n \n const parts = [];\n if (elementId) parts.push(elementId);\n parts.push(`${type} • ${duration}`);\n if (this.element.startTimeMs > 0) {\n parts.push(`${startTime} → ${endTime}`);\n }\n \n // Add composition mode for timegroups\n if (type === \"timegroup\") {\n const mode = (this.element as any).mode || \"fixed\";\n parts.push(`mode: ${mode}`);\n }\n \n return parts.join(\" • \");\n }\n\n @property({ type: Object, attribute: false })\n element: TemporalMixinInterface & LitElement = new EFTimegroup();\n\n @property({ type: Number, attribute: \"pixels-per-ms\" })\n pixelsPerMs = 0.04;\n\n @property({ type: Boolean, attribute: \"enable-trim\" })\n enableTrim = false;\n\n @property({ type: Array, attribute: false })\n hideSelectors?: string[];\n\n @property({ type: Array, attribute: false })\n showSelectors?: string[];\n\n /**\n * When true, positions the track at the element's absolute start time\n * (startTimeMs) rather than relative to parent (startTimeWithinParentMs).\n * Used for flat row architectures where each element gets its own row.\n */\n @property({ type: Boolean, attribute: \"use-absolute-position\" })\n useAbsolutePosition = false;\n\n get gutterStyles() {\n const startMs = this.useAbsolutePosition\n ? this.element.startTimeMs\n : this.element.startTimeWithinParentMs;\n // When using absolute positioning, don't subtract sourceStartMs - the track container\n // should be positioned at the element's absolute start time in the timeline.\n // When using relative positioning, sourceStartMs is already accounted for in startTimeWithinParentMs.\n const leftOffset = this.useAbsolutePosition\n ? startMs\n : startMs - this.element.sourceStartMs;\n return {\n position: \"relative\",\n left: `${this.pixelsPerMs * leftOffset}px`,\n width: `${this.pixelsPerMs * (this.element.intrinsicDurationMs ?? this.element.durationMs)}px`,\n };\n }\n\n get trimPortionStyles() {\n // When using absolute positioning, the trim container should start at 0\n // relative to the gutter (which is already positioned at startTimeMs).\n // When using relative positioning, we need to offset by sourceStartMs\n // to show the trimmed portion correctly.\n const leftOffset = this.useAbsolutePosition\n ? 0\n : this.element.sourceStartMs;\n return {\n width: `${this.pixelsPerMs * this.element.durationMs}px`,\n left: `${this.pixelsPerMs * leftOffset}px`,\n };\n }\n\n protected handleTrimChange(e: CustomEvent<TrimChangeDetail>): void {\n const { type, newValueMs } = e.detail;\n\n if (type === \"start\") {\n this.element.trimStartMs = newValueMs;\n } else {\n this.element.trimEndMs = newValueMs;\n }\n\n this.dispatchEvent(\n new CustomEvent(\"track-trim-change\", {\n detail: {\n elementId: this.element.id || \"\",\n type,\n newValueMs,\n },\n bubbles: true,\n composed: true,\n }),\n );\n }\n\n contents(): TemplateResult | typeof nothing {\n return nothing;\n }\n\n animations() {\n // TEMPORARILY DISABLED: getAnimations() is expensive and called on every render\n // TODO: Cache animations or only compute when element structure changes\n return [];\n \n // const animations = this.element.getAnimations();\n // return animations.map((animation) => {\n // const effect = animation.effect;\n // if (!(effect instanceof KeyframeEffect)) {\n // return nothing;\n // }\n // const start = effect.getTiming().delay ?? 0;\n // const duration = effect.getTiming().duration;\n // if (duration === null) {\n // return nothing;\n // }\n // const keyframes = effect.getKeyframes();\n // const firstKeyframe = keyframes[0];\n // if (!firstKeyframe) {\n // return nothing;\n // }\n // const properties = new Set(Object.keys(firstKeyframe));\n // for (const key of CommonEffectKeys) {\n // properties.delete(key);\n // }\n\n // return html`<div\n // class=\"relative h-[5px] opacity-50\"\n // label=\"animation\"\n // style=${styleMap({\n // left: `${this.pixelsPerMs * start}px`,\n // width: `${this.pixelsPerMs * Number(duration)}px`,\n // backgroundColor: \"var(--filmstrip-animation-bg)\",\n // })}\n // >\n // ${effect.getKeyframes().map((keyframe) => {\n // return html`<div\n // class=\"absolute top-0 h-full w-1\"\n // style=${styleMap({\n // left: `${\n // this.pixelsPerMs * keyframe.computedOffset * Number(duration)\n // }px`,\n // backgroundColor: \"var(--filmstrip-keyframe-bg)\",\n // })}\n // ></div>`;\n // })}\n // </div>`;\n // });\n }\n\n renderChildren(): Array<TemplateResult<1> | typeof nothing> | typeof nothing {\n return nothing;\n }\n\n render() {\n const elementId = (this.element as HTMLElement).id || \"\";\n const trimStartMs = this.element.trimStartMs ?? 0;\n const trimEndMs = this.element.trimEndMs ?? 0;\n const intrinsicDurationMs =\n this.element.intrinsicDurationMs ?? this.element.durationMs;\n\n const typeColor = this.getElementTypeColor();\n \n return html`<div style=${styleMap(this.gutterStyles)}>\n <div\n ?data-focused=${this.isFocused}\n @mouseenter=${() => {\n if (this.focusContext) {\n this.focusContext.focusedElement = this.element;\n }\n }}\n @mouseleave=${() => {\n if (this.focusContext) {\n this.focusContext.focusedElement = null;\n }\n }}\n >\n <div\n ?data-focused=${this.isFocused}\n class=\"trim-container\"\n style=${styleMap({\n ...this.trimPortionStyles,\n height: \"var(--timeline-track-height, 22px)\",\n backgroundColor: this.isFocused\n ? \"rgba(59, 130, 246, 0.25)\"\n : \"rgba(30, 41, 59, 0.8)\",\n borderLeft: `3px solid ${typeColor}`,\n })}\n title=\"${this.getTooltipText()}\"\n >\n ${this.animations()}\n ${this.contents()}\n ${\n this.enableTrim\n ? html`<ef-trim-handles\n element-id=${elementId}\n pixels-per-ms=${this.pixelsPerMs}\n trim-start-ms=${trimStartMs}\n trim-end-ms=${trimEndMs}\n intrinsic-duration-ms=${intrinsicDurationMs}\n @trim-change=${this.handleTrimChange}\n ></ef-trim-handles>`\n : nothing\n }\n </div>\n </div>\n ${this.renderChildren()}\n </div>`;\n }\n\n protected trackController?: ElementTrackController;\n\n update(changedProperties: PropertyValueMap<any> | Map<PropertyKey, unknown>) {\n if (\n changedProperties.has(\"element\") &&\n this.element instanceof LitElement\n ) {\n this.trackController?.remove();\n this.trackController = new ElementTrackController(\n this.element,\n this,\n );\n }\n super.update(changedProperties);\n }\n}\n\ndeclare global {\n interface HTMLElementTagNameMap {\n \"ef-track-item\": TrackItem;\n }\n}\n\n"],"mappings":";;;;;;;;;;;;;;;;;AA6BA,IAAM,yBAAN,MAA2D;CAIzD,YACE,AAAQA,MACR,AAAQC,OACR;EAFQ;EACA;sBALa;6BA8BO;GAC5B,MAAM,kBAAmB,KAAK,KAAa,cAAc;AACzD,OAAI,oBAAoB,KAAK,cAAc;AACzC,SAAK,eAAe;AAEpB,SAAK,MAAM,eAAe;;AAG5B,OAAI,oBAAoB,EACtB,MAAK,qBAAqB,sBAAsB,KAAK,cAAc;;AAhCrE,OAAK,KAAK,cAAc,KAAK;;CAG/B,SAAS;AACP,OAAK,KAAK,iBAAiB,KAAK;AAChC,MAAI,KAAK,mBACP,sBAAqB,KAAK,mBAAmB;;CAIjD,mBAAmB;AACjB,OAAK,KAAK,iBAAiB,KAAK;AAChC,MAAI,KAAK,mBACP,sBAAqB,KAAK,mBAAmB;;CAIjD,gBAAsB;AAEpB,OAAK,eAAgB,KAAK,KAAa,cAAc;AACrD,OAAK,eAAe;;CAgBtB,cAAoB;;AAef,sBAAMC,oBAAkB,QAAQ,WAAW,CAAC;;;iBAqIF,IAAI,aAAa;qBAGlD;oBAGD;6BAcS;;;gBAxJN,CACd,GAAG;;;;;;;;;;;;;;;;;MAkBJ;;CAQD,IAAI,YAAY;AACd,SAAO,KAAK,WAAW,KAAK,cAAc,mBAAmB,KAAK;;;;;CAMpE,AAAU,iBAA8F;AAEtG,MAAK,KAAK,QAAgB,YAAY,iBACjC,KAAK,QAAgB,SAAS,aAAa,KAAK,cACnD,QAAO;AAET,MAAI,KAAK,mBAAmB,QAAS,QAAO;AAC5C,MAAI,KAAK,mBAAmB,QAAS,QAAO;AAC5C,MAAI,KAAK,mBAAmB,QAAS,QAAO;AAC5C,MAAI,KAAK,mBAAmB,OAAQ,QAAO;AAC3C,MAAI,KAAK,mBAAmB,YAAa,QAAO;AAChD,SAAO;;;;;CAMT,AAAU,sBAA8B;AAEtC,UADa,KAAK,gBAAgB,EAClC;GACE,KAAK,QACH,QAAO;GACT,KAAK,QACH,QAAO;GACT,KAAK,QACH,QAAO;GACT,KAAK,OACH,QAAO;GACT,KAAK,WACH,QAAO;GACT,KAAK,YACH,QAAO;GACT,QACE,QAAO;;;;;;CAOb,AAAU,iBAAkD;AAE1D,UADa,KAAK,gBAAgB,EAClC;GACE,KAAK,QACH,QAAO,aAAa,MAAM,WAAW,GAAG;GAC1C,KAAK,QACH,QAAO,aAAa,MAAM,aAAa,GAAG;GAC5C,KAAK,QACH,QAAO,aAAa,MAAM,OAAO,GAAG;GACtC,KAAK,OACH,QAAO,aAAa,MAAM,OAAO,GAAG;GACtC,KAAK,WACH,QAAO,aAAa,MAAM,WAAW,GAAG;GAC1C,KAAK,YACH,QAAO,aAAa,MAAM,WAAW,GAAG;GAC1C,QACE,QAAO;;;;;;CAOb,AAAU,eAAe,IAAoB;AAC3C,MAAI,KAAK,IACP,QAAO,GAAG,KAAK,MAAM,GAAG,CAAC;AAG3B,SAAO,IADU,KAAK,KAAM,QAAQ,EAAE,CACpB;;;;;CAMpB,AAAU,iBAAyB;EACjC,MAAM,YAAa,KAAK,SAAyB,MAAM;EACvD,MAAM,OAAO,KAAK,gBAAgB;EAClC,MAAM,WAAW,KAAK,eAAe,KAAK,QAAQ,cAAc,EAAE;EAClE,MAAM,YAAY,KAAK,eAAe,KAAK,QAAQ,eAAe,EAAE;EACpE,MAAM,UAAU,KAAK,gBAAgB,KAAK,QAAQ,eAAe,MAAM,KAAK,QAAQ,cAAc,GAAG;EAErG,MAAM,QAAQ,EAAE;AAChB,MAAI,UAAW,OAAM,KAAK,UAAU;AACpC,QAAM,KAAK,GAAG,KAAK,KAAK,WAAW;AACnC,MAAI,KAAK,QAAQ,cAAc,EAC7B,OAAM,KAAK,GAAG,UAAU,KAAK,UAAU;AAIzC,MAAI,SAAS,aAAa;GACxB,MAAM,OAAQ,KAAK,QAAgB,QAAQ;AAC3C,SAAM,KAAK,SAAS,OAAO;;AAG7B,SAAO,MAAM,KAAK,MAAM;;CA0B1B,IAAI,eAAe;EACjB,MAAM,UAAU,KAAK,sBACjB,KAAK,QAAQ,cACb,KAAK,QAAQ;EAIjB,MAAM,aAAa,KAAK,sBACpB,UACA,UAAU,KAAK,QAAQ;AAC3B,SAAO;GACL,UAAU;GACV,MAAM,GAAG,KAAK,cAAc,WAAW;GACvC,OAAO,GAAG,KAAK,eAAe,KAAK,QAAQ,uBAAuB,KAAK,QAAQ,YAAY;GAC5F;;CAGH,IAAI,oBAAoB;EAKtB,MAAM,aAAa,KAAK,sBACpB,IACA,KAAK,QAAQ;AACjB,SAAO;GACL,OAAO,GAAG,KAAK,cAAc,KAAK,QAAQ,WAAW;GACrD,MAAM,GAAG,KAAK,cAAc,WAAW;GACxC;;CAGH,AAAU,iBAAiB,GAAwC;EACjE,MAAM,EAAE,MAAM,eAAe,EAAE;AAE/B,MAAI,SAAS,QACX,MAAK,QAAQ,cAAc;MAE3B,MAAK,QAAQ,YAAY;AAG3B,OAAK,cACH,IAAI,YAAY,qBAAqB;GACnC,QAAQ;IACN,WAAW,KAAK,QAAQ,MAAM;IAC9B;IACA;IACD;GACD,SAAS;GACT,UAAU;GACX,CAAC,CACH;;CAGH,WAA4C;AAC1C,SAAO;;CAGT,aAAa;AAGX,SAAO,EAAE;;CA+CX,iBAA6E;AAC3E,SAAO;;CAGT,SAAS;EACP,MAAM,YAAa,KAAK,QAAwB,MAAM;EACtD,MAAM,cAAc,KAAK,QAAQ,eAAe;EAChD,MAAM,YAAY,KAAK,QAAQ,aAAa;EAC5C,MAAM,sBACJ,KAAK,QAAQ,uBAAuB,KAAK,QAAQ;EAEnD,MAAM,YAAY,KAAK,qBAAqB;AAE5C,SAAO,IAAI,cAAc,SAAS,KAAK,aAAa,CAAC;;wBAEjC,KAAK,UAAU;4BACX;AAClB,OAAI,KAAK,aACP,MAAK,aAAa,iBAAiB,KAAK;IAE1C;4BACkB;AAClB,OAAI,KAAK,aACP,MAAK,aAAa,iBAAiB;IAErC;;;0BAGgB,KAAK,UAAU;;kBAEvB,SAAS;GACf,GAAG,KAAK;GACR,QAAQ;GACR,iBAAiB,KAAK,YAClB,6BACA;GACJ,YAAY,aAAa;GAC1B,CAAC,CAAC;mBACM,KAAK,gBAAgB,CAAC;;YAE7B,KAAK,YAAY,CAAC;YAClB,KAAK,UAAU,CAAC;YAEhB,KAAK,aACD,IAAI;6BACS,UAAU;gCACP,KAAK,YAAY;gCACjB,YAAY;8BACd,UAAU;wCACA,oBAAoB;+BAC7B,KAAK,iBAAiB;qCAErC,QACL;;;QAGH,KAAK,gBAAgB,CAAC;;;CAM5B,OAAO,mBAAsE;AAC3E,MACE,kBAAkB,IAAI,UAAU,IAChC,KAAK,mBAAmB,YACxB;AACA,QAAK,iBAAiB,QAAQ;AAC9B,QAAK,kBAAkB,IAAI,uBACzB,KAAK,SACL,KACD;;AAEH,QAAM,OAAO,kBAAkB;;;YAzThC,QAAQ;CAAE,SAAS;CAAc,WAAW;CAAM,CAAC;YAGnD,QAAQ;CAAE,SAAS;CAAuB,WAAW;CAAM,CAAC;YA2G5D,SAAS;CAAE,MAAM;CAAQ,WAAW;CAAO,CAAC;YAG5C,SAAS;CAAE,MAAM;CAAQ,WAAW;CAAiB,CAAC;YAGtD,SAAS;CAAE,MAAM;CAAS,WAAW;CAAe,CAAC;YAGrD,SAAS;CAAE,MAAM;CAAO,WAAW;CAAO,CAAC;YAG3C,SAAS;CAAE,MAAM;CAAO,WAAW;CAAO,CAAC;YAQ3C,SAAS;CAAE,MAAM;CAAS,WAAW;CAAyB,CAAC;wBAzJjE,cAAc,gBAAgB"}
1
+ {"version":3,"file":"TrackItem.js","names":["host: LitElement","track: TrackItem","TrackItem"],"sources":["../../../../src/gui/timeline/tracks/TrackItem.ts"],"sourcesContent":["import { consume } from \"@lit/context\";\nimport {\n css,\n html,\n LitElement,\n nothing,\n type PropertyValueMap,\n type ReactiveController,\n type TemplateResult,\n} from \"lit\";\nimport { customElement, property } from \"lit/decorators.js\";\nimport { styleMap } from \"lit/directives/style-map.js\";\n\nimport { type TemporalMixinInterface } from \"../../../elements/EFTemporal.js\";\nimport { EFTimegroup } from \"../../../elements/EFTimegroup.js\";\nimport { EFVideo } from \"../../../elements/EFVideo.js\";\nimport { EFAudio } from \"../../../elements/EFAudio.js\";\nimport { EFImage } from \"../../../elements/EFImage.js\";\nimport { EFText } from \"../../../elements/EFText.js\";\nimport { type FocusContext, focusContext } from \"../../focusContext.js\";\nimport { focusedElementContext } from \"../../focusedElementContext.js\";\nimport { TWMixin } from \"../../TWMixin.js\";\nimport { phosphorIcon, ICONS } from \"../../icons.js\";\nimport \"../TrimHandles.js\";\nimport type { TrimChangeDetail } from \"../TrimHandles.js\";\nimport { getElementTypeColor } from \"../../theme.js\";\n\nclass ElementTrackController implements ReactiveController {\n private lastDuration = 0;\n private durationCheckFrame?: number;\n\n constructor(\n private host: LitElement,\n private track: TrackItem,\n ) {\n this.host.addController(this);\n }\n\n remove() {\n this.host.removeController(this);\n if (this.durationCheckFrame) {\n cancelAnimationFrame(this.durationCheckFrame);\n }\n }\n\n hostDisconnected() {\n this.host.removeController(this);\n if (this.durationCheckFrame) {\n cancelAnimationFrame(this.durationCheckFrame);\n }\n }\n\n hostConnected(): void {\n // Start watching for duration changes\n this.lastDuration = (this.host as any).durationMs ?? 0;\n this.checkDuration();\n }\n\n private checkDuration = () => {\n const currentDuration = (this.host as any).durationMs ?? 0;\n if (currentDuration !== this.lastDuration) {\n this.lastDuration = currentDuration;\n // Duration changed - trigger re-render of the track\n this.track.requestUpdate();\n }\n // Keep checking if duration is still 0 (waiting for media to load)\n if (currentDuration === 0) {\n this.durationCheckFrame = requestAnimationFrame(this.checkDuration);\n }\n };\n\n hostUpdated(): void {\n // TEMPORARILY DISABLED: This causes every TrackItem to re-render on every frame\n // during playback, even though TrackItem doesn't display currentTimeMs.\n // Duration changes are now handled separately via checkDuration()\n }\n}\n\n@customElement(\"ef-track-item\")\nexport class TrackItem extends TWMixin(LitElement) {\n static styles = [\n css`\n :host {\n display: block;\n }\n .trim-container {\n position: relative;\n border-radius: 3px;\n transition: background-color 0.15s ease, box-shadow 0.15s ease;\n }\n \n :host(:hover) .trim-container {\n box-shadow: inset 0 0 0 1px var(--ef-color-hover);\n }\n \n :host([data-focused]) .trim-container {\n box-shadow: inset 0 0 0 1px var(--ef-color-primary);\n }\n `,\n ];\n\n @consume({ context: focusContext, subscribe: true })\n focusContext?: FocusContext;\n\n @consume({ context: focusedElementContext, subscribe: true })\n focusedElement?: HTMLElement | null;\n\n get isFocused() {\n return this.element && this.focusContext?.focusedElement === this.element;\n }\n\n /**\n * Get element type for styling and icons\n */\n protected getElementType():\n | \"video\"\n | \"audio\"\n | \"image\"\n | \"text\"\n | \"timegroup\"\n | \"captions\"\n | \"unknown\" {\n // Check for captions element\n if (\n (this.element as any).tagName === \"EF-CAPTIONS\" ||\n (this.element as any).tagName?.toLowerCase() === \"ef-captions\"\n ) {\n return \"captions\";\n }\n if (this.element instanceof EFVideo) return \"video\";\n if (this.element instanceof EFAudio) return \"audio\";\n if (this.element instanceof EFImage) return \"image\";\n if (this.element instanceof EFText) return \"text\";\n if (this.element instanceof EFTimegroup) return \"timegroup\";\n return \"unknown\";\n }\n\n /**\n * Get color for element type using shared theme utility\n */\n protected getElementTypeColor(): string {\n const type = this.getElementType();\n return getElementTypeColor(type, this);\n }\n\n /**\n * @deprecated Use getElementTypeColor() instead\n */\n protected getElementTypeColorOld(): string {\n const type = this.getElementType();\n switch (type) {\n case \"video\":\n return \"rgb(59, 130, 246)\"; // Blue\n case \"audio\":\n return \"rgb(34, 197, 94)\"; // Green\n case \"image\":\n return \"rgb(168, 85, 247)\"; // Purple\n case \"text\":\n return \"rgb(249, 115, 22)\"; // Orange\n case \"captions\":\n return \"rgb(34, 197, 94)\"; // Green (same as audio, but distinct usage)\n case \"timegroup\":\n return \"rgb(148, 163, 184)\"; // Gray\n default:\n return \"rgb(148, 163, 184)\"; // Gray\n }\n }\n\n /**\n * Get icon for element type\n */\n protected getElementIcon(): TemplateResult | typeof nothing {\n const type = this.getElementType();\n switch (type) {\n case \"video\":\n return phosphorIcon(ICONS.filmStrip, 14);\n case \"audio\":\n return phosphorIcon(ICONS.speakerHigh, 14);\n case \"image\":\n return phosphorIcon(ICONS.image, 14);\n case \"text\":\n return phosphorIcon(ICONS.textT, 14);\n case \"captions\":\n return phosphorIcon(ICONS.subtitles, 14);\n case \"timegroup\":\n return phosphorIcon(ICONS.filmSlate, 14);\n default:\n return nothing;\n }\n }\n\n /**\n * Format duration for display\n */\n protected formatDuration(ms: number): string {\n if (ms < 1000) {\n return `${Math.round(ms)}ms`;\n }\n const seconds = (ms / 1000).toFixed(1);\n return `${seconds}s`;\n }\n\n /**\n * Get tooltip text with element info\n */\n protected getTooltipText(): string {\n const elementId = (this.element as HTMLElement)?.id || \"\";\n const type = this.getElementType();\n const duration = this.formatDuration(this.element.durationMs ?? 0);\n const startTime = this.formatDuration(this.element.startTimeMs ?? 0);\n const endTime = this.formatDuration(\n (this.element.startTimeMs ?? 0) + (this.element.durationMs ?? 0),\n );\n\n const parts = [];\n if (elementId) parts.push(elementId);\n parts.push(`${type} • ${duration}`);\n if (this.element.startTimeMs > 0) {\n parts.push(`${startTime} → ${endTime}`);\n }\n\n // Add composition mode for timegroups\n if (type === \"timegroup\") {\n const mode = (this.element as any).mode || \"fixed\";\n parts.push(`mode: ${mode}`);\n }\n\n return parts.join(\" • \");\n }\n\n @property({ type: Object, attribute: false })\n element: TemporalMixinInterface & LitElement = new EFTimegroup();\n\n @property({ type: Number, attribute: \"pixels-per-ms\" })\n pixelsPerMs = 0.04;\n\n @property({ type: Boolean, attribute: \"enable-trim\" })\n enableTrim = false;\n\n @property({ type: Array, attribute: false })\n hideSelectors?: string[];\n\n @property({ type: Array, attribute: false })\n showSelectors?: string[];\n\n get gutterStyles() {\n // Always use absolute positioning (startTimeMs) for flat row architecture.\n // startTimeMs already includes the cumulative position from all parent timegroups.\n const startMs = this.element.startTimeMs;\n const leftOffset = startMs;\n\n return {\n position: \"relative\",\n left: `${this.pixelsPerMs * leftOffset}px`,\n width: `${this.pixelsPerMs * (this.element.intrinsicDurationMs ?? this.element.durationMs)}px`,\n };\n }\n\n get trimPortionStyles() {\n // The trim container starts at 0 relative to the gutter,\n // which is already positioned at the element's absolute startTimeMs.\n return {\n width: `${this.pixelsPerMs * this.element.durationMs}px`,\n left: \"0px\",\n };\n }\n\n protected handleTrimChange(e: CustomEvent<TrimChangeDetail>): void {\n const { type, value } = e.detail;\n\n this.element.trimStartMs = value.startMs;\n this.element.trimEndMs = value.endMs;\n\n this.dispatchEvent(\n new CustomEvent(\"track-trim-change\", {\n detail: {\n elementId: this.element.id || \"\",\n type,\n value,\n },\n bubbles: true,\n composed: true,\n }),\n );\n }\n\n contents(): TemplateResult | typeof nothing {\n return nothing;\n }\n\n animations() {\n // TEMPORARILY DISABLED: getAnimations() is expensive and called on every render\n // TODO: Cache animations or only compute when element structure changes\n return [];\n\n // const animations = this.element.getAnimations();\n // return animations.map((animation) => {\n // const effect = animation.effect;\n // if (!(effect instanceof KeyframeEffect)) {\n // return nothing;\n // }\n // const start = effect.getTiming().delay ?? 0;\n // const duration = effect.getTiming().duration;\n // if (duration === null) {\n // return nothing;\n // }\n // const keyframes = effect.getKeyframes();\n // const firstKeyframe = keyframes[0];\n // if (!firstKeyframe) {\n // return nothing;\n // }\n // const properties = new Set(Object.keys(firstKeyframe));\n // for (const key of CommonEffectKeys) {\n // properties.delete(key);\n // }\n\n // return html`<div\n // class=\"relative h-[5px] opacity-50\"\n // label=\"animation\"\n // style=${styleMap({\n // left: `${this.pixelsPerMs * start}px`,\n // width: `${this.pixelsPerMs * Number(duration)}px`,\n // backgroundColor: \"var(--filmstrip-animation-bg)\",\n // })}\n // >\n // ${effect.getKeyframes().map((keyframe) => {\n // return html`<div\n // class=\"absolute top-0 h-full w-1\"\n // style=${styleMap({\n // left: `${\n // this.pixelsPerMs * keyframe.computedOffset * Number(duration)\n // }px`,\n // backgroundColor: \"var(--filmstrip-keyframe-bg)\",\n // })}\n // ></div>`;\n // })}\n // </div>`;\n // });\n }\n\n renderChildren(): Array<TemplateResult<1> | typeof nothing> | typeof nothing {\n return nothing;\n }\n\n render() {\n const elementId = (this.element as HTMLElement).id || \"\";\n const trimStartMs = this.element.trimStartMs ?? 0;\n const trimEndMs = this.element.trimEndMs ?? 0;\n const intrinsicDurationMs =\n this.element.intrinsicDurationMs ?? this.element.durationMs;\n\n const typeColor = this.getElementTypeColor();\n\n return html`<div style=${styleMap(this.gutterStyles)}>\n <div\n ?data-focused=${this.isFocused}\n @mouseenter=${() => {\n if (this.focusContext) {\n this.focusContext.focusedElement = this.element;\n }\n }}\n @mouseleave=${() => {\n if (this.focusContext) {\n this.focusContext.focusedElement = null;\n }\n }}\n >\n <div\n ?data-focused=${this.isFocused}\n class=\"trim-container\"\n style=${styleMap({\n ...this.trimPortionStyles,\n height: \"var(--timeline-track-height, 22px)\",\n backgroundColor: this.isFocused\n ? \"rgba(59, 130, 246, 0.25)\"\n : \"rgba(30, 41, 59, 0.8)\",\n borderLeft: `3px solid ${typeColor}`,\n })}\n title=\"${this.getTooltipText()}\"\n >\n ${this.animations()}\n ${this.contents()}\n ${\n this.enableTrim\n ? html`<ef-trim-handles\n mode=\"track\"\n element-id=${elementId}\n pixels-per-ms=${this.pixelsPerMs}\n trim-start-ms=${trimStartMs}\n trim-end-ms=${trimEndMs}\n intrinsic-duration-ms=${intrinsicDurationMs}\n @trim-change=${this.handleTrimChange}\n ></ef-trim-handles>`\n : nothing\n }\n </div>\n </div>\n ${this.renderChildren()}\n </div>`;\n }\n\n protected trackController?: ElementTrackController;\n\n update(changedProperties: PropertyValueMap<any> | Map<PropertyKey, unknown>) {\n if (\n changedProperties.has(\"element\") &&\n this.element instanceof LitElement\n ) {\n this.trackController?.remove();\n this.trackController = new ElementTrackController(this.element, this);\n }\n super.update(changedProperties);\n }\n}\n\ndeclare global {\n interface HTMLElementTagNameMap {\n \"ef-track-item\": TrackItem;\n }\n}\n"],"mappings":"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"}
@@ -1,13 +1,13 @@
1
1
  import { TrackItem } from "./TrackItem.js";
2
- import "../../../elements/EFThumbnailStrip.js";
3
- import * as lit39 from "lit";
2
+ import "./EFThumbnailStrip.js";
3
+ import * as lit40 from "lit";
4
4
  import * as lit_html36 from "lit-html";
5
5
  import * as lit_html_directives_ref_js5 from "lit-html/directives/ref.js";
6
6
 
7
7
  //#region src/gui/timeline/tracks/VideoTrack.d.ts
8
8
  declare class EFVideoTrack extends TrackItem {
9
9
  #private;
10
- static styles: lit39.CSSResult[];
10
+ static styles: lit40.CSSResult[];
11
11
  audioCanvasRef: lit_html_directives_ref_js5.Ref<HTMLCanvasElement>;
12
12
  private _timelineState?;
13
13
  private _waveformData;
@@ -3,7 +3,7 @@ import { EFVideo } from "../../../elements/EFVideo.js";
3
3
  import { TrackItem } from "./TrackItem.js";
4
4
  import { extractWaveformData } from "./waveformUtils.js";
5
5
  import { timelineStateContext } from "../timelineStateContext.js";
6
- import "../../../elements/EFThumbnailStrip.js";
6
+ import "./EFThumbnailStrip.js";
7
7
  import { consume } from "@lit/context";
8
8
  import { css, html, nothing } from "lit";
9
9
  import { customElement, state } from "lit/decorators.js";
@@ -35,19 +35,14 @@ let EFVideoTrack = class EFVideoTrack$1 extends TrackItem {
35
35
  position: relative;
36
36
  flex: 0 0 ${THUMBNAIL_HEIGHT}px;
37
37
  height: ${THUMBNAIL_HEIGHT}px;
38
- }
39
- ef-thumbnail-strip {
40
- height: 100%;
41
- border: none;
42
- border-radius: 0;
43
- background: transparent;
38
+ background: var(--ef-color-bg-inset);
44
39
  }
45
40
  .audio-section {
46
41
  position: relative;
47
42
  flex: 0 0 ${AUDIO_SECTION_HEIGHT}px;
48
43
  height: ${AUDIO_SECTION_HEIGHT}px;
49
- background: rgba(0, 0, 0, 0.3);
50
- border-top: 1px solid rgba(255, 255, 255, 0.1);
44
+ background: var(--ef-color-bg-elevated);
45
+ border-top: 1px solid var(--ef-color-border-subtle);
51
46
  overflow: hidden;
52
47
  }
53
48
  .audio-section-canvas {
@@ -145,7 +140,7 @@ let EFVideoTrack = class EFVideoTrack$1 extends TrackItem {
145
140
  const centerY = height / 2;
146
141
  const halfHeight = height / 2 - 1;
147
142
  const pixelsPerSample = width / sampleCount;
148
- ctx.fillStyle = "rgb(74, 222, 128)";
143
+ ctx.fillStyle = getComputedStyle(this).getPropertyValue("--ef-color-success").trim() || "rgb(74, 222, 128)";
149
144
  ctx.globalAlpha = .9;
150
145
  ctx.beginPath();
151
146
  for (let i = 0; i <= sampleCount; i++) {
@@ -168,7 +163,7 @@ let EFVideoTrack = class EFVideoTrack$1 extends TrackItem {
168
163
  ctx.closePath();
169
164
  ctx.fill();
170
165
  ctx.globalAlpha = .3;
171
- ctx.strokeStyle = "rgb(74, 222, 128)";
166
+ ctx.strokeStyle = getComputedStyle(this).getPropertyValue("--ef-color-success").trim() || "rgb(74, 222, 128)";
172
167
  ctx.lineWidth = 1;
173
168
  ctx.beginPath();
174
169
  ctx.moveTo(0, centerY);
@@ -223,7 +218,7 @@ let EFVideoTrack = class EFVideoTrack$1 extends TrackItem {
223
218
  style=${styleMap({
224
219
  ...this.trimPortionStyles,
225
220
  height: `${trackHeight}px`,
226
- backgroundColor: this.isFocused ? "rgba(59, 130, 246, 0.25)" : "rgba(30, 41, 59, 0.8)",
221
+ backgroundColor: this.isFocused ? "color-mix(in srgb, var(--ef-color-primary) 25%, transparent)" : "var(--ef-color-bg-inset)",
227
222
  borderLeft: `3px solid ${typeColor}`,
228
223
  borderRadius: "3px"
229
224
  })}
@@ -231,8 +226,10 @@ let EFVideoTrack = class EFVideoTrack$1 extends TrackItem {
231
226
  <div class="video-content">
232
227
  <div class="thumbnail-section">
233
228
  <ef-thumbnail-strip
234
- .targetElement=${video}
235
- .useIntrinsicDuration=${true}
229
+ .targetElement=${this.element}
230
+ thumbnail-height=${THUMBNAIL_HEIGHT}
231
+ thumbnail-spacing-px="48"
232
+ pixels-per-ms=${this.pixelsPerMs}
236
233
  ></ef-thumbnail-strip>
237
234
  </div>
238
235
  ${hasAudioSection ? html`<div class="audio-section">
@@ -1 +1 @@
1
- {"version":3,"file":"VideoTrack.js","names":["EFVideoTrack","#checkAndLoadAudioWaveform","#lastSrc","#abortController","#scheduleRender","#renderRequested","#renderAudioOverlay","#drawAudioWaveform","#getTrackHeight"],"sources":["../../../../src/gui/timeline/tracks/VideoTrack.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 { styleMap } from \"lit/directives/style-map.js\";\nimport { EFVideo } from \"../../../elements/EFVideo.js\";\nimport \"../../../elements/EFThumbnailStrip.js\";\n// TrackItem must be pre-loaded before this module is imported\n// See preloadTracks.ts for the initialization sequence\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 for virtual rendering */\nconst VIRTUAL_RENDER_PADDING_PX = 100;\n\n/** Height of thumbnail section */\nconst THUMBNAIL_HEIGHT = 24;\n/** Height of audio section when present */\nconst AUDIO_SECTION_HEIGHT = 14;\n\n@customElement(\"ef-video-track\")\nexport class EFVideoTrack extends TrackItem {\n static override styles = [\n ...TrackItem.styles,\n css`\n .video-content {\n display: flex;\n flex-direction: column;\n height: 100%;\n }\n .thumbnail-section {\n position: relative;\n flex: 0 0 ${THUMBNAIL_HEIGHT}px;\n height: ${THUMBNAIL_HEIGHT}px;\n }\n ef-thumbnail-strip {\n height: 100%;\n border: none;\n border-radius: 0;\n background: transparent;\n }\n .audio-section {\n position: relative;\n flex: 0 0 ${AUDIO_SECTION_HEIGHT}px;\n height: ${AUDIO_SECTION_HEIGHT}px;\n background: rgba(0, 0, 0, 0.3);\n border-top: 1px solid rgba(255, 255, 255, 0.1);\n overflow: hidden;\n }\n .audio-section-canvas {\n position: absolute;\n top: 0;\n height: 100%;\n }\n `,\n ];\n\n audioCanvasRef = createRef<HTMLCanvasElement>();\n\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 _hasAudio = false;\n\n #lastSrc: string | null = null;\n #abortController: AbortController | null = null;\n #renderRequested = false;\n\n /**\n * Check if video has audio and load waveform data\n */\n async #checkAndLoadAudioWaveform(): Promise<void> {\n const video = this.element as EFVideo;\n const src = video?.src;\n\n if (!src || src === this.#lastSrc) {\n return;\n }\n\n this.#lastSrc = src;\n this._hasAudio = false;\n this._waveformData = null;\n\n // Cancel any in-progress load\n this.#abortController?.abort();\n this.#abortController = new AbortController();\n\n try {\n // Wait for media engine to determine if video has audio\n if (video.mediaEngineTask) {\n const mediaEngine = await video.mediaEngineTask.taskComplete;\n if (mediaEngine?.audioRendition) {\n this._hasAudio = true;\n\n // Load waveform data\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 }\n }\n } catch (error) {\n if (!(error instanceof DOMException && error.name === \"AbortError\")) {\n // Silently fail - audio overlay is optional\n }\n }\n }\n\n #scheduleRender(): void {\n if (this.#renderRequested) return;\n this.#renderRequested = true;\n\n requestAnimationFrame(() => {\n this.#renderRequested = false;\n this.#renderAudioOverlay();\n });\n }\n\n #renderAudioOverlay(): void {\n const canvas = this.audioCanvasRef.value;\n const waveformData = this._waveformData;\n\n if (!canvas || !waveformData || !this._hasAudio) return;\n\n const video = this.element as EFVideo;\n const durationMs = video.durationMs ?? 0;\n if (durationMs === 0) return;\n\n const pixelsPerMs = this._timelineState?.pixelsPerMs ?? this.pixelsPerMs;\n const trackWidthPx = durationMs * pixelsPerMs;\n const trackStartMs = video.startTimeMs ?? 0;\n const trackStartPx = trackStartMs * pixelsPerMs;\n\n // Get scroll/viewport info\n const scrollLeft = this._timelineState?.viewportScrollLeft ?? 0;\n const viewportWidth = this._timelineState?.viewportWidth ?? 800;\n\n // Calculate visible region\n const visibleLeftPx = scrollLeft - VIRTUAL_RENDER_PADDING_PX;\n const visibleRightPx = scrollLeft + viewportWidth + VIRTUAL_RENDER_PADDING_PX;\n const trackEndPx = trackStartPx + trackWidthPx;\n\n // Check visibility\n if (trackEndPx < visibleLeftPx || trackStartPx > visibleRightPx) {\n canvas.style.display = \"none\";\n return;\n }\n canvas.style.display = \"block\";\n\n // Calculate visible portion within track\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 = AUDIO_SECTION_HEIGHT;\n const dpr = window.devicePixelRatio || 1;\n\n // Set canvas size\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 canvas.style.left = `${visibleStartInTrack}px`;\n canvas.style.width = `${visibleWidthPx}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 time range to render\n const sourceInMs = video.sourceStartMs ?? 0;\n const timeStartMs = sourceInMs + visibleStartInTrack / pixelsPerMs;\n const timeEndMs = sourceInMs + visibleEndInTrack / pixelsPerMs;\n\n // Draw waveform in dedicated section\n this.#drawAudioWaveform(ctx, waveformData, visibleWidthPx, height, timeStartMs, timeEndMs);\n }\n\n #drawAudioWaveform(\n ctx: CanvasRenderingContext2D,\n waveformData: WaveformData,\n width: number,\n height: number,\n startMs: number,\n endMs: number,\n ): void {\n const { peaks, samplesPerSecond } = waveformData;\n\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) - 1;\n const pixelsPerSample = width / sampleCount;\n\n // Draw filled waveform\n ctx.fillStyle = \"rgb(74, 222, 128)\";\n ctx.globalAlpha = 0.9;\n ctx.beginPath();\n\n // Draw top half (max values) left to right\n for (let i = 0; i <= sampleCount; i++) {\n const sampleIndex = startSample + i;\n const peakIndex = sampleIndex * 2;\n if (peakIndex + 1 >= peaks.length) break;\n\n const maxValue = peaks[peakIndex + 1] ?? 0;\n const px = 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 if (peakIndex >= peaks.length) continue;\n\n const minValue = peaks[peakIndex] ?? 0;\n const px = 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 = \"rgb(74, 222, 128)\";\n ctx.lineWidth = 1;\n ctx.beginPath();\n ctx.moveTo(0, centerY);\n ctx.lineTo(width, centerY);\n ctx.stroke();\n\n ctx.globalAlpha = 1;\n }\n\n connectedCallback(): void {\n super.connectedCallback();\n this.#checkAndLoadAudioWaveform();\n }\n\n disconnectedCallback(): void {\n super.disconnectedCallback();\n this.#abortController?.abort();\n }\n\n updated(changedProperties: Map<string | number | symbol, unknown>): void {\n super.updated(changedProperties);\n\n const video = this.element as EFVideo;\n if (video?.src !== this.#lastSrc) {\n this.#checkAndLoadAudioWaveform();\n }\n\n if (changedProperties.has(\"_timelineState\") || changedProperties.has(\"_waveformData\")) {\n this.#scheduleRender();\n }\n\n // Always schedule render after update\n if (this._waveformData) {\n this.#scheduleRender();\n }\n }\n\n /**\n * Get the total track height based on whether audio is present\n */\n #getTrackHeight(): number {\n if (this._hasAudio && this._waveformData) {\n return THUMBNAIL_HEIGHT + AUDIO_SECTION_HEIGHT;\n }\n return THUMBNAIL_HEIGHT;\n }\n\n override render() {\n const video = this.element as EFVideo;\n const elementId = (this.element as HTMLElement).id || \"\";\n\n // Don't render thumbnail strip until we have a valid EFVideo element\n if (!(video instanceof EFVideo)) {\n return html``;\n }\n const trimStartMs = this.element.trimStartMs ?? 0;\n const trimEndMs = this.element.trimEndMs ?? 0;\n const intrinsicDurationMs =\n this.element.intrinsicDurationMs ?? this.element.durationMs;\n\n const trackHeight = this.#getTrackHeight();\n const hasAudioSection = this._hasAudio && this._waveformData;\n\n const typeColor = this.getElementTypeColor();\n \n return html`<div style=${styleMap(this.gutterStyles)}>\n <div\n ?data-focused=${this.isFocused}\n @mouseenter=${() => {\n if (this.focusContext) {\n this.focusContext.focusedElement = this.element;\n }\n }}\n @mouseleave=${() => {\n if (this.focusContext) {\n this.focusContext.focusedElement = null;\n }\n }}\n >\n <div\n ?data-focused=${this.isFocused}\n class=\"trim-container\"\n style=${styleMap({\n ...this.trimPortionStyles,\n height: `${trackHeight}px`,\n backgroundColor: this.isFocused\n ? \"rgba(59, 130, 246, 0.25)\"\n : \"rgba(30, 41, 59, 0.8)\",\n borderLeft: `3px solid ${typeColor}`,\n borderRadius: \"3px\",\n })}\n >\n <div class=\"video-content\">\n <div class=\"thumbnail-section\">\n <ef-thumbnail-strip\n .targetElement=${video}\n .useIntrinsicDuration=${true}\n ></ef-thumbnail-strip>\n </div>\n ${hasAudioSection\n ? html`<div class=\"audio-section\">\n <canvas ${ref(this.audioCanvasRef)} class=\"audio-section-canvas\"></canvas>\n </div>`\n : nothing\n }\n </div>\n ${\n this.enableTrim\n ? html`<ef-trim-handles\n element-id=${elementId}\n pixels-per-ms=${this.pixelsPerMs}\n trim-start-ms=${trimStartMs}\n trim-end-ms=${trimEndMs}\n intrinsic-duration-ms=${intrinsicDurationMs}\n @trim-change=${this.handleTrimChange}\n ></ef-trim-handles>`\n : nothing\n }\n </div>\n </div>\n ${this.renderChildren()}\n </div>`;\n }\n}\n\ndeclare global {\n interface HTMLElementTagNameMap {\n \"ef-video-track\": EFVideoTrack;\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"VideoTrack.js","names":["EFVideoTrack","#checkAndLoadAudioWaveform","#lastSrc","#abortController","#scheduleRender","#renderRequested","#renderAudioOverlay","#drawAudioWaveform","#getTrackHeight"],"sources":["../../../../src/gui/timeline/tracks/VideoTrack.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 { styleMap } from \"lit/directives/style-map.js\";\nimport { EFVideo } from \"../../../elements/EFVideo.js\";\n\n// TrackItem must be pre-loaded before this module is imported\n// See preloadTracks.ts for the initialization sequence\nimport { TrackItem } from \"./TrackItem.js\";\nimport { extractWaveformData, type WaveformData } from \"./waveformUtils.js\";\nimport {\n timelineStateContext,\n type TimelineState,\n} from \"../timelineStateContext.js\";\nimport \"./EFThumbnailStrip.js\";\n\n/** Padding for virtual rendering */\nconst VIRTUAL_RENDER_PADDING_PX = 100;\n\n/** Height of thumbnail section */\nconst THUMBNAIL_HEIGHT = 24;\n/** Height of audio section when present */\nconst AUDIO_SECTION_HEIGHT = 14;\n\n@customElement(\"ef-video-track\")\nexport class EFVideoTrack extends TrackItem {\n static override styles = [\n ...TrackItem.styles,\n css`\n .video-content {\n display: flex;\n flex-direction: column;\n height: 100%;\n }\n .thumbnail-section {\n position: relative;\n flex: 0 0 ${THUMBNAIL_HEIGHT}px;\n height: ${THUMBNAIL_HEIGHT}px;\n background: var(--ef-color-bg-inset);\n }\n .audio-section {\n position: relative;\n flex: 0 0 ${AUDIO_SECTION_HEIGHT}px;\n height: ${AUDIO_SECTION_HEIGHT}px;\n background: var(--ef-color-bg-elevated);\n border-top: 1px solid var(--ef-color-border-subtle);\n overflow: hidden;\n }\n .audio-section-canvas {\n position: absolute;\n top: 0;\n height: 100%;\n }\n `,\n ];\n\n audioCanvasRef = createRef<HTMLCanvasElement>();\n\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 _hasAudio = false;\n\n #lastSrc: string | null = null;\n #abortController: AbortController | null = null;\n #renderRequested = false;\n\n /**\n * Check if video has audio and load waveform data\n */\n async #checkAndLoadAudioWaveform(): Promise<void> {\n const video = this.element as EFVideo;\n const src = video?.src;\n\n if (!src || src === this.#lastSrc) {\n return;\n }\n\n this.#lastSrc = src;\n this._hasAudio = false;\n this._waveformData = null;\n\n // Cancel any in-progress load\n this.#abortController?.abort();\n this.#abortController = new AbortController();\n\n try {\n // Wait for media engine to determine if video has audio\n if (video.mediaEngineTask) {\n const mediaEngine = await video.mediaEngineTask.taskComplete;\n if (mediaEngine?.audioRendition) {\n this._hasAudio = true;\n\n // Load waveform data\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 }\n }\n } catch (error) {\n if (!(error instanceof DOMException && error.name === \"AbortError\")) {\n // Silently fail - audio overlay is optional\n }\n }\n }\n\n #scheduleRender(): void {\n if (this.#renderRequested) return;\n this.#renderRequested = true;\n\n requestAnimationFrame(() => {\n this.#renderRequested = false;\n this.#renderAudioOverlay();\n });\n }\n\n #renderAudioOverlay(): void {\n const canvas = this.audioCanvasRef.value;\n const waveformData = this._waveformData;\n\n if (!canvas || !waveformData || !this._hasAudio) return;\n\n const video = this.element as EFVideo;\n const durationMs = video.durationMs ?? 0;\n if (durationMs === 0) return;\n\n const pixelsPerMs = this._timelineState?.pixelsPerMs ?? this.pixelsPerMs;\n const trackWidthPx = durationMs * pixelsPerMs;\n const trackStartMs = video.startTimeMs ?? 0;\n const trackStartPx = trackStartMs * pixelsPerMs;\n\n // Get scroll/viewport info\n const scrollLeft = this._timelineState?.viewportScrollLeft ?? 0;\n const viewportWidth = this._timelineState?.viewportWidth ?? 800;\n\n // Calculate visible region\n const visibleLeftPx = scrollLeft - VIRTUAL_RENDER_PADDING_PX;\n const visibleRightPx =\n scrollLeft + viewportWidth + VIRTUAL_RENDER_PADDING_PX;\n const trackEndPx = trackStartPx + trackWidthPx;\n\n // Check visibility\n if (trackEndPx < visibleLeftPx || trackStartPx > visibleRightPx) {\n canvas.style.display = \"none\";\n return;\n }\n canvas.style.display = \"block\";\n\n // Calculate visible portion within track\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 = AUDIO_SECTION_HEIGHT;\n const dpr = window.devicePixelRatio || 1;\n\n // Set canvas size\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 canvas.style.left = `${visibleStartInTrack}px`;\n canvas.style.width = `${visibleWidthPx}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 time range to render\n const sourceInMs = video.sourceStartMs ?? 0;\n const timeStartMs = sourceInMs + visibleStartInTrack / pixelsPerMs;\n const timeEndMs = sourceInMs + visibleEndInTrack / pixelsPerMs;\n\n // Draw waveform in dedicated section\n this.#drawAudioWaveform(\n ctx,\n waveformData,\n visibleWidthPx,\n height,\n timeStartMs,\n timeEndMs,\n );\n }\n\n #drawAudioWaveform(\n ctx: CanvasRenderingContext2D,\n waveformData: WaveformData,\n width: number,\n height: number,\n startMs: number,\n endMs: number,\n ): void {\n const { peaks, samplesPerSecond } = waveformData;\n\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 - 1;\n const pixelsPerSample = width / sampleCount;\n\n // Draw filled waveform\n ctx.fillStyle =\n getComputedStyle(this).getPropertyValue(\"--ef-color-success\").trim() ||\n \"rgb(74, 222, 128)\";\n ctx.globalAlpha = 0.9;\n ctx.beginPath();\n\n // Draw top half (max values) left to right\n for (let i = 0; i <= sampleCount; i++) {\n const sampleIndex = startSample + i;\n const peakIndex = sampleIndex * 2;\n if (peakIndex + 1 >= peaks.length) break;\n\n const maxValue = peaks[peakIndex + 1] ?? 0;\n const px = 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 if (peakIndex >= peaks.length) continue;\n\n const minValue = peaks[peakIndex] ?? 0;\n const px = 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 =\n getComputedStyle(this).getPropertyValue(\"--ef-color-success\").trim() ||\n \"rgb(74, 222, 128)\";\n ctx.lineWidth = 1;\n ctx.beginPath();\n ctx.moveTo(0, centerY);\n ctx.lineTo(width, centerY);\n ctx.stroke();\n\n ctx.globalAlpha = 1;\n }\n\n connectedCallback(): void {\n super.connectedCallback();\n this.#checkAndLoadAudioWaveform();\n }\n\n disconnectedCallback(): void {\n super.disconnectedCallback();\n this.#abortController?.abort();\n }\n\n updated(changedProperties: Map<string | number | symbol, unknown>): void {\n super.updated(changedProperties);\n\n const video = this.element as EFVideo;\n if (video?.src !== this.#lastSrc) {\n this.#checkAndLoadAudioWaveform();\n }\n\n if (\n changedProperties.has(\"_timelineState\") ||\n changedProperties.has(\"_waveformData\")\n ) {\n this.#scheduleRender();\n }\n\n // Always schedule render after update\n if (this._waveformData) {\n this.#scheduleRender();\n }\n }\n\n /**\n * Get the total track height based on whether audio is present\n */\n #getTrackHeight(): number {\n if (this._hasAudio && this._waveformData) {\n return THUMBNAIL_HEIGHT + AUDIO_SECTION_HEIGHT;\n }\n return THUMBNAIL_HEIGHT;\n }\n\n override render() {\n const video = this.element as EFVideo;\n const elementId = (this.element as HTMLElement).id || \"\";\n\n // Don't render thumbnail strip until we have a valid EFVideo element\n if (!(video instanceof EFVideo)) {\n return html``;\n }\n const trimStartMs = this.element.trimStartMs ?? 0;\n const trimEndMs = this.element.trimEndMs ?? 0;\n const intrinsicDurationMs =\n this.element.intrinsicDurationMs ?? this.element.durationMs;\n\n const trackHeight = this.#getTrackHeight();\n const hasAudioSection = this._hasAudio && this._waveformData;\n\n const typeColor = this.getElementTypeColor();\n\n return html`<div style=${styleMap(this.gutterStyles)}>\n <div\n ?data-focused=${this.isFocused}\n @mouseenter=${() => {\n if (this.focusContext) {\n this.focusContext.focusedElement = this.element;\n }\n }}\n @mouseleave=${() => {\n if (this.focusContext) {\n this.focusContext.focusedElement = null;\n }\n }}\n >\n <div\n ?data-focused=${this.isFocused}\n class=\"trim-container\"\n style=${styleMap({\n ...this.trimPortionStyles,\n height: `${trackHeight}px`,\n backgroundColor: this.isFocused\n ? \"color-mix(in srgb, var(--ef-color-primary) 25%, transparent)\"\n : \"var(--ef-color-bg-inset)\",\n borderLeft: `3px solid ${typeColor}`,\n borderRadius: \"3px\",\n })}\n >\n <div class=\"video-content\">\n <div class=\"thumbnail-section\">\n <ef-thumbnail-strip\n .targetElement=${this.element}\n thumbnail-height=${THUMBNAIL_HEIGHT}\n thumbnail-spacing-px=\"48\"\n pixels-per-ms=${this.pixelsPerMs}\n ></ef-thumbnail-strip>\n </div>\n ${\n hasAudioSection\n ? html`<div class=\"audio-section\">\n <canvas ${ref(this.audioCanvasRef)} class=\"audio-section-canvas\"></canvas>\n </div>`\n : nothing\n }\n </div>\n ${\n this.enableTrim\n ? html`<ef-trim-handles\n element-id=${elementId}\n pixels-per-ms=${this.pixelsPerMs}\n trim-start-ms=${trimStartMs}\n trim-end-ms=${trimEndMs}\n intrinsic-duration-ms=${intrinsicDurationMs}\n @trim-change=${this.handleTrimChange}\n ></ef-trim-handles>`\n : nothing\n }\n </div>\n </div>\n ${this.renderChildren()}\n </div>`;\n }\n}\n\ndeclare global {\n interface HTMLElementTagNameMap {\n \"ef-video-track\": EFVideoTrack;\n }\n}\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"WaveformTrack.js","names":["EFWaveformTrack"],"sources":["../../../../src/gui/timeline/tracks/WaveformTrack.ts"],"sourcesContent":["import { html, nothing, type TemplateResult } from \"lit\";\nimport { customElement } from \"lit/decorators.js\";\nimport { phosphorIcon, ICONS } from \"../../icons.js\";\n// TrackItem must be pre-loaded before this module is imported\n// See preloadTracks.ts for the initialization sequence\nimport { TrackItem } from \"./TrackItem.js\";\n\n@customElement(\"ef-waveform-track\")\nexport class EFWaveformTrack extends TrackItem {\n contents() {\n return phosphorIcon(ICONS.waveform);\n }\n\n renderChildren(): Array<TemplateResult<1> | typeof nothing> | typeof nothing {\n return nothing;\n }\n}\n\ndeclare global {\n interface HTMLElementTagNameMap {\n \"ef-waveform-track\": EFWaveformTrack;\n }\n}\n\n"],"mappings":";;;;;;;AAQO,4BAAMA,0BAAwB,UAAU;CAC7C,WAAW;AACT,SAAO,aAAa,MAAM,SAAS;;CAGrC,iBAA6E;AAC3E,SAAO;;;8BAPV,cAAc,oBAAoB"}
1
+ {"version":3,"file":"WaveformTrack.js","names":["EFWaveformTrack"],"sources":["../../../../src/gui/timeline/tracks/WaveformTrack.ts"],"sourcesContent":["import { nothing, type TemplateResult } from \"lit\";\nimport { customElement } from \"lit/decorators.js\";\nimport { phosphorIcon, ICONS } from \"../../icons.js\";\n// TrackItem must be pre-loaded before this module is imported\n// See preloadTracks.ts for the initialization sequence\nimport { TrackItem } from \"./TrackItem.js\";\n\n@customElement(\"ef-waveform-track\")\nexport class EFWaveformTrack extends TrackItem {\n contents() {\n return phosphorIcon(ICONS.waveform);\n }\n\n renderChildren(): Array<TemplateResult<1> | typeof nothing> | typeof nothing {\n return nothing;\n }\n}\n\ndeclare global {\n interface HTMLElementTagNameMap {\n \"ef-waveform-track\": EFWaveformTrack;\n }\n}\n"],"mappings":";;;;;;;AAQO,4BAAMA,0BAAwB,UAAU;CAC7C,WAAW;AACT,SAAO,aAAa,MAAM,SAAS;;CAGrC,iBAA6E;AAC3E,SAAO;;;8BAPV,cAAc,oBAAoB"}
@@ -1 +1 @@
1
- {"version":3,"file":"renderTrackChildren.js","names":[],"sources":["../../../../src/gui/timeline/tracks/renderTrackChildren.ts"],"sourcesContent":["import { html, nothing, type TemplateResult } from \"lit\";\nimport { EFAudio } from \"../../../elements/EFAudio.js\";\nimport {\n EFCaptions,\n EFCaptionsActiveWord,\n} from \"../../../elements/EFCaptions.js\";\nimport { EFImage } from \"../../../elements/EFImage.js\";\nimport { EFText } from \"../../../elements/EFText.js\";\nimport { EFTextSegment } from \"../../../elements/EFTextSegment.js\";\nimport { EFTimegroup } from \"../../../elements/EFTimegroup.js\";\nimport { EFVideo } from \"../../../elements/EFVideo.js\";\nimport { EFWaveform } from \"../../../elements/EFWaveform.js\";\nimport { shouldRenderElement } from \"../../hierarchy/EFHierarchyItem.js\";\n\n// NOTE: Track components are NOT imported here to avoid circular dependencies.\n// They must be pre-loaded elsewhere before this module is used.\n// The custom elements (ef-audio-track, ef-video-track, etc.) are rendered\n// via Lit templates which will work as long as the elements are registered.\n// See preloadTracks.ts for the track component initialization.\n\nexport function renderTrackChildren(\n children: Element[],\n pixelsPerMs: number,\n hideSelectors?: string[],\n showSelectors?: string[],\n skipRootFiltering = false,\n enableTrim = false,\n useAbsolutePosition = false,\n): Array<TemplateResult<1> | typeof nothing> {\n return children.map((child) => {\n if (\n !skipRootFiltering &&\n !shouldRenderElement(child, hideSelectors, showSelectors)\n ) {\n return nothing;\n }\n\n if (child instanceof EFTimegroup) {\n return html`<ef-timegroup-track\n .element=${child}\n pixels-per-ms=${pixelsPerMs}\n ?enable-trim=${enableTrim}\n ?use-absolute-position=${useAbsolutePosition}\n .hideSelectors=${hideSelectors}\n .showSelectors=${showSelectors}\n >\n </ef-timegroup-track>`;\n }\n if (child instanceof EFImage) {\n return html`<ef-image-track\n .element=${child}\n pixels-per-ms=${pixelsPerMs}\n ?enable-trim=${enableTrim}\n ?use-absolute-position=${useAbsolutePosition}\n .hideSelectors=${hideSelectors}\n .showSelectors=${showSelectors}\n ></ef-image-track>`;\n }\n if (child instanceof EFAudio) {\n return html`<ef-audio-track\n .element=${child}\n pixels-per-ms=${pixelsPerMs}\n ?enable-trim=${enableTrim}\n ?use-absolute-position=${useAbsolutePosition}\n .hideSelectors=${hideSelectors}\n .showSelectors=${showSelectors}\n ></ef-audio-track>`;\n }\n if (child instanceof EFVideo) {\n return html`<ef-video-track\n .element=${child}\n pixels-per-ms=${pixelsPerMs}\n ?enable-trim=${enableTrim}\n ?use-absolute-position=${useAbsolutePosition}\n .hideSelectors=${hideSelectors}\n .showSelectors=${showSelectors}\n ></ef-video-track>`;\n }\n if (child instanceof EFCaptions) {\n return html`<ef-captions-track\n .element=${child}\n pixels-per-ms=${pixelsPerMs}\n ?enable-trim=${enableTrim}\n ?use-absolute-position=${useAbsolutePosition}\n .hideSelectors=${hideSelectors}\n .showSelectors=${showSelectors}\n ></ef-captions-track>`;\n }\n if (child instanceof EFCaptionsActiveWord) {\n return html`<ef-captions-active-word-track\n .element=${child}\n pixels-per-ms=${pixelsPerMs}\n ?enable-trim=${enableTrim}\n ?use-absolute-position=${useAbsolutePosition}\n .hideSelectors=${hideSelectors}\n .showSelectors=${showSelectors}\n ></ef-captions-active-word-track>`;\n }\n if (child instanceof EFText) {\n return html`<ef-text-track\n .element=${child}\n pixels-per-ms=${pixelsPerMs}\n ?enable-trim=${enableTrim}\n ?use-absolute-position=${useAbsolutePosition}\n .hideSelectors=${hideSelectors}\n .showSelectors=${showSelectors}\n ></ef-text-track>`;\n }\n if (child instanceof EFTextSegment) {\n return html`<ef-text-segment-track\n .element=${child}\n pixels-per-ms=${pixelsPerMs}\n ?enable-trim=${enableTrim}\n ?use-absolute-position=${useAbsolutePosition}\n .hideSelectors=${hideSelectors}\n .showSelectors=${showSelectors}\n ></ef-text-segment-track>`;\n }\n if (child.tagName === \"EF-CAPTIONS-SEGMENT\") {\n return html`<ef-captions-segment-track\n .element=${child}\n pixels-per-ms=${pixelsPerMs}\n ?enable-trim=${enableTrim}\n ?use-absolute-position=${useAbsolutePosition}\n .hideSelectors=${hideSelectors}\n .showSelectors=${showSelectors}\n ></ef-captions-segment-track>`;\n }\n if (child.tagName === \"EF-CAPTIONS-BEFORE-ACTIVE-WORD\") {\n return html`<ef-captions-before-word-track\n .element=${child}\n pixels-per-ms=${pixelsPerMs}\n ?enable-trim=${enableTrim}\n ?use-absolute-position=${useAbsolutePosition}\n .hideSelectors=${hideSelectors}\n .showSelectors=${showSelectors}\n ></ef-captions-before-word-track>`;\n }\n if (child.tagName === \"EF-CAPTIONS-AFTER-ACTIVE-WORD\") {\n return html`<ef-captions-after-word-track\n .element=${child}\n pixels-per-ms=${pixelsPerMs}\n ?enable-trim=${enableTrim}\n ?use-absolute-position=${useAbsolutePosition}\n .hideSelectors=${hideSelectors}\n .showSelectors=${showSelectors}\n ></ef-captions-after-word-track>`;\n }\n if (child instanceof EFWaveform) {\n return html`<ef-waveform-track\n .element=${child}\n pixels-per-ms=${pixelsPerMs}\n ?enable-trim=${enableTrim}\n ?use-absolute-position=${useAbsolutePosition}\n .hideSelectors=${hideSelectors}\n .showSelectors=${showSelectors}\n ></ef-waveform-track>`;\n }\n return nothing;\n });\n}\n\n"],"mappings":";;;;;;;;;;;;AAoBA,SAAgB,oBACd,UACA,aACA,eACA,eACA,oBAAoB,OACpB,aAAa,OACb,sBAAsB,OACqB;AAC3C,QAAO,SAAS,KAAK,UAAU;AAC7B,MACE,CAAC,qBACD,CAAC,oBAAoB,OAAO,eAAe,cAAc,CAEzD,QAAO;AAGT,MAAI,iBAAiB,YACnB,QAAO,IAAI;mBACE,MAAM;wBACD,YAAY;uBACb,WAAW;iCACD,oBAAoB;yBAC5B,cAAc;yBACd,cAAc;;;AAInC,MAAI,iBAAiB,QACnB,QAAO,IAAI;mBACE,MAAM;wBACD,YAAY;uBACb,WAAW;iCACD,oBAAoB;yBAC5B,cAAc;yBACd,cAAc;;AAGnC,MAAI,iBAAiB,QACnB,QAAO,IAAI;mBACE,MAAM;wBACD,YAAY;uBACb,WAAW;iCACD,oBAAoB;yBAC5B,cAAc;yBACd,cAAc;;AAGnC,MAAI,iBAAiB,QACnB,QAAO,IAAI;mBACE,MAAM;wBACD,YAAY;uBACb,WAAW;iCACD,oBAAoB;yBAC5B,cAAc;yBACd,cAAc;;AAGnC,MAAI,iBAAiB,WACnB,QAAO,IAAI;mBACE,MAAM;wBACD,YAAY;uBACb,WAAW;iCACD,oBAAoB;yBAC5B,cAAc;yBACd,cAAc;;AAGnC,MAAI,iBAAiB,qBACnB,QAAO,IAAI;mBACE,MAAM;wBACD,YAAY;uBACb,WAAW;iCACD,oBAAoB;yBAC5B,cAAc;yBACd,cAAc;;AAGnC,MAAI,iBAAiB,OACnB,QAAO,IAAI;mBACE,MAAM;wBACD,YAAY;uBACb,WAAW;iCACD,oBAAoB;yBAC5B,cAAc;yBACd,cAAc;;AAGnC,MAAI,iBAAiB,cACnB,QAAO,IAAI;mBACE,MAAM;wBACD,YAAY;uBACb,WAAW;iCACD,oBAAoB;yBAC5B,cAAc;yBACd,cAAc;;AAGnC,MAAI,MAAM,YAAY,sBACpB,QAAO,IAAI;mBACE,MAAM;wBACD,YAAY;uBACb,WAAW;iCACD,oBAAoB;yBAC5B,cAAc;yBACd,cAAc;;AAGnC,MAAI,MAAM,YAAY,iCACpB,QAAO,IAAI;mBACE,MAAM;wBACD,YAAY;uBACb,WAAW;iCACD,oBAAoB;yBAC5B,cAAc;yBACd,cAAc;;AAGnC,MAAI,MAAM,YAAY,gCACpB,QAAO,IAAI;mBACE,MAAM;wBACD,YAAY;uBACb,WAAW;iCACD,oBAAoB;yBAC5B,cAAc;yBACd,cAAc;;AAGnC,MAAI,iBAAiB,WACnB,QAAO,IAAI;mBACE,MAAM;wBACD,YAAY;uBACb,WAAW;iCACD,oBAAoB;yBAC5B,cAAc;yBACd,cAAc;;AAGnC,SAAO;GACP"}
1
+ {"version":3,"file":"renderTrackChildren.js","names":[],"sources":["../../../../src/gui/timeline/tracks/renderTrackChildren.ts"],"sourcesContent":["import { html, nothing, type TemplateResult } from \"lit\";\nimport { EFAudio } from \"../../../elements/EFAudio.js\";\nimport {\n EFCaptions,\n EFCaptionsActiveWord,\n} from \"../../../elements/EFCaptions.js\";\nimport { EFImage } from \"../../../elements/EFImage.js\";\nimport { EFText } from \"../../../elements/EFText.js\";\nimport { EFTextSegment } from \"../../../elements/EFTextSegment.js\";\nimport { EFTimegroup } from \"../../../elements/EFTimegroup.js\";\nimport { EFVideo } from \"../../../elements/EFVideo.js\";\nimport { EFWaveform } from \"../../../elements/EFWaveform.js\";\nimport { shouldRenderElement } from \"../../hierarchy/EFHierarchyItem.js\";\n\n// NOTE: Track components are NOT imported here to avoid circular dependencies.\n// They must be pre-loaded elsewhere before this module is used.\n// The custom elements (ef-audio-track, ef-video-track, etc.) are rendered\n// via Lit templates which will work as long as the elements are registered.\n// See preloadTracks.ts for the track component initialization.\n\nexport function renderTrackChildren(\n children: Element[],\n pixelsPerMs: number,\n hideSelectors?: string[],\n showSelectors?: string[],\n skipRootFiltering = false,\n enableTrim = false,\n useAbsolutePosition = false,\n): Array<TemplateResult<1> | typeof nothing> {\n return children.map((child) => {\n if (\n !skipRootFiltering &&\n !shouldRenderElement(child, hideSelectors, showSelectors)\n ) {\n return nothing;\n }\n\n if (child instanceof EFTimegroup) {\n return html`<ef-timegroup-track\n .element=${child}\n pixels-per-ms=${pixelsPerMs}\n ?enable-trim=${enableTrim}\n ?use-absolute-position=${useAbsolutePosition}\n .hideSelectors=${hideSelectors}\n .showSelectors=${showSelectors}\n >\n </ef-timegroup-track>`;\n }\n if (child instanceof EFImage) {\n return html`<ef-image-track\n .element=${child}\n pixels-per-ms=${pixelsPerMs}\n ?enable-trim=${enableTrim}\n ?use-absolute-position=${useAbsolutePosition}\n .hideSelectors=${hideSelectors}\n .showSelectors=${showSelectors}\n ></ef-image-track>`;\n }\n if (child instanceof EFAudio) {\n return html`<ef-audio-track\n .element=${child}\n pixels-per-ms=${pixelsPerMs}\n ?enable-trim=${enableTrim}\n ?use-absolute-position=${useAbsolutePosition}\n .hideSelectors=${hideSelectors}\n .showSelectors=${showSelectors}\n ></ef-audio-track>`;\n }\n if (child instanceof EFVideo) {\n return html`<ef-video-track\n .element=${child}\n pixels-per-ms=${pixelsPerMs}\n ?enable-trim=${enableTrim}\n ?use-absolute-position=${useAbsolutePosition}\n .hideSelectors=${hideSelectors}\n .showSelectors=${showSelectors}\n ></ef-video-track>`;\n }\n if (child instanceof EFCaptions) {\n return html`<ef-captions-track\n .element=${child}\n pixels-per-ms=${pixelsPerMs}\n ?enable-trim=${enableTrim}\n ?use-absolute-position=${useAbsolutePosition}\n .hideSelectors=${hideSelectors}\n .showSelectors=${showSelectors}\n ></ef-captions-track>`;\n }\n if (child instanceof EFCaptionsActiveWord) {\n return html`<ef-captions-active-word-track\n .element=${child}\n pixels-per-ms=${pixelsPerMs}\n ?enable-trim=${enableTrim}\n ?use-absolute-position=${useAbsolutePosition}\n .hideSelectors=${hideSelectors}\n .showSelectors=${showSelectors}\n ></ef-captions-active-word-track>`;\n }\n if (child instanceof EFText) {\n return html`<ef-text-track\n .element=${child}\n pixels-per-ms=${pixelsPerMs}\n ?enable-trim=${enableTrim}\n ?use-absolute-position=${useAbsolutePosition}\n .hideSelectors=${hideSelectors}\n .showSelectors=${showSelectors}\n ></ef-text-track>`;\n }\n if (child instanceof EFTextSegment) {\n return html`<ef-text-segment-track\n .element=${child}\n pixels-per-ms=${pixelsPerMs}\n ?enable-trim=${enableTrim}\n ?use-absolute-position=${useAbsolutePosition}\n .hideSelectors=${hideSelectors}\n .showSelectors=${showSelectors}\n ></ef-text-segment-track>`;\n }\n if (child.tagName === \"EF-CAPTIONS-SEGMENT\") {\n return html`<ef-captions-segment-track\n .element=${child}\n pixels-per-ms=${pixelsPerMs}\n ?enable-trim=${enableTrim}\n ?use-absolute-position=${useAbsolutePosition}\n .hideSelectors=${hideSelectors}\n .showSelectors=${showSelectors}\n ></ef-captions-segment-track>`;\n }\n if (child.tagName === \"EF-CAPTIONS-BEFORE-ACTIVE-WORD\") {\n return html`<ef-captions-before-word-track\n .element=${child}\n pixels-per-ms=${pixelsPerMs}\n ?enable-trim=${enableTrim}\n ?use-absolute-position=${useAbsolutePosition}\n .hideSelectors=${hideSelectors}\n .showSelectors=${showSelectors}\n ></ef-captions-before-word-track>`;\n }\n if (child.tagName === \"EF-CAPTIONS-AFTER-ACTIVE-WORD\") {\n return html`<ef-captions-after-word-track\n .element=${child}\n pixels-per-ms=${pixelsPerMs}\n ?enable-trim=${enableTrim}\n ?use-absolute-position=${useAbsolutePosition}\n .hideSelectors=${hideSelectors}\n .showSelectors=${showSelectors}\n ></ef-captions-after-word-track>`;\n }\n if (child instanceof EFWaveform) {\n return html`<ef-waveform-track\n .element=${child}\n pixels-per-ms=${pixelsPerMs}\n ?enable-trim=${enableTrim}\n ?use-absolute-position=${useAbsolutePosition}\n .hideSelectors=${hideSelectors}\n .showSelectors=${showSelectors}\n ></ef-waveform-track>`;\n }\n return nothing;\n });\n}\n"],"mappings":";;;;;;;;;;;;AAoBA,SAAgB,oBACd,UACA,aACA,eACA,eACA,oBAAoB,OACpB,aAAa,OACb,sBAAsB,OACqB;AAC3C,QAAO,SAAS,KAAK,UAAU;AAC7B,MACE,CAAC,qBACD,CAAC,oBAAoB,OAAO,eAAe,cAAc,CAEzD,QAAO;AAGT,MAAI,iBAAiB,YACnB,QAAO,IAAI;mBACE,MAAM;wBACD,YAAY;uBACb,WAAW;iCACD,oBAAoB;yBAC5B,cAAc;yBACd,cAAc;;;AAInC,MAAI,iBAAiB,QACnB,QAAO,IAAI;mBACE,MAAM;wBACD,YAAY;uBACb,WAAW;iCACD,oBAAoB;yBAC5B,cAAc;yBACd,cAAc;;AAGnC,MAAI,iBAAiB,QACnB,QAAO,IAAI;mBACE,MAAM;wBACD,YAAY;uBACb,WAAW;iCACD,oBAAoB;yBAC5B,cAAc;yBACd,cAAc;;AAGnC,MAAI,iBAAiB,QACnB,QAAO,IAAI;mBACE,MAAM;wBACD,YAAY;uBACb,WAAW;iCACD,oBAAoB;yBAC5B,cAAc;yBACd,cAAc;;AAGnC,MAAI,iBAAiB,WACnB,QAAO,IAAI;mBACE,MAAM;wBACD,YAAY;uBACb,WAAW;iCACD,oBAAoB;yBAC5B,cAAc;yBACd,cAAc;;AAGnC,MAAI,iBAAiB,qBACnB,QAAO,IAAI;mBACE,MAAM;wBACD,YAAY;uBACb,WAAW;iCACD,oBAAoB;yBAC5B,cAAc;yBACd,cAAc;;AAGnC,MAAI,iBAAiB,OACnB,QAAO,IAAI;mBACE,MAAM;wBACD,YAAY;uBACb,WAAW;iCACD,oBAAoB;yBAC5B,cAAc;yBACd,cAAc;;AAGnC,MAAI,iBAAiB,cACnB,QAAO,IAAI;mBACE,MAAM;wBACD,YAAY;uBACb,WAAW;iCACD,oBAAoB;yBAC5B,cAAc;yBACd,cAAc;;AAGnC,MAAI,MAAM,YAAY,sBACpB,QAAO,IAAI;mBACE,MAAM;wBACD,YAAY;uBACb,WAAW;iCACD,oBAAoB;yBAC5B,cAAc;yBACd,cAAc;;AAGnC,MAAI,MAAM,YAAY,iCACpB,QAAO,IAAI;mBACE,MAAM;wBACD,YAAY;uBACb,WAAW;iCACD,oBAAoB;yBAC5B,cAAc;yBACd,cAAc;;AAGnC,MAAI,MAAM,YAAY,gCACpB,QAAO,IAAI;mBACE,MAAM;wBACD,YAAY;uBACb,WAAW;iCACD,oBAAoB;yBAC5B,cAAc;yBACd,cAAc;;AAGnC,MAAI,iBAAiB,WACnB,QAAO,IAAI;mBACE,MAAM;wBACD,YAAY;uBACb,WAAW;iCACD,oBAAoB;yBAC5B,cAAc;yBACd,cAAc;;AAGnC,SAAO;GACP"}
@@ -1,7 +1,7 @@
1
1
  //#region src/gui/timeline/tracks/waveformUtils.ts
2
2
  /**
3
3
  * Waveform extraction utilities for DAW-style audio visualization.
4
- *
4
+ *
5
5
  * Extracts min/max peak pairs from audio data at a given resolution.
6
6
  * Designed for timeline visualization where we need to see amplitude
7
7
  * overview across the entire audio duration.
@@ -1 +1 @@
1
- {"version":3,"file":"waveformUtils.js","names":["audioBuffer: AudioBuffer","waveformData: WaveformData"],"sources":["../../../../src/gui/timeline/tracks/waveformUtils.ts"],"sourcesContent":["/**\n * Waveform extraction utilities for DAW-style audio visualization.\n * \n * Extracts min/max peak pairs from audio data at a given resolution.\n * Designed for timeline visualization where we need to see amplitude\n * overview across the entire audio duration.\n */\n\n/** Samples per second for waveform data - balances resolution vs. data size */\nexport const WAVEFORM_SAMPLES_PER_SECOND = 100;\n\n/** Waveform peak data: alternating min/max values normalized to [-1, 1] */\nexport interface WaveformData {\n /** Peak data: [min0, max0, min1, max1, ...] normalized to [-1, 1] */\n peaks: Float32Array;\n /** Duration of the audio in milliseconds */\n durationMs: number;\n /** Samples per second (for interpreting peaks array) */\n samplesPerSecond: number;\n}\n\n/** Simple cache for waveform data keyed by audio URL */\nconst waveformCache = new Map<string, WaveformData>();\n\n/**\n * Extract waveform peak data from an audio URL.\n * Uses Web Audio API to decode and analyze the audio.\n * Results are cached by URL.\n */\nexport async function extractWaveformData(\n audioUrl: string,\n signal?: AbortSignal,\n): Promise<WaveformData | null> {\n // Check cache first\n const cached = waveformCache.get(audioUrl);\n if (cached) {\n return cached;\n }\n\n try {\n // Fetch the audio file\n const response = await fetch(audioUrl, { signal });\n if (!response.ok) {\n console.warn(`Failed to fetch audio for waveform: ${response.status}`);\n return null;\n }\n\n const arrayBuffer = await response.arrayBuffer();\n signal?.throwIfAborted();\n\n // Decode audio data\n const audioContext = new OfflineAudioContext(1, 1, 44100);\n let audioBuffer: AudioBuffer;\n \n try {\n audioBuffer = await audioContext.decodeAudioData(arrayBuffer);\n } catch (decodeError) {\n console.warn(\"Failed to decode audio for waveform:\", decodeError);\n return null;\n }\n\n signal?.throwIfAborted();\n\n // Extract peaks from the decoded audio\n const peaks = extractPeaksFromBuffer(audioBuffer, WAVEFORM_SAMPLES_PER_SECOND);\n const durationMs = audioBuffer.duration * 1000;\n\n const waveformData: WaveformData = {\n peaks,\n durationMs,\n samplesPerSecond: WAVEFORM_SAMPLES_PER_SECOND,\n };\n\n // Cache the result\n waveformCache.set(audioUrl, waveformData);\n\n return waveformData;\n } catch (error) {\n if (error instanceof DOMException && error.name === \"AbortError\") {\n throw error;\n }\n console.warn(\"Error extracting waveform data:\", error);\n return null;\n }\n}\n\n/**\n * Extract min/max peaks from an AudioBuffer.\n * Returns Float32Array with alternating [min, max, min, max, ...] values.\n */\nfunction extractPeaksFromBuffer(\n buffer: AudioBuffer,\n samplesPerSecond: number,\n): Float32Array {\n const channelData = buffer.getChannelData(0); // Use first channel\n const sampleRate = buffer.sampleRate;\n const duration = buffer.duration;\n \n // Calculate how many samples to output\n const outputSamples = Math.ceil(duration * samplesPerSecond);\n \n // Each output sample has min and max\n const peaks = new Float32Array(outputSamples * 2);\n \n // Samples per output window\n const samplesPerWindow = Math.floor(sampleRate / samplesPerSecond);\n \n for (let i = 0; i < outputSamples; i++) {\n const startSample = i * samplesPerWindow;\n const endSample = Math.min(startSample + samplesPerWindow, channelData.length);\n \n let min = 0;\n let max = 0;\n \n for (let j = startSample; j < endSample; j++) {\n const sample = channelData[j] ?? 0;\n if (sample < min) min = sample;\n if (sample > max) max = sample;\n }\n \n // Store as alternating min/max pairs\n peaks[i * 2] = min;\n peaks[i * 2 + 1] = max;\n }\n \n return peaks;\n}\n\n/**\n * Render waveform data to a canvas context.\n * Draws a filled waveform path centered vertically.\n */\nexport function renderWaveformToCanvas(\n ctx: CanvasRenderingContext2D,\n waveformData: WaveformData,\n x: number,\n y: number,\n width: number,\n height: number,\n color: string,\n startMs: number = 0,\n endMs?: number,\n): void {\n const { peaks, durationMs, samplesPerSecond } = waveformData;\n const actualEndMs = endMs ?? durationMs;\n \n // Calculate which samples to render\n const startSample = Math.floor((startMs / 1000) * samplesPerSecond);\n const endSample = Math.ceil((actualEndMs / 1000) * samplesPerSecond);\n const sampleCount = endSample - startSample;\n \n if (sampleCount <= 0) return;\n \n const centerY = y + height / 2;\n const halfHeight = height / 2;\n const pixelsPerSample = width / sampleCount;\n \n ctx.fillStyle = color;\n ctx.beginPath();\n \n // Draw top half (max values) left to right\n for (let i = 0; i < sampleCount; i++) {\n const sampleIndex = startSample + i;\n const peakIndex = sampleIndex * 2;\n const maxValue = peaks[peakIndex + 1] ?? 0;\n \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 - 1; i >= 0; i--) {\n const sampleIndex = startSample + i;\n const peakIndex = sampleIndex * 2;\n const minValue = peaks[peakIndex] ?? 0;\n \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\n/**\n * Clear waveform cache (useful for testing or memory management)\n */\nexport function clearWaveformCache(): void {\n waveformCache.clear();\n}\n"],"mappings":";;;;;;;;;AASA,MAAa,8BAA8B;;AAa3C,MAAM,gCAAgB,IAAI,KAA2B;;;;;;AAOrD,eAAsB,oBACpB,UACA,QAC8B;CAE9B,MAAM,SAAS,cAAc,IAAI,SAAS;AAC1C,KAAI,OACF,QAAO;AAGT,KAAI;EAEF,MAAM,WAAW,MAAM,MAAM,UAAU,EAAE,QAAQ,CAAC;AAClD,MAAI,CAAC,SAAS,IAAI;AAChB,WAAQ,KAAK,uCAAuC,SAAS,SAAS;AACtE,UAAO;;EAGT,MAAM,cAAc,MAAM,SAAS,aAAa;AAChD,UAAQ,gBAAgB;EAGxB,MAAM,eAAe,IAAI,oBAAoB,GAAG,GAAG,MAAM;EACzD,IAAIA;AAEJ,MAAI;AACF,iBAAc,MAAM,aAAa,gBAAgB,YAAY;WACtD,aAAa;AACpB,WAAQ,KAAK,wCAAwC,YAAY;AACjE,UAAO;;AAGT,UAAQ,gBAAgB;EAMxB,MAAMC,eAA6B;GACjC,OAJY,uBAAuB,aAAa,4BAA4B;GAK5E,YAJiB,YAAY,WAAW;GAKxC,kBAAkB;GACnB;AAGD,gBAAc,IAAI,UAAU,aAAa;AAEzC,SAAO;UACA,OAAO;AACd,MAAI,iBAAiB,gBAAgB,MAAM,SAAS,aAClD,OAAM;AAER,UAAQ,KAAK,mCAAmC,MAAM;AACtD,SAAO;;;;;;;AAQX,SAAS,uBACP,QACA,kBACc;CACd,MAAM,cAAc,OAAO,eAAe,EAAE;CAC5C,MAAM,aAAa,OAAO;CAC1B,MAAM,WAAW,OAAO;CAGxB,MAAM,gBAAgB,KAAK,KAAK,WAAW,iBAAiB;CAG5D,MAAM,QAAQ,IAAI,aAAa,gBAAgB,EAAE;CAGjD,MAAM,mBAAmB,KAAK,MAAM,aAAa,iBAAiB;AAElE,MAAK,IAAI,IAAI,GAAG,IAAI,eAAe,KAAK;EACtC,MAAM,cAAc,IAAI;EACxB,MAAM,YAAY,KAAK,IAAI,cAAc,kBAAkB,YAAY,OAAO;EAE9E,IAAI,MAAM;EACV,IAAI,MAAM;AAEV,OAAK,IAAI,IAAI,aAAa,IAAI,WAAW,KAAK;GAC5C,MAAM,SAAS,YAAY,MAAM;AACjC,OAAI,SAAS,IAAK,OAAM;AACxB,OAAI,SAAS,IAAK,OAAM;;AAI1B,QAAM,IAAI,KAAK;AACf,QAAM,IAAI,IAAI,KAAK;;AAGrB,QAAO"}
1
+ {"version":3,"file":"waveformUtils.js","names":["audioBuffer: AudioBuffer","waveformData: WaveformData"],"sources":["../../../../src/gui/timeline/tracks/waveformUtils.ts"],"sourcesContent":["/**\n * Waveform extraction utilities for DAW-style audio visualization.\n *\n * Extracts min/max peak pairs from audio data at a given resolution.\n * Designed for timeline visualization where we need to see amplitude\n * overview across the entire audio duration.\n */\n\n/** Samples per second for waveform data - balances resolution vs. data size */\nexport const WAVEFORM_SAMPLES_PER_SECOND = 100;\n\n/** Waveform peak data: alternating min/max values normalized to [-1, 1] */\nexport interface WaveformData {\n /** Peak data: [min0, max0, min1, max1, ...] normalized to [-1, 1] */\n peaks: Float32Array;\n /** Duration of the audio in milliseconds */\n durationMs: number;\n /** Samples per second (for interpreting peaks array) */\n samplesPerSecond: number;\n}\n\n/** Simple cache for waveform data keyed by audio URL */\nconst waveformCache = new Map<string, WaveformData>();\n\n/**\n * Extract waveform peak data from an audio URL.\n * Uses Web Audio API to decode and analyze the audio.\n * Results are cached by URL.\n */\nexport async function extractWaveformData(\n audioUrl: string,\n signal?: AbortSignal,\n): Promise<WaveformData | null> {\n // Check cache first\n const cached = waveformCache.get(audioUrl);\n if (cached) {\n return cached;\n }\n\n try {\n // Fetch the audio file\n const response = await fetch(audioUrl, { signal });\n if (!response.ok) {\n console.warn(`Failed to fetch audio for waveform: ${response.status}`);\n return null;\n }\n\n const arrayBuffer = await response.arrayBuffer();\n signal?.throwIfAborted();\n\n // Decode audio data\n const audioContext = new OfflineAudioContext(1, 1, 44100);\n let audioBuffer: AudioBuffer;\n\n try {\n audioBuffer = await audioContext.decodeAudioData(arrayBuffer);\n } catch (decodeError) {\n console.warn(\"Failed to decode audio for waveform:\", decodeError);\n return null;\n }\n\n signal?.throwIfAborted();\n\n // Extract peaks from the decoded audio\n const peaks = extractPeaksFromBuffer(\n audioBuffer,\n WAVEFORM_SAMPLES_PER_SECOND,\n );\n const durationMs = audioBuffer.duration * 1000;\n\n const waveformData: WaveformData = {\n peaks,\n durationMs,\n samplesPerSecond: WAVEFORM_SAMPLES_PER_SECOND,\n };\n\n // Cache the result\n waveformCache.set(audioUrl, waveformData);\n\n return waveformData;\n } catch (error) {\n if (error instanceof DOMException && error.name === \"AbortError\") {\n throw error;\n }\n console.warn(\"Error extracting waveform data:\", error);\n return null;\n }\n}\n\n/**\n * Extract min/max peaks from an AudioBuffer.\n * Returns Float32Array with alternating [min, max, min, max, ...] values.\n */\nfunction extractPeaksFromBuffer(\n buffer: AudioBuffer,\n samplesPerSecond: number,\n): Float32Array {\n const channelData = buffer.getChannelData(0); // Use first channel\n const sampleRate = buffer.sampleRate;\n const duration = buffer.duration;\n\n // Calculate how many samples to output\n const outputSamples = Math.ceil(duration * samplesPerSecond);\n\n // Each output sample has min and max\n const peaks = new Float32Array(outputSamples * 2);\n\n // Samples per output window\n const samplesPerWindow = Math.floor(sampleRate / samplesPerSecond);\n\n for (let i = 0; i < outputSamples; i++) {\n const startSample = i * samplesPerWindow;\n const endSample = Math.min(\n startSample + samplesPerWindow,\n channelData.length,\n );\n\n let min = 0;\n let max = 0;\n\n for (let j = startSample; j < endSample; j++) {\n const sample = channelData[j] ?? 0;\n if (sample < min) min = sample;\n if (sample > max) max = sample;\n }\n\n // Store as alternating min/max pairs\n peaks[i * 2] = min;\n peaks[i * 2 + 1] = max;\n }\n\n return peaks;\n}\n\n/**\n * Render waveform data to a canvas context.\n * Draws a filled waveform path centered vertically.\n */\nexport function renderWaveformToCanvas(\n ctx: CanvasRenderingContext2D,\n waveformData: WaveformData,\n x: number,\n y: number,\n width: number,\n height: number,\n color: string,\n startMs: number = 0,\n endMs?: number,\n): void {\n const { peaks, durationMs, samplesPerSecond } = waveformData;\n const actualEndMs = endMs ?? durationMs;\n\n // Calculate which samples to render\n const startSample = Math.floor((startMs / 1000) * samplesPerSecond);\n const endSample = Math.ceil((actualEndMs / 1000) * samplesPerSecond);\n const sampleCount = endSample - startSample;\n\n if (sampleCount <= 0) return;\n\n const centerY = y + height / 2;\n const halfHeight = height / 2;\n const pixelsPerSample = width / sampleCount;\n\n ctx.fillStyle = color;\n ctx.beginPath();\n\n // Draw top half (max values) left to right\n for (let i = 0; i < sampleCount; i++) {\n const sampleIndex = startSample + i;\n const peakIndex = sampleIndex * 2;\n const maxValue = peaks[peakIndex + 1] ?? 0;\n\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 - 1; i >= 0; i--) {\n const sampleIndex = startSample + i;\n const peakIndex = sampleIndex * 2;\n const minValue = peaks[peakIndex] ?? 0;\n\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\n/**\n * Clear waveform cache (useful for testing or memory management)\n */\nexport function clearWaveformCache(): void {\n waveformCache.clear();\n}\n"],"mappings":";;;;;;;;;AASA,MAAa,8BAA8B;;AAa3C,MAAM,gCAAgB,IAAI,KAA2B;;;;;;AAOrD,eAAsB,oBACpB,UACA,QAC8B;CAE9B,MAAM,SAAS,cAAc,IAAI,SAAS;AAC1C,KAAI,OACF,QAAO;AAGT,KAAI;EAEF,MAAM,WAAW,MAAM,MAAM,UAAU,EAAE,QAAQ,CAAC;AAClD,MAAI,CAAC,SAAS,IAAI;AAChB,WAAQ,KAAK,uCAAuC,SAAS,SAAS;AACtE,UAAO;;EAGT,MAAM,cAAc,MAAM,SAAS,aAAa;AAChD,UAAQ,gBAAgB;EAGxB,MAAM,eAAe,IAAI,oBAAoB,GAAG,GAAG,MAAM;EACzD,IAAIA;AAEJ,MAAI;AACF,iBAAc,MAAM,aAAa,gBAAgB,YAAY;WACtD,aAAa;AACpB,WAAQ,KAAK,wCAAwC,YAAY;AACjE,UAAO;;AAGT,UAAQ,gBAAgB;EASxB,MAAMC,eAA6B;GACjC,OAPY,uBACZ,aACA,4BACD;GAKC,YAJiB,YAAY,WAAW;GAKxC,kBAAkB;GACnB;AAGD,gBAAc,IAAI,UAAU,aAAa;AAEzC,SAAO;UACA,OAAO;AACd,MAAI,iBAAiB,gBAAgB,MAAM,SAAS,aAClD,OAAM;AAER,UAAQ,KAAK,mCAAmC,MAAM;AACtD,SAAO;;;;;;;AAQX,SAAS,uBACP,QACA,kBACc;CACd,MAAM,cAAc,OAAO,eAAe,EAAE;CAC5C,MAAM,aAAa,OAAO;CAC1B,MAAM,WAAW,OAAO;CAGxB,MAAM,gBAAgB,KAAK,KAAK,WAAW,iBAAiB;CAG5D,MAAM,QAAQ,IAAI,aAAa,gBAAgB,EAAE;CAGjD,MAAM,mBAAmB,KAAK,MAAM,aAAa,iBAAiB;AAElE,MAAK,IAAI,IAAI,GAAG,IAAI,eAAe,KAAK;EACtC,MAAM,cAAc,IAAI;EACxB,MAAM,YAAY,KAAK,IACrB,cAAc,kBACd,YAAY,OACb;EAED,IAAI,MAAM;EACV,IAAI,MAAM;AAEV,OAAK,IAAI,IAAI,aAAa,IAAI,WAAW,KAAK;GAC5C,MAAM,SAAS,YAAY,MAAM;AACjC,OAAI,SAAS,IAAK,OAAM;AACxB,OAAI,SAAS,IAAK,OAAM;;AAI1B,QAAM,IAAI,KAAK;AACf,QAAM,IAAI,IAAI,KAAK;;AAGrB,QAAO"}
@@ -1,6 +1,6 @@
1
1
  import { TreeItem } from "./treeContext.js";
2
2
  import "./EFTreeItem.js";
3
- import * as lit12 from "lit";
3
+ import * as lit13 from "lit";
4
4
  import { LitElement, PropertyValues } from "lit";
5
5
  import * as lit_html12 from "lit-html";
6
6
 
@@ -29,7 +29,7 @@ import * as lit_html12 from "lit-html";
29
29
  * ```
30
30
  */
31
31
  declare class EFTree extends LitElement {
32
- static styles: lit12.CSSResult;
32
+ static styles: lit13.CSSResult;
33
33
  /** Tree items to display */
34
34
  items: TreeItem[];
35
35
  /** Optional header text */
@@ -65,17 +65,11 @@ let EFTree = class EFTree$1 extends LitElement {
65
65
  overflow: auto;
66
66
  font-size: 12px;
67
67
 
68
- --tree-bg: rgb(30 41 59);
69
- --tree-text: rgb(226 232 240);
70
- --tree-hover-bg: rgba(148, 163, 184, 0.2);
71
- --tree-selected-bg: rgba(59, 130, 246, 0.3);
72
- }
73
-
74
- :host(.light) {
75
- --tree-bg: rgb(241 245 249);
76
- --tree-text: rgb(30 41 59);
77
- --tree-hover-bg: rgba(100, 116, 139, 0.15);
78
- --tree-selected-bg: rgba(59, 130, 246, 0.2);
68
+ --tree-bg: var(--ef-color-bg);
69
+ --tree-text: var(--ef-color-text);
70
+ --tree-hover-bg: var(--ef-color-hover);
71
+ --tree-selected-bg: var(--ef-color-selected);
72
+ --tree-border: var(--ef-color-border);
79
73
  }
80
74
 
81
75
  .tree-container {
@@ -91,15 +85,15 @@ let EFTree = class EFTree$1 extends LitElement {
91
85
  font-size: 11px;
92
86
  text-transform: uppercase;
93
87
  letter-spacing: 0.05em;
94
- color: rgba(148, 163, 184, 0.8);
95
- border-bottom: 1px solid var(--tree-border, rgb(71 85 105));
88
+ color: var(--ef-color-text-muted);
89
+ border-bottom: 1px solid var(--tree-border);
96
90
  margin-bottom: 4px;
97
91
  }
98
92
 
99
93
  .empty {
100
94
  padding: 16px;
101
95
  text-align: center;
102
- color: rgba(148, 163, 184, 0.6);
96
+ color: var(--ef-color-text-subtle);
103
97
  font-style: italic;
104
98
  }
105
99
  `;
@@ -1 +1 @@
1
- {"version":3,"file":"EFTree.js","names":["EFTree"],"sources":["../../../src/gui/tree/EFTree.ts"],"sourcesContent":["import { provide } from \"@lit/context\";\nimport { css, html, LitElement, nothing, type PropertyValues } from \"lit\";\nimport { customElement, property, state } from \"lit/decorators.js\";\n\nimport type { TreeItem, TreeContext, TreeState, TreeActions } from \"./treeContext.js\";\nimport { treeContext, collectAllIds } from \"./treeContext.js\";\nimport \"./EFTreeItem.js\";\n\n/**\n * Generic tree component for displaying hierarchical data.\n * \n * Takes an array of TreeItem objects and renders them as an expandable tree.\n * Provides context for selection and expand/collapse state.\n * \n * @fires tree-select - When an item is selected. Detail: { id: string, item: TreeItem }\n * \n * @example\n * ```html\n * <ef-tree\n * .items=${[\n * { id: \"folder1\", label: \"Folder 1\", children: [\n * { id: \"file1\", label: \"File 1\" },\n * { id: \"file2\", label: \"File 2\" },\n * ]},\n * { id: \"file3\", label: \"File 3\" },\n * ]}\n * @tree-select=${(e) => console.log('Selected:', e.detail.id)}\n * ></ef-tree>\n * ```\n */\n@customElement(\"ef-tree\")\nexport class EFTree extends LitElement {\n static styles = css`\n :host {\n display: block;\n overflow: auto;\n font-size: 12px;\n\n --tree-bg: rgb(30 41 59);\n --tree-text: rgb(226 232 240);\n --tree-hover-bg: rgba(148, 163, 184, 0.2);\n --tree-selected-bg: rgba(59, 130, 246, 0.3);\n }\n\n :host(.light) {\n --tree-bg: rgb(241 245 249);\n --tree-text: rgb(30 41 59);\n --tree-hover-bg: rgba(100, 116, 139, 0.15);\n --tree-selected-bg: rgba(59, 130, 246, 0.2);\n }\n\n .tree-container {\n background: var(--tree-bg);\n color: var(--tree-text);\n min-height: 100%;\n padding: 4px 0;\n }\n\n .header {\n padding: 8px 12px;\n font-weight: 600;\n font-size: 11px;\n text-transform: uppercase;\n letter-spacing: 0.05em;\n color: rgba(148, 163, 184, 0.8);\n border-bottom: 1px solid var(--tree-border, rgb(71 85 105));\n margin-bottom: 4px;\n }\n\n .empty {\n padding: 16px;\n text-align: center;\n color: rgba(148, 163, 184, 0.6);\n font-style: italic;\n }\n `;\n\n /** Tree items to display */\n @property({ type: Array, attribute: false })\n items: TreeItem[] = [];\n\n /** Optional header text */\n @property({ type: String })\n header = \"\";\n\n /** Whether to show the header */\n @property({ type: Boolean, attribute: \"show-header\" })\n showHeader = false;\n\n /** Currently selected item ID (can be set externally) */\n @property({ type: String, attribute: \"selected-id\" })\n selectedId: string | null = null;\n\n /** Whether to expand all items by default */\n @property({ type: Boolean, attribute: \"expand-all\" })\n expandAll = true;\n\n @state()\n private treeState: TreeState = {\n selectedId: null,\n expandedIds: new Set(),\n };\n\n private treeActions: TreeActions = {\n select: (id: string | null) => {\n this.treeState = {\n ...this.treeState,\n selectedId: id,\n };\n \n // Find the item for the event detail\n const item = id ? this.findItem(id, this.items) : null;\n \n this.dispatchEvent(\n new CustomEvent(\"tree-select\", {\n detail: { id, item },\n bubbles: true,\n composed: true,\n })\n );\n },\n\n toggleExpanded: (id: string) => {\n const newExpanded = new Set(this.treeState.expandedIds);\n if (newExpanded.has(id)) {\n newExpanded.delete(id);\n } else {\n newExpanded.add(id);\n }\n this.treeState = {\n ...this.treeState,\n expandedIds: newExpanded,\n };\n },\n\n setExpanded: (id: string, expanded: boolean) => {\n const newExpanded = new Set(this.treeState.expandedIds);\n if (expanded) {\n newExpanded.add(id);\n } else {\n newExpanded.delete(id);\n }\n this.treeState = {\n ...this.treeState,\n expandedIds: newExpanded,\n };\n },\n };\n\n @provide({ context: treeContext })\n @state()\n private providedContext: TreeContext = {\n state: this.treeState,\n actions: this.treeActions,\n };\n\n private findItem(id: string, items: TreeItem[]): TreeItem | null {\n for (const item of items) {\n if (item.id === id) return item;\n if (item.children) {\n const found = this.findItem(id, item.children);\n if (found) return found;\n }\n }\n return null;\n }\n\n private initializeExpandedState(): void {\n if (this.expandAll && this.items.length > 0) {\n this.treeState = {\n ...this.treeState,\n expandedIds: collectAllIds(this.items),\n };\n }\n }\n\n protected willUpdate(changedProperties: PropertyValues): void {\n // Sync external selectedId with internal state\n if (changedProperties.has(\"selectedId\") && this.selectedId !== this.treeState.selectedId) {\n this.treeState = {\n ...this.treeState,\n selectedId: this.selectedId,\n };\n }\n\n // Always update provided context\n this.providedContext = {\n state: this.treeState,\n actions: this.treeActions,\n };\n\n super.willUpdate(changedProperties);\n }\n\n protected updated(changedProperties: PropertyValues): void {\n super.updated(changedProperties);\n\n // Re-initialize expanded state when items change\n if (changedProperties.has(\"items\")) {\n this.initializeExpandedState();\n }\n }\n\n connectedCallback(): void {\n super.connectedCallback();\n this.initializeExpandedState();\n }\n\n render() {\n return html`\n <div class=\"tree-container\">\n ${this.showHeader ? html`<div class=\"header\">${this.header}</div>` : nothing}\n ${this.items.length > 0\n ? this.items.map(\n (item) => html`<ef-tree-item .item=${item}></ef-tree-item>`\n )\n : html`<div class=\"empty\">No items</div>`}\n </div>\n `;\n }\n}\n\ndeclare global {\n interface HTMLElementTagNameMap {\n \"ef-tree\": EFTree;\n }\n}\n"],"mappings":";;;;;;;;AA+BO,mBAAMA,iBAAe,WAAW;;;eAgDjB,EAAE;gBAIb;oBAII;oBAIe;mBAIhB;mBAGmB;GAC7B,YAAY;GACZ,6BAAa,IAAI,KAAK;GACvB;qBAEkC;GACjC,SAAS,OAAsB;AAC7B,SAAK,YAAY;KACf,GAAG,KAAK;KACR,YAAY;KACb;IAGD,MAAM,OAAO,KAAK,KAAK,SAAS,IAAI,KAAK,MAAM,GAAG;AAElD,SAAK,cACH,IAAI,YAAY,eAAe;KAC7B,QAAQ;MAAE;MAAI;MAAM;KACpB,SAAS;KACT,UAAU;KACX,CAAC,CACH;;GAGH,iBAAiB,OAAe;IAC9B,MAAM,cAAc,IAAI,IAAI,KAAK,UAAU,YAAY;AACvD,QAAI,YAAY,IAAI,GAAG,CACrB,aAAY,OAAO,GAAG;QAEtB,aAAY,IAAI,GAAG;AAErB,SAAK,YAAY;KACf,GAAG,KAAK;KACR,aAAa;KACd;;GAGH,cAAc,IAAY,aAAsB;IAC9C,MAAM,cAAc,IAAI,IAAI,KAAK,UAAU,YAAY;AACvD,QAAI,SACF,aAAY,IAAI,GAAG;QAEnB,aAAY,OAAO,GAAG;AAExB,SAAK,YAAY;KACf,GAAG,KAAK;KACR,aAAa;KACd;;GAEJ;yBAIsC;GACrC,OAAO,KAAK;GACZ,SAAS,KAAK;GACf;;;gBA1He,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA4HnB,AAAQ,SAAS,IAAY,OAAoC;AAC/D,OAAK,MAAM,QAAQ,OAAO;AACxB,OAAI,KAAK,OAAO,GAAI,QAAO;AAC3B,OAAI,KAAK,UAAU;IACjB,MAAM,QAAQ,KAAK,SAAS,IAAI,KAAK,SAAS;AAC9C,QAAI,MAAO,QAAO;;;AAGtB,SAAO;;CAGT,AAAQ,0BAAgC;AACtC,MAAI,KAAK,aAAa,KAAK,MAAM,SAAS,EACxC,MAAK,YAAY;GACf,GAAG,KAAK;GACR,aAAa,cAAc,KAAK,MAAM;GACvC;;CAIL,AAAU,WAAW,mBAAyC;AAE5D,MAAI,kBAAkB,IAAI,aAAa,IAAI,KAAK,eAAe,KAAK,UAAU,WAC5E,MAAK,YAAY;GACf,GAAG,KAAK;GACR,YAAY,KAAK;GAClB;AAIH,OAAK,kBAAkB;GACrB,OAAO,KAAK;GACZ,SAAS,KAAK;GACf;AAED,QAAM,WAAW,kBAAkB;;CAGrC,AAAU,QAAQ,mBAAyC;AACzD,QAAM,QAAQ,kBAAkB;AAGhC,MAAI,kBAAkB,IAAI,QAAQ,CAChC,MAAK,yBAAyB;;CAIlC,oBAA0B;AACxB,QAAM,mBAAmB;AACzB,OAAK,yBAAyB;;CAGhC,SAAS;AACP,SAAO,IAAI;;UAEL,KAAK,aAAa,IAAI,uBAAuB,KAAK,OAAO,UAAU,QAAQ;UAC3E,KAAK,MAAM,SAAS,IAClB,KAAK,MAAM,KACR,SAAS,IAAI,uBAAuB,KAAK,kBAC3C,GACD,IAAI,oCAAoC;;;;;YA1IjD,SAAS;CAAE,MAAM;CAAO,WAAW;CAAO,CAAC;YAI3C,SAAS,EAAE,MAAM,QAAQ,CAAC;YAI1B,SAAS;CAAE,MAAM;CAAS,WAAW;CAAe,CAAC;YAIrD,SAAS;CAAE,MAAM;CAAQ,WAAW;CAAe,CAAC;YAIpD,SAAS;CAAE,MAAM;CAAS,WAAW;CAAc,CAAC;YAGpD,OAAO;YAoDP,QAAQ,EAAE,SAAS,aAAa,CAAC,EACjC,OAAO;qBAxHT,cAAc,UAAU"}
1
+ {"version":3,"file":"EFTree.js","names":["EFTree"],"sources":["../../../src/gui/tree/EFTree.ts"],"sourcesContent":["import { provide } from \"@lit/context\";\nimport { css, html, LitElement, nothing, type PropertyValues } from \"lit\";\nimport { customElement, property, state } from \"lit/decorators.js\";\n\nimport type {\n TreeItem,\n TreeContext,\n TreeState,\n TreeActions,\n} from \"./treeContext.js\";\nimport { treeContext, collectAllIds } from \"./treeContext.js\";\nimport \"./EFTreeItem.js\";\n\n/**\n * Generic tree component for displaying hierarchical data.\n *\n * Takes an array of TreeItem objects and renders them as an expandable tree.\n * Provides context for selection and expand/collapse state.\n *\n * @fires tree-select - When an item is selected. Detail: { id: string, item: TreeItem }\n *\n * @example\n * ```html\n * <ef-tree\n * .items=${[\n * { id: \"folder1\", label: \"Folder 1\", children: [\n * { id: \"file1\", label: \"File 1\" },\n * { id: \"file2\", label: \"File 2\" },\n * ]},\n * { id: \"file3\", label: \"File 3\" },\n * ]}\n * @tree-select=${(e) => console.log('Selected:', e.detail.id)}\n * ></ef-tree>\n * ```\n */\n@customElement(\"ef-tree\")\nexport class EFTree extends LitElement {\n static styles = css`\n :host {\n display: block;\n overflow: auto;\n font-size: 12px;\n\n --tree-bg: var(--ef-color-bg);\n --tree-text: var(--ef-color-text);\n --tree-hover-bg: var(--ef-color-hover);\n --tree-selected-bg: var(--ef-color-selected);\n --tree-border: var(--ef-color-border);\n }\n\n .tree-container {\n background: var(--tree-bg);\n color: var(--tree-text);\n min-height: 100%;\n padding: 4px 0;\n }\n\n .header {\n padding: 8px 12px;\n font-weight: 600;\n font-size: 11px;\n text-transform: uppercase;\n letter-spacing: 0.05em;\n color: var(--ef-color-text-muted);\n border-bottom: 1px solid var(--tree-border);\n margin-bottom: 4px;\n }\n\n .empty {\n padding: 16px;\n text-align: center;\n color: var(--ef-color-text-subtle);\n font-style: italic;\n }\n `;\n\n /** Tree items to display */\n @property({ type: Array, attribute: false })\n items: TreeItem[] = [];\n\n /** Optional header text */\n @property({ type: String })\n header = \"\";\n\n /** Whether to show the header */\n @property({ type: Boolean, attribute: \"show-header\" })\n showHeader = false;\n\n /** Currently selected item ID (can be set externally) */\n @property({ type: String, attribute: \"selected-id\" })\n selectedId: string | null = null;\n\n /** Whether to expand all items by default */\n @property({ type: Boolean, attribute: \"expand-all\" })\n expandAll = true;\n\n @state()\n private treeState: TreeState = {\n selectedId: null,\n expandedIds: new Set(),\n };\n\n private treeActions: TreeActions = {\n select: (id: string | null) => {\n this.treeState = {\n ...this.treeState,\n selectedId: id,\n };\n\n // Find the item for the event detail\n const item = id ? this.findItem(id, this.items) : null;\n\n this.dispatchEvent(\n new CustomEvent(\"tree-select\", {\n detail: { id, item },\n bubbles: true,\n composed: true,\n }),\n );\n },\n\n toggleExpanded: (id: string) => {\n const newExpanded = new Set(this.treeState.expandedIds);\n if (newExpanded.has(id)) {\n newExpanded.delete(id);\n } else {\n newExpanded.add(id);\n }\n this.treeState = {\n ...this.treeState,\n expandedIds: newExpanded,\n };\n },\n\n setExpanded: (id: string, expanded: boolean) => {\n const newExpanded = new Set(this.treeState.expandedIds);\n if (expanded) {\n newExpanded.add(id);\n } else {\n newExpanded.delete(id);\n }\n this.treeState = {\n ...this.treeState,\n expandedIds: newExpanded,\n };\n },\n };\n\n @provide({ context: treeContext })\n @state()\n // @ts-ignore\n private providedContext: TreeContext = {\n state: this.treeState,\n actions: this.treeActions,\n };\n\n private findItem(id: string, items: TreeItem[]): TreeItem | null {\n for (const item of items) {\n if (item.id === id) return item;\n if (item.children) {\n const found = this.findItem(id, item.children);\n if (found) return found;\n }\n }\n return null;\n }\n\n private initializeExpandedState(): void {\n if (this.expandAll && this.items.length > 0) {\n this.treeState = {\n ...this.treeState,\n expandedIds: collectAllIds(this.items),\n };\n }\n }\n\n protected willUpdate(changedProperties: PropertyValues): void {\n // Sync external selectedId with internal state\n if (\n changedProperties.has(\"selectedId\") &&\n this.selectedId !== this.treeState.selectedId\n ) {\n this.treeState = {\n ...this.treeState,\n selectedId: this.selectedId,\n };\n }\n\n // Always update provided context\n this.providedContext = {\n state: this.treeState,\n actions: this.treeActions,\n };\n\n super.willUpdate(changedProperties);\n }\n\n protected updated(changedProperties: PropertyValues): void {\n super.updated(changedProperties);\n\n // Re-initialize expanded state when items change\n if (changedProperties.has(\"items\")) {\n this.initializeExpandedState();\n }\n }\n\n connectedCallback(): void {\n super.connectedCallback();\n this.initializeExpandedState();\n }\n\n render() {\n return html`\n <div class=\"tree-container\">\n ${this.showHeader ? html`<div class=\"header\">${this.header}</div>` : nothing}\n ${\n this.items.length > 0\n ? this.items.map(\n (item) => html`<ef-tree-item .item=${item}></ef-tree-item>`,\n )\n : html`<div class=\"empty\">No items</div>`\n }\n </div>\n `;\n }\n}\n\ndeclare global {\n interface HTMLElementTagNameMap {\n \"ef-tree\": EFTree;\n }\n}\n"],"mappings":";;;;;;;;AAoCO,mBAAMA,iBAAe,WAAW;;;eA0CjB,EAAE;gBAIb;oBAII;oBAIe;mBAIhB;mBAGmB;GAC7B,YAAY;GACZ,6BAAa,IAAI,KAAK;GACvB;qBAEkC;GACjC,SAAS,OAAsB;AAC7B,SAAK,YAAY;KACf,GAAG,KAAK;KACR,YAAY;KACb;IAGD,MAAM,OAAO,KAAK,KAAK,SAAS,IAAI,KAAK,MAAM,GAAG;AAElD,SAAK,cACH,IAAI,YAAY,eAAe;KAC7B,QAAQ;MAAE;MAAI;MAAM;KACpB,SAAS;KACT,UAAU;KACX,CAAC,CACH;;GAGH,iBAAiB,OAAe;IAC9B,MAAM,cAAc,IAAI,IAAI,KAAK,UAAU,YAAY;AACvD,QAAI,YAAY,IAAI,GAAG,CACrB,aAAY,OAAO,GAAG;QAEtB,aAAY,IAAI,GAAG;AAErB,SAAK,YAAY;KACf,GAAG,KAAK;KACR,aAAa;KACd;;GAGH,cAAc,IAAY,aAAsB;IAC9C,MAAM,cAAc,IAAI,IAAI,KAAK,UAAU,YAAY;AACvD,QAAI,SACF,aAAY,IAAI,GAAG;QAEnB,aAAY,OAAO,GAAG;AAExB,SAAK,YAAY;KACf,GAAG,KAAK;KACR,aAAa;KACd;;GAEJ;yBAKsC;GACrC,OAAO,KAAK;GACZ,SAAS,KAAK;GACf;;;gBArHe,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAuHnB,AAAQ,SAAS,IAAY,OAAoC;AAC/D,OAAK,MAAM,QAAQ,OAAO;AACxB,OAAI,KAAK,OAAO,GAAI,QAAO;AAC3B,OAAI,KAAK,UAAU;IACjB,MAAM,QAAQ,KAAK,SAAS,IAAI,KAAK,SAAS;AAC9C,QAAI,MAAO,QAAO;;;AAGtB,SAAO;;CAGT,AAAQ,0BAAgC;AACtC,MAAI,KAAK,aAAa,KAAK,MAAM,SAAS,EACxC,MAAK,YAAY;GACf,GAAG,KAAK;GACR,aAAa,cAAc,KAAK,MAAM;GACvC;;CAIL,AAAU,WAAW,mBAAyC;AAE5D,MACE,kBAAkB,IAAI,aAAa,IACnC,KAAK,eAAe,KAAK,UAAU,WAEnC,MAAK,YAAY;GACf,GAAG,KAAK;GACR,YAAY,KAAK;GAClB;AAIH,OAAK,kBAAkB;GACrB,OAAO,KAAK;GACZ,SAAS,KAAK;GACf;AAED,QAAM,WAAW,kBAAkB;;CAGrC,AAAU,QAAQ,mBAAyC;AACzD,QAAM,QAAQ,kBAAkB;AAGhC,MAAI,kBAAkB,IAAI,QAAQ,CAChC,MAAK,yBAAyB;;CAIlC,oBAA0B;AACxB,QAAM,mBAAmB;AACzB,OAAK,yBAAyB;;CAGhC,SAAS;AACP,SAAO,IAAI;;UAEL,KAAK,aAAa,IAAI,uBAAuB,KAAK,OAAO,UAAU,QAAQ;UAE3E,KAAK,MAAM,SAAS,IAChB,KAAK,MAAM,KACR,SAAS,IAAI,uBAAuB,KAAK,kBAC3C,GACD,IAAI,oCACT;;;;;YAhJN,SAAS;CAAE,MAAM;CAAO,WAAW;CAAO,CAAC;YAI3C,SAAS,EAAE,MAAM,QAAQ,CAAC;YAI1B,SAAS;CAAE,MAAM;CAAS,WAAW;CAAe,CAAC;YAIrD,SAAS;CAAE,MAAM;CAAQ,WAAW;CAAe,CAAC;YAIpD,SAAS;CAAE,MAAM;CAAS,WAAW;CAAc,CAAC;YAGpD,OAAO;YAoDP,QAAQ,EAAE,SAAS,aAAa,CAAC,EACjC,OAAO;qBAlHT,cAAc,UAAU"}
@@ -1,5 +1,5 @@
1
1
  import { TreeContext, TreeItem } from "./treeContext.js";
2
- import * as lit13 from "lit";
2
+ import * as lit14 from "lit";
3
3
  import { LitElement, nothing } from "lit";
4
4
  import * as lit_html13 from "lit-html";
5
5
 
@@ -17,7 +17,7 @@ import * as lit_html13 from "lit-html";
17
17
  * @fires tree-item-click - When item is clicked (for selection)
18
18
  */
19
19
  declare class EFTreeItem extends LitElement {
20
- static styles: lit13.CSSResult;
20
+ static styles: lit14.CSSResult;
21
21
  treeContext?: TreeContext;
22
22
  item: TreeItem;
23
23
  private localExpanded;
@@ -25,15 +25,15 @@ let EFTreeItem = class EFTreeItem$1 extends LitElement {
25
25
  font-size: var(--tree-item-font-size, 0.75rem);
26
26
  cursor: pointer;
27
27
  user-select: none;
28
- color: var(--tree-text, rgb(226 232 240));
28
+ color: var(--tree-text);
29
29
  }
30
30
 
31
31
  .item-row:hover {
32
- background: var(--tree-hover-bg, rgba(148, 163, 184, 0.2));
32
+ background: var(--tree-hover-bg);
33
33
  }
34
34
 
35
35
  .item-row[data-selected] {
36
- background: var(--tree-selected-bg, rgba(59, 130, 246, 0.3));
36
+ background: var(--tree-selected-bg);
37
37
  }
38
38
 
39
39
  .expand-icon {