@editframe/elements 0.45.2 → 0.45.4

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 (165) hide show
  1. package/dist/DelayedLoadingState.js.map +1 -1
  2. package/dist/EF_FRAMEGEN.js.map +1 -1
  3. package/dist/EF_RENDERING.js.map +1 -1
  4. package/dist/canvas/EFCanvas.js +3 -3
  5. package/dist/canvas/EFCanvas.js.map +1 -1
  6. package/dist/canvas/EFCanvasItem.js.map +1 -1
  7. package/dist/canvas/api/CanvasAPI.js.map +1 -1
  8. package/dist/canvas/getElementBounds.js.map +1 -1
  9. package/dist/canvas/overlays/SelectionOverlay.js.map +1 -1
  10. package/dist/canvas/overlays/overlayState.js.map +1 -1
  11. package/dist/canvas/selection/SelectionController.js +25 -23
  12. package/dist/canvas/selection/SelectionController.js.map +1 -1
  13. package/dist/canvas/selection/SelectionModel.js.map +1 -1
  14. package/dist/canvas/selection/selectionContext.js.map +1 -1
  15. package/dist/elements/ContainerInfo.js.map +1 -1
  16. package/dist/elements/CrossUpdateController.js.map +1 -1
  17. package/dist/elements/EFAudio.js.map +1 -1
  18. package/dist/elements/EFCaptions.js.map +1 -1
  19. package/dist/elements/EFImage.js +1 -1
  20. package/dist/elements/EFImage.js.map +1 -1
  21. package/dist/elements/EFMedia/BufferedSeekingInput.js.map +1 -1
  22. package/dist/elements/EFMedia/CachedFetcher.js.map +1 -1
  23. package/dist/elements/EFMedia/MediaEngine.js.map +1 -1
  24. package/dist/elements/EFMedia/SegmentIndex.js.map +1 -1
  25. package/dist/elements/EFMedia/SegmentTransport.js.map +1 -1
  26. package/dist/elements/EFMedia/TimingModel.js.map +1 -1
  27. package/dist/elements/EFMedia/shared/AudioSpanUtils.js.map +1 -1
  28. package/dist/elements/EFMedia/shared/GlobalInputCache.js.map +1 -1
  29. package/dist/elements/EFMedia/shared/PrecisionUtils.js.map +1 -1
  30. package/dist/elements/EFMedia/shared/ThumbnailExtractor.js.map +1 -1
  31. package/dist/elements/EFMedia/videoTasks/MainVideoInputCache.js.map +1 -1
  32. package/dist/elements/EFMedia/videoTasks/ScrubInputCache.js.map +1 -1
  33. package/dist/elements/EFMedia.js.map +1 -1
  34. package/dist/elements/EFPanZoom.js +9 -8
  35. package/dist/elements/EFPanZoom.js.map +1 -1
  36. package/dist/elements/EFSourceMixin.js +1 -1
  37. package/dist/elements/EFSourceMixin.js.map +1 -1
  38. package/dist/elements/EFSurface.js.map +1 -1
  39. package/dist/elements/EFTemporal.js.map +1 -1
  40. package/dist/elements/EFText.d.ts +4 -4
  41. package/dist/elements/EFText.js.map +1 -1
  42. package/dist/elements/EFTextSegment.d.ts +4 -4
  43. package/dist/elements/EFTimegroup.js +7 -8
  44. package/dist/elements/EFTimegroup.js.map +1 -1
  45. package/dist/elements/EFVideo.d.ts +4 -4
  46. package/dist/elements/EFVideo.js.map +1 -1
  47. package/dist/elements/EFWaveform.d.ts +4 -4
  48. package/dist/elements/EFWaveform.js.map +1 -1
  49. package/dist/elements/ElementPositionInfo.js.map +1 -1
  50. package/dist/elements/FetchMixin.js.map +1 -1
  51. package/dist/elements/SampleBuffer.js.map +1 -1
  52. package/dist/elements/TargetController.js.map +1 -1
  53. package/dist/elements/TimegroupController.js.map +1 -1
  54. package/dist/elements/cloneFactoryRegistry.js.map +1 -1
  55. package/dist/elements/durationConverter.js.map +1 -1
  56. package/dist/elements/easingUtils.js.map +1 -1
  57. package/dist/elements/renderTemporalAudio.js.map +1 -1
  58. package/dist/elements/setupTemporalHierarchy.js.map +1 -1
  59. package/dist/elements/updateAnimations.js +1 -1
  60. package/dist/elements/updateAnimations.js.map +1 -1
  61. package/dist/getRenderInfo.js.map +1 -1
  62. package/dist/gui/ContextMixin.js.map +1 -1
  63. package/dist/gui/Controllable.js.map +1 -1
  64. package/dist/gui/EFActiveRootTemporal.d.ts +4 -4
  65. package/dist/gui/EFActiveRootTemporal.js.map +1 -1
  66. package/dist/gui/EFConfiguration.d.ts +4 -4
  67. package/dist/gui/EFControls.js.map +1 -1
  68. package/dist/gui/EFDial.d.ts +4 -4
  69. package/dist/gui/EFFilmstrip.d.ts +4 -4
  70. package/dist/gui/EFFilmstrip.js.map +1 -1
  71. package/dist/gui/EFFitScale.js.map +1 -1
  72. package/dist/gui/EFOverlayItem.d.ts +4 -4
  73. package/dist/gui/EFOverlayItem.js.map +1 -1
  74. package/dist/gui/EFOverlayLayer.d.ts +4 -4
  75. package/dist/gui/EFOverlayLayer.js.map +1 -1
  76. package/dist/gui/EFPause.d.ts +4 -4
  77. package/dist/gui/EFPlay.d.ts +4 -4
  78. package/dist/gui/EFPreview.d.ts +4 -4
  79. package/dist/gui/EFPreview.js.map +1 -1
  80. package/dist/gui/EFResizableBox.js.map +1 -1
  81. package/dist/gui/EFScrubber.d.ts +4 -4
  82. package/dist/gui/EFScrubber.js.map +1 -1
  83. package/dist/gui/EFTimeDisplay.d.ts +4 -4
  84. package/dist/gui/EFTimeDisplay.js.map +1 -1
  85. package/dist/gui/EFTimelineRuler.d.ts +4 -4
  86. package/dist/gui/EFTimelineRuler.js.map +1 -1
  87. package/dist/gui/EFToggleLoop.d.ts +4 -4
  88. package/dist/gui/EFTogglePlay.d.ts +4 -4
  89. package/dist/gui/EFTogglePlay.js.map +1 -1
  90. package/dist/gui/EFTransformHandles.js.map +1 -1
  91. package/dist/gui/EFWorkbench.d.ts +4 -4
  92. package/dist/gui/EFWorkbench.js.map +1 -1
  93. package/dist/gui/FitScaleHelpers.js.map +1 -1
  94. package/dist/gui/PlaybackController.js.map +1 -1
  95. package/dist/gui/TWMixin2.js.map +1 -1
  96. package/dist/gui/TargetOrContextMixin.js.map +1 -1
  97. package/dist/gui/currentTimeContext.js.map +1 -1
  98. package/dist/gui/efContext.js.map +1 -1
  99. package/dist/gui/fetchContext.js.map +1 -1
  100. package/dist/gui/hierarchy/EFHierarchy.d.ts +4 -4
  101. package/dist/gui/hierarchy/EFHierarchy.js.map +1 -1
  102. package/dist/gui/hierarchy/EFHierarchyItem.d.ts +2 -2
  103. package/dist/gui/hierarchy/EFHierarchyItem.js.map +1 -1
  104. package/dist/gui/hierarchy/hierarchyContext.js.map +1 -1
  105. package/dist/gui/panZoomTransformContext.js.map +1 -1
  106. package/dist/gui/previewSettingsContext.js.map +1 -1
  107. package/dist/gui/theme.js.map +1 -1
  108. package/dist/gui/timeline/EFTimeline.d.ts +2 -2
  109. package/dist/gui/timeline/EFTimeline.js +0 -1
  110. package/dist/gui/timeline/EFTimeline.js.map +1 -1
  111. package/dist/gui/timeline/EFTimelineRow.js.map +1 -1
  112. package/dist/gui/timeline/TrimHandles.d.ts +4 -4
  113. package/dist/gui/timeline/TrimHandles.js.map +1 -1
  114. package/dist/gui/timeline/flattenHierarchy.js.map +1 -1
  115. package/dist/gui/timeline/timelineStateContext.js.map +1 -1
  116. package/dist/gui/timeline/tracks/AudioTrack.js.map +1 -1
  117. package/dist/gui/timeline/tracks/CaptionsTrack.js.map +1 -1
  118. package/dist/gui/timeline/tracks/EFThumbnailStrip.js.map +1 -1
  119. package/dist/gui/timeline/tracks/ImageTrack.js.map +1 -1
  120. package/dist/gui/timeline/tracks/TextTrack.js.map +1 -1
  121. package/dist/gui/timeline/tracks/TimegroupTrack.js.map +1 -1
  122. package/dist/gui/timeline/tracks/TrackItem.js.map +1 -1
  123. package/dist/gui/timeline/tracks/VideoTrack.js.map +1 -1
  124. package/dist/gui/timeline/tracks/renderTrackChildren.js.map +1 -1
  125. package/dist/gui/timeline/tracks/waveformUtils.js.map +1 -1
  126. package/dist/gui/transformCalculations.js.map +1 -1
  127. package/dist/gui/transformUtils.js.map +1 -1
  128. package/dist/gui/tree/EFTree.d.ts +4 -4
  129. package/dist/gui/tree/EFTree.js.map +1 -1
  130. package/dist/gui/tree/EFTreeItem.d.ts +4 -4
  131. package/dist/gui/tree/EFTreeItem.js.map +1 -1
  132. package/dist/index.js.map +1 -1
  133. package/dist/otel/BridgeSpanExporter.js.map +1 -1
  134. package/dist/otel/setupBrowserTracing.js.map +1 -1
  135. package/dist/otel/tracingHelpers.js.map +1 -1
  136. package/dist/preview/AdaptiveResolutionTracker.js.map +1 -1
  137. package/dist/preview/FrameController.js.map +1 -1
  138. package/dist/preview/QualityUpgradeScheduler.js.map +1 -1
  139. package/dist/preview/RenderContext.js.map +1 -1
  140. package/dist/preview/RenderProfiler.js.map +1 -1
  141. package/dist/preview/RenderStats.js.map +1 -1
  142. package/dist/preview/encoding/canvasEncoder.js.map +1 -1
  143. package/dist/preview/encoding/mainThreadEncoder.js +1 -1
  144. package/dist/preview/encoding/mainThreadEncoder.js.map +1 -1
  145. package/dist/preview/previewSettings.js.map +1 -1
  146. package/dist/preview/previewTypes.js.map +1 -1
  147. package/dist/preview/renderElementToCanvas.js.map +1 -1
  148. package/dist/preview/renderTimegroupToCanvas.js +2 -44
  149. package/dist/preview/renderTimegroupToCanvas.js.map +1 -1
  150. package/dist/preview/renderTimegroupToVideo.js +2 -2
  151. package/dist/preview/renderTimegroupToVideo.js.map +1 -1
  152. package/dist/preview/renderVideoToVideo.js +2 -2
  153. package/dist/preview/renderVideoToVideo.js.map +1 -1
  154. package/dist/preview/renderers.js.map +1 -1
  155. package/dist/preview/rendering/ScaleConfig.js.map +1 -1
  156. package/dist/preview/rendering/loadImage.js.map +1 -1
  157. package/dist/preview/rendering/renderToImageNative.js.map +1 -1
  158. package/dist/preview/rendering/serializeTimelineDirect.js +1 -1
  159. package/dist/preview/rendering/serializeTimelineDirect.js.map +1 -1
  160. package/dist/preview/statsTrackingStrategy.js.map +1 -1
  161. package/dist/preview/workers/WorkerPool.js.map +1 -1
  162. package/dist/render/EFRenderAPI.js.map +1 -1
  163. package/dist/transcoding/cache/RequestDeduplicator.js.map +1 -1
  164. package/dist/utils/LRUCache.js.map +1 -1
  165. package/package.json +1 -1
@@ -1 +1 @@
1
- {"version":3,"file":"EFTimelineRow.js","names":["EFTimelineRow","iconMap: Record<string, TemplateResult>"],"sources":["../../../src/gui/timeline/EFTimelineRow.ts"],"sourcesContent":["import { consume } from \"@lit/context\";\nimport {\n css,\n html,\n LitElement,\n nothing,\n type PropertyValues,\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 { EFCaptions } from \"../../elements/EFCaptions.js\";\nimport { TWMixin } from \"../TWMixin.js\";\nimport { renderTrackChildren } from \"./tracks/renderTrackChildren.js\";\nimport { phosphorIcon, ICONS } from \"../icons.js\";\nimport {\n timelineEditingContext,\n type TimelineEditingContext,\n} from \"./timelineEditingContext.js\";\nimport { getElementTypeColor } from \"../theme.js\";\n// NOTE: Track components (ef-timegroup-track, etc.) are NOT imported here\n// to avoid circular dependencies with TrackItem. They must be registered before\n// EFTimelineRow is used. See preloadTracks.ts for the registration sequence.\n\nconst INDENT_PX = 16;\n\n/**\n * EFTimelineRow - A unified timeline row containing both label and track\n *\n * This component renders a single row in the timeline with:\n * - A sticky label on the left (stays fixed during horizontal scroll)\n * - Track content on the right (scrolls horizontally with the timeline)\n *\n * Heights are determined by content, not hardcoded.\n */\n@customElement(\"ef-timeline-row\")\nexport class EFTimelineRow extends TWMixin(LitElement) {\n static styles = [\n css`\n :host {\n display: flex;\n min-height: var(--timeline-row-height, 28px);\n border-bottom: 1px solid var(--ef-color-border-subtle);\n }\n\n :host(.nested-timegroup) {\n min-height: 18px;\n --timeline-track-height: 14px;\n }\n\n :host(.nested-timegroup) .row-label {\n font-size: 10px;\n opacity: 0.7;\n }\n\n /* Root timegroup row with filmstrip - taller to show thumbnails */\n :host(.root-timegroup) {\n min-height: 52px;\n height: 52px;\n /* Sticky at top below ruler (ruler is 24px) */\n position: sticky;\n top: 24px;\n /* Higher z-index than regular row labels (z-index: 8) so everything scrolls underneath */\n z-index: 15;\n background: var(--timeline-bg, var(--ef-color-bg));\n border-bottom: 1px solid var(--ef-color-border);\n }\n\n /* Root timegroup label needs higher z-index to stay above other labels when scrolling */\n :host(.root-timegroup) .row-label {\n z-index: 16;\n }\n\n /* Hover state - this row is directly hovered */\n :host(.hovered) {\n background: var(--ef-color-hover);\n }\n\n /* Ancestor hovered - a descendant of this row is hovered */\n :host(.ancestor-hovered) {\n background: var(--ef-color-selected-subtle);\n }\n\n /* Descendant hovered - an ancestor of this row is hovered */\n :host(.descendant-hovered) {\n background: var(--ef-color-selected-subtle);\n }\n\n /* Selected state */\n :host(.selected) {\n background: var(--ef-color-selected);\n }\n \n :host(.selected) .row-label {\n font-weight: 500;\n }\n\n /* Ancestor has selected descendant */\n :host(.ancestor-selected) {\n background: var(--ef-color-selected-subtle);\n }\n\n .row-label {\n position: sticky;\n left: 0;\n /* Lower z-index so labels scroll underneath the sticky root timegroup row (z-index: 15) */\n z-index: 8;\n width: var(--timeline-hierarchy-width, 200px);\n flex-shrink: 0;\n background: var(--ef-color-bg-panel);\n border-right: 1px solid var(--ef-color-border-subtle);\n display: flex;\n align-items: center;\n font-size: 11px;\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n color: var(--ef-color-text);\n cursor: pointer;\n transition: background-color 0.1s ease;\n }\n\n /* Hide label column when hierarchy is disabled */\n :host([hide-label]) .row-label {\n display: none;\n }\n\n .row-label:hover {\n background: color-mix(in srgb, var(--ef-color-bg-panel) 70%, var(--ef-color-hover) 30%);\n }\n\n :host(.hovered) .row-label {\n background: var(--ef-color-bg-elevated);\n border-left: 3px solid var(--ef-color-primary);\n padding-left: calc(var(--indent, 0px) - 3px);\n }\n\n :host(.selected) .row-label {\n background: var(--ef-color-bg-elevated);\n border-left: 3px solid var(--ef-color-primary);\n padding-left: calc(var(--indent, 0px) - 3px);\n }\n\n .row-track {\n flex: 1;\n position: relative;\n min-width: 0;\n }\n \n :host(:first-child) .row-track::before {\n display: none;\n }\n \n /* Grouping indicator for nested elements */\n .row-track::after {\n content: \"\";\n position: absolute;\n left: -12px;\n top: 0;\n bottom: 0;\n width: 2px;\n background: var(--timeline-border, rgb(71 85 105));\n opacity: 0.2;\n z-index: 0;\n }\n \n :host(:first-child) .row-track::after {\n display: none;\n }\n `,\n ];\n\n @property({ type: Object, attribute: false })\n element!: TemporalMixinInterface & Element;\n\n @property({ type: Number })\n depth = 0;\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: Boolean, attribute: \"hide-label\", reflect: true })\n hideLabel = false;\n\n @property({ type: Array, attribute: false })\n hideSelectors?: string[];\n\n @property({ type: Array, attribute: false })\n showSelectors?: string[];\n\n /**\n * The currently highlighted element from canvas (source of truth).\n * Passed from parent timeline which reads it from canvas.\n */\n @property({ type: Object, attribute: false })\n highlightedElement: Element | null = null;\n\n @property({ type: Object, attribute: false })\n selectedIds: ReadonlySet<string> = new Set();\n\n @consume({ context: timelineEditingContext, subscribe: true })\n editingContext?: TimelineEditingContext;\n\n // Derived interaction states (computed on-demand)\n private get isHovered(): boolean {\n return this.highlightedElement === this.element;\n }\n\n private get isSelected(): boolean {\n const elementId = (this.element as unknown as HTMLElement)?.id;\n return elementId ? this.selectedIds.has(elementId) : false;\n }\n\n private get isAncestorSelected(): boolean {\n if (!this.element) return false;\n // Check if this element contains any selected element\n const elementAsHTMLElement = this.element as unknown as HTMLElement;\n for (const selectedId of this.selectedIds) {\n const selectedElement = document.getElementById(selectedId);\n if (\n selectedElement &&\n elementAsHTMLElement.contains(selectedElement) &&\n selectedElement !== elementAsHTMLElement\n ) {\n return true;\n }\n }\n return false;\n }\n\n private get isAncestorHovered(): boolean {\n if (!this.highlightedElement || !this.element) return false;\n // This row's element contains the highlighted element (highlighted is a descendant)\n return (\n this.element !== this.highlightedElement &&\n this.element.contains(this.highlightedElement)\n );\n }\n\n private get isDescendantHovered(): boolean {\n if (!this.highlightedElement || !this.element) return false;\n // The highlighted element contains this row's element (highlighted is an ancestor)\n return (\n this.element !== this.highlightedElement &&\n this.highlightedElement.contains(this.element)\n );\n }\n\n protected updated(changedProperties: PropertyValues): void {\n super.updated(changedProperties);\n\n // Update host classes based on interaction state\n if (\n changedProperties.has(\"highlightedElement\") ||\n changedProperties.has(\"element\")\n ) {\n this.classList.toggle(\"hovered\", this.isHovered);\n this.classList.toggle(\"ancestor-hovered\", this.isAncestorHovered);\n this.classList.toggle(\"descendant-hovered\", this.isDescendantHovered);\n }\n\n // Update selection classes\n if (\n changedProperties.has(\"selectedIds\") ||\n changedProperties.has(\"element\")\n ) {\n this.classList.toggle(\"selected\", this.isSelected);\n this.classList.toggle(\"ancestor-selected\", this.isAncestorSelected);\n }\n\n // Update root/nested timegroup classes\n if (changedProperties.has(\"element\")) {\n const isRoot =\n this.element instanceof EFTimegroup && this.element.isRootTimegroup;\n const isNested =\n this.element instanceof EFTimegroup && !this.element.isRootTimegroup;\n this.classList.toggle(\"root-timegroup\", isRoot);\n this.classList.toggle(\"nested-timegroup\", isNested);\n }\n }\n\n private handleMouseEnter = (): void => {\n // Skip hover interactions during active editing operations (scrubbing, trimming, etc.)\n if (this.editingContext && !this.editingContext.canInteract()) {\n return;\n }\n\n this.dispatchEvent(\n new CustomEvent(\"row-hover\", {\n detail: { element: this.element },\n bubbles: true,\n composed: true,\n }),\n );\n };\n\n private handleMouseLeave = (): void => {\n // Skip hover interactions during active editing operations (scrubbing, trimming, etc.)\n if (this.editingContext && !this.editingContext.canInteract()) {\n return;\n }\n\n this.dispatchEvent(\n new CustomEvent(\"row-hover\", {\n detail: { element: null },\n bubbles: true,\n composed: true,\n }),\n );\n };\n\n private handleClick = (e: Event): void => {\n e.stopPropagation();\n const elementId = (this.element as unknown as HTMLElement)?.id;\n if (elementId) {\n this.dispatchEvent(\n new CustomEvent(\"row-select\", {\n detail: { elementId, element: this.element },\n bubbles: true,\n composed: true,\n }),\n );\n }\n };\n\n private getElementType(element: Element): string {\n if (element instanceof EFVideo) return \"video\";\n if (element instanceof EFAudio) return \"audio\";\n if (element instanceof EFImage) return \"image\";\n if (element instanceof EFText) return \"text\";\n if (element instanceof EFCaptions) return \"captions\";\n if (element instanceof EFTimegroup) return \"timegroup\";\n return \"unknown\";\n }\n\n // Use shared type color utility from theme.ts\n // This reads from CSS variables (--ef-color-type-video, etc.) defined in ef-theme.css\n\n private getElementIcon(type: string): TemplateResult {\n const iconMap: Record<string, TemplateResult> = {\n video: phosphorIcon(ICONS.filmStrip, 14),\n audio: phosphorIcon(ICONS.speakerHigh, 14),\n image: phosphorIcon(ICONS.image, 14),\n text: phosphorIcon(ICONS.textT, 14),\n captions: phosphorIcon(ICONS.subtitles, 14),\n timegroup: phosphorIcon(ICONS.filmSlate, 14),\n unknown: phosphorIcon(ICONS.code, 14),\n };\n return iconMap[type] ?? iconMap.unknown!;\n }\n\n private getElementLabel(element: Element): string {\n const id = element.id || \"\";\n const type = this.getElementType(element);\n\n // If element has a meaningful ID (not auto-generated), use it\n if (id && !id.includes(\"-\") && !id.match(/^\\d+$/)) {\n return id;\n }\n\n // For auto-generated IDs, create a friendly name based on type\n // Count siblings of same type to generate \"Video 1\", \"Video 2\", etc.\n const parent = element.parentElement;\n if (parent) {\n const siblings = Array.from(parent.children).filter(\n (child) => this.getElementType(child) === type,\n );\n const index = siblings.indexOf(element) + 1;\n const typeLabels: Record<string, string> = {\n video: \"Video\",\n audio: \"Audio\",\n image: \"Image\",\n text: \"Text\",\n captions: \"Captions\",\n timegroup: \"Composition\",\n unknown: \"Layer\",\n };\n const label = typeLabels[type] || \"Layer\";\n\n // If there's only one of this type, don't add number\n if (siblings.length === 1) {\n return label;\n }\n return `${label} ${index}`;\n }\n\n // Fallback: capitalize the type\n return type.charAt(0).toUpperCase() + type.slice(1);\n }\n\n /**\n * Get additional detail text for the label (mode, preview, etc.)\n */\n private getElementDetail(element: Element): string | null {\n if (element instanceof EFTimegroup) {\n const mode = element.mode || \"fixed\";\n const modeLabels: Record<string, string> = {\n fixed: \"Fixed\",\n sequence: \"Sequence\",\n contain: \"Container\",\n };\n return modeLabels[mode] || mode;\n }\n if (element instanceof EFText) {\n // Get text preview\n const textContent = Array.from(element.childNodes)\n .filter((node) => node.nodeType === Node.TEXT_NODE)\n .map((node) => node.textContent?.trim())\n .filter(Boolean)\n .join(\" \");\n if (textContent) {\n return textContent.length > 20\n ? textContent.slice(0, 20) + \"...\"\n : textContent;\n }\n }\n return null;\n }\n\n private renderTrack(): TemplateResult | typeof nothing {\n if (!this.element || !isEFTemporal(this.element)) return nothing;\n\n // For timegroups, use skip-children since children get their own rows\n if (this.element instanceof EFTimegroup) {\n // Show filmstrip for root timegroups (no parent timegroup)\n // Use the timegroup's own isRootTimegroup property for reliability\n const showFilmstrip = this.element.isRootTimegroup;\n return html`<ef-timegroup-track\n .element=${this.element}\n pixels-per-ms=${this.pixelsPerMs}\n ?enable-trim=${this.enableTrim}\n ?skip-children=${true}\n ?show-filmstrip=${showFilmstrip}\n .hideSelectors=${this.hideSelectors}\n .showSelectors=${this.showSelectors}\n ></ef-timegroup-track>`;\n }\n\n return html`${renderTrackChildren(\n [this.element as unknown as Element],\n this.pixelsPerMs,\n this.hideSelectors,\n this.showSelectors,\n true, // skipRootFiltering - the row itself handles filtering\n this.enableTrim,\n )}`;\n }\n\n connectedCallback(): void {\n super.connectedCallback();\n this.addEventListener(\"mouseenter\", this.handleMouseEnter);\n this.addEventListener(\"mouseleave\", this.handleMouseLeave);\n this.addEventListener(\"click\", this.handleClick);\n }\n\n disconnectedCallback(): void {\n super.disconnectedCallback();\n this.removeEventListener(\"mouseenter\", this.handleMouseEnter);\n this.removeEventListener(\"mouseleave\", this.handleMouseLeave);\n this.removeEventListener(\"click\", this.handleClick);\n }\n\n render() {\n if (!this.element) return nothing;\n\n const type = this.getElementType(this.element);\n const label = this.getElementLabel(this.element);\n const detail = this.getElementDetail(this.element);\n const typeColor = getElementTypeColor(type, this);\n const icon = this.getElementIcon(type);\n const indentPx = this.depth * INDENT_PX;\n\n return html`\n <div\n class=\"row-label\"\n part=\"label\"\n style=${styleMap({\n paddingLeft: `${indentPx}px`,\n borderLeftColor: typeColor,\n borderLeftWidth: \"3px\",\n borderLeftStyle: \"solid\",\n })}\n >\n <span style=\"color: ${typeColor}; opacity: 0.9; margin-right: 6px; flex-shrink: 0;\">\n ${icon}\n </span>\n <span style=\"flex-shrink: 0;\">${label}</span>\n ${\n detail\n ? html`\n <span style=\"margin-left: 6px; opacity: 0.6; font-size: 10px; overflow: hidden; text-overflow: ellipsis;\">\n ${detail}\n </span>\n `\n : nothing\n }\n </div>\n <div class=\"row-track\" part=\"track\">${this.renderTrack()}</div>\n `;\n }\n}\n\ndeclare global {\n interface HTMLElementTagNameMap {\n \"ef-timeline-row\": EFTimelineRow;\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"EFTimelineRow.js","names":["EFTimelineRow","iconMap: Record<string, TemplateResult>"],"sources":["../../../src/gui/timeline/EFTimelineRow.ts"],"sourcesContent":["import { consume } from \"@lit/context\";\nimport { css, html, LitElement, nothing, type PropertyValues, type TemplateResult } from \"lit\";\nimport { customElement, property } from \"lit/decorators.js\";\nimport { styleMap } from \"lit/directives/style-map.js\";\n\nimport { isEFTemporal, 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 { EFCaptions } from \"../../elements/EFCaptions.js\";\nimport { TWMixin } from \"../TWMixin.js\";\nimport { renderTrackChildren } from \"./tracks/renderTrackChildren.js\";\nimport { phosphorIcon, ICONS } from \"../icons.js\";\nimport { timelineEditingContext, type TimelineEditingContext } from \"./timelineEditingContext.js\";\nimport { getElementTypeColor } from \"../theme.js\";\n// NOTE: Track components (ef-timegroup-track, etc.) are NOT imported here\n// to avoid circular dependencies with TrackItem. They must be registered before\n// EFTimelineRow is used. See preloadTracks.ts for the registration sequence.\n\nconst INDENT_PX = 16;\n\n/**\n * EFTimelineRow - A unified timeline row containing both label and track\n *\n * This component renders a single row in the timeline with:\n * - A sticky label on the left (stays fixed during horizontal scroll)\n * - Track content on the right (scrolls horizontally with the timeline)\n *\n * Heights are determined by content, not hardcoded.\n */\n@customElement(\"ef-timeline-row\")\nexport class EFTimelineRow extends TWMixin(LitElement) {\n static styles = [\n css`\n :host {\n display: flex;\n min-height: var(--timeline-row-height, 28px);\n border-bottom: 1px solid var(--ef-color-border-subtle);\n }\n\n :host(.nested-timegroup) {\n min-height: 18px;\n --timeline-track-height: 14px;\n }\n\n :host(.nested-timegroup) .row-label {\n font-size: 10px;\n opacity: 0.7;\n }\n\n /* Root timegroup row with filmstrip - taller to show thumbnails */\n :host(.root-timegroup) {\n min-height: 52px;\n height: 52px;\n /* Sticky at top below ruler (ruler is 24px) */\n position: sticky;\n top: 24px;\n /* Higher z-index than regular row labels (z-index: 8) so everything scrolls underneath */\n z-index: 15;\n background: var(--timeline-bg, var(--ef-color-bg));\n border-bottom: 1px solid var(--ef-color-border);\n }\n\n /* Root timegroup label needs higher z-index to stay above other labels when scrolling */\n :host(.root-timegroup) .row-label {\n z-index: 16;\n }\n\n /* Hover state - this row is directly hovered */\n :host(.hovered) {\n background: var(--ef-color-hover);\n }\n\n /* Ancestor hovered - a descendant of this row is hovered */\n :host(.ancestor-hovered) {\n background: var(--ef-color-selected-subtle);\n }\n\n /* Descendant hovered - an ancestor of this row is hovered */\n :host(.descendant-hovered) {\n background: var(--ef-color-selected-subtle);\n }\n\n /* Selected state */\n :host(.selected) {\n background: var(--ef-color-selected);\n }\n \n :host(.selected) .row-label {\n font-weight: 500;\n }\n\n /* Ancestor has selected descendant */\n :host(.ancestor-selected) {\n background: var(--ef-color-selected-subtle);\n }\n\n .row-label {\n position: sticky;\n left: 0;\n /* Lower z-index so labels scroll underneath the sticky root timegroup row (z-index: 15) */\n z-index: 8;\n width: var(--timeline-hierarchy-width, 200px);\n flex-shrink: 0;\n background: var(--ef-color-bg-panel);\n border-right: 1px solid var(--ef-color-border-subtle);\n display: flex;\n align-items: center;\n font-size: 11px;\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n color: var(--ef-color-text);\n cursor: pointer;\n transition: background-color 0.1s ease;\n }\n\n /* Hide label column when hierarchy is disabled */\n :host([hide-label]) .row-label {\n display: none;\n }\n\n .row-label:hover {\n background: color-mix(in srgb, var(--ef-color-bg-panel) 70%, var(--ef-color-hover) 30%);\n }\n\n :host(.hovered) .row-label {\n background: var(--ef-color-bg-elevated);\n border-left: 3px solid var(--ef-color-primary);\n padding-left: calc(var(--indent, 0px) - 3px);\n }\n\n :host(.selected) .row-label {\n background: var(--ef-color-bg-elevated);\n border-left: 3px solid var(--ef-color-primary);\n padding-left: calc(var(--indent, 0px) - 3px);\n }\n\n .row-track {\n flex: 1;\n position: relative;\n min-width: 0;\n }\n \n :host(:first-child) .row-track::before {\n display: none;\n }\n \n /* Grouping indicator for nested elements */\n .row-track::after {\n content: \"\";\n position: absolute;\n left: -12px;\n top: 0;\n bottom: 0;\n width: 2px;\n background: var(--timeline-border, rgb(71 85 105));\n opacity: 0.2;\n z-index: 0;\n }\n \n :host(:first-child) .row-track::after {\n display: none;\n }\n `,\n ];\n\n @property({ type: Object, attribute: false })\n element!: TemporalMixinInterface & Element;\n\n @property({ type: Number })\n depth = 0;\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: Boolean, attribute: \"hide-label\", reflect: true })\n hideLabel = false;\n\n @property({ type: Array, attribute: false })\n hideSelectors?: string[];\n\n @property({ type: Array, attribute: false })\n showSelectors?: string[];\n\n /**\n * The currently highlighted element from canvas (source of truth).\n * Passed from parent timeline which reads it from canvas.\n */\n @property({ type: Object, attribute: false })\n highlightedElement: Element | null = null;\n\n @property({ type: Object, attribute: false })\n selectedIds: ReadonlySet<string> = new Set();\n\n @consume({ context: timelineEditingContext, subscribe: true })\n editingContext?: TimelineEditingContext;\n\n // Derived interaction states (computed on-demand)\n private get isHovered(): boolean {\n return this.highlightedElement === this.element;\n }\n\n private get isSelected(): boolean {\n const elementId = (this.element as unknown as HTMLElement)?.id;\n return elementId ? this.selectedIds.has(elementId) : false;\n }\n\n private get isAncestorSelected(): boolean {\n if (!this.element) return false;\n // Check if this element contains any selected element\n const elementAsHTMLElement = this.element as unknown as HTMLElement;\n for (const selectedId of this.selectedIds) {\n const selectedElement = document.getElementById(selectedId);\n if (\n selectedElement &&\n elementAsHTMLElement.contains(selectedElement) &&\n selectedElement !== elementAsHTMLElement\n ) {\n return true;\n }\n }\n return false;\n }\n\n private get isAncestorHovered(): boolean {\n if (!this.highlightedElement || !this.element) return false;\n // This row's element contains the highlighted element (highlighted is a descendant)\n return (\n this.element !== this.highlightedElement && this.element.contains(this.highlightedElement)\n );\n }\n\n private get isDescendantHovered(): boolean {\n if (!this.highlightedElement || !this.element) return false;\n // The highlighted element contains this row's element (highlighted is an ancestor)\n return (\n this.element !== this.highlightedElement && this.highlightedElement.contains(this.element)\n );\n }\n\n protected updated(changedProperties: PropertyValues): void {\n super.updated(changedProperties);\n\n // Update host classes based on interaction state\n if (changedProperties.has(\"highlightedElement\") || changedProperties.has(\"element\")) {\n this.classList.toggle(\"hovered\", this.isHovered);\n this.classList.toggle(\"ancestor-hovered\", this.isAncestorHovered);\n this.classList.toggle(\"descendant-hovered\", this.isDescendantHovered);\n }\n\n // Update selection classes\n if (changedProperties.has(\"selectedIds\") || changedProperties.has(\"element\")) {\n this.classList.toggle(\"selected\", this.isSelected);\n this.classList.toggle(\"ancestor-selected\", this.isAncestorSelected);\n }\n\n // Update root/nested timegroup classes\n if (changedProperties.has(\"element\")) {\n const isRoot = this.element instanceof EFTimegroup && this.element.isRootTimegroup;\n const isNested = this.element instanceof EFTimegroup && !this.element.isRootTimegroup;\n this.classList.toggle(\"root-timegroup\", isRoot);\n this.classList.toggle(\"nested-timegroup\", isNested);\n }\n }\n\n private handleMouseEnter = (): void => {\n // Skip hover interactions during active editing operations (scrubbing, trimming, etc.)\n if (this.editingContext && !this.editingContext.canInteract()) {\n return;\n }\n\n this.dispatchEvent(\n new CustomEvent(\"row-hover\", {\n detail: { element: this.element },\n bubbles: true,\n composed: true,\n }),\n );\n };\n\n private handleMouseLeave = (): void => {\n // Skip hover interactions during active editing operations (scrubbing, trimming, etc.)\n if (this.editingContext && !this.editingContext.canInteract()) {\n return;\n }\n\n this.dispatchEvent(\n new CustomEvent(\"row-hover\", {\n detail: { element: null },\n bubbles: true,\n composed: true,\n }),\n );\n };\n\n private handleClick = (e: Event): void => {\n e.stopPropagation();\n const elementId = (this.element as unknown as HTMLElement)?.id;\n if (elementId) {\n this.dispatchEvent(\n new CustomEvent(\"row-select\", {\n detail: { elementId, element: this.element },\n bubbles: true,\n composed: true,\n }),\n );\n }\n };\n\n private getElementType(element: Element): string {\n if (element instanceof EFVideo) return \"video\";\n if (element instanceof EFAudio) return \"audio\";\n if (element instanceof EFImage) return \"image\";\n if (element instanceof EFText) return \"text\";\n if (element instanceof EFCaptions) return \"captions\";\n if (element instanceof EFTimegroup) return \"timegroup\";\n return \"unknown\";\n }\n\n // Use shared type color utility from theme.ts\n // This reads from CSS variables (--ef-color-type-video, etc.) defined in ef-theme.css\n\n private getElementIcon(type: string): TemplateResult {\n const iconMap: Record<string, TemplateResult> = {\n video: phosphorIcon(ICONS.filmStrip, 14),\n audio: phosphorIcon(ICONS.speakerHigh, 14),\n image: phosphorIcon(ICONS.image, 14),\n text: phosphorIcon(ICONS.textT, 14),\n captions: phosphorIcon(ICONS.subtitles, 14),\n timegroup: phosphorIcon(ICONS.filmSlate, 14),\n unknown: phosphorIcon(ICONS.code, 14),\n };\n return iconMap[type] ?? iconMap.unknown!;\n }\n\n private getElementLabel(element: Element): string {\n const id = element.id || \"\";\n const type = this.getElementType(element);\n\n // If element has a meaningful ID (not auto-generated), use it\n if (id && !id.includes(\"-\") && !id.match(/^\\d+$/)) {\n return id;\n }\n\n // For auto-generated IDs, create a friendly name based on type\n // Count siblings of same type to generate \"Video 1\", \"Video 2\", etc.\n const parent = element.parentElement;\n if (parent) {\n const siblings = Array.from(parent.children).filter(\n (child) => this.getElementType(child) === type,\n );\n const index = siblings.indexOf(element) + 1;\n const typeLabels: Record<string, string> = {\n video: \"Video\",\n audio: \"Audio\",\n image: \"Image\",\n text: \"Text\",\n captions: \"Captions\",\n timegroup: \"Composition\",\n unknown: \"Layer\",\n };\n const label = typeLabels[type] || \"Layer\";\n\n // If there's only one of this type, don't add number\n if (siblings.length === 1) {\n return label;\n }\n return `${label} ${index}`;\n }\n\n // Fallback: capitalize the type\n return type.charAt(0).toUpperCase() + type.slice(1);\n }\n\n /**\n * Get additional detail text for the label (mode, preview, etc.)\n */\n private getElementDetail(element: Element): string | null {\n if (element instanceof EFTimegroup) {\n const mode = element.mode || \"fixed\";\n const modeLabels: Record<string, string> = {\n fixed: \"Fixed\",\n sequence: \"Sequence\",\n contain: \"Container\",\n };\n return modeLabels[mode] || mode;\n }\n if (element instanceof EFText) {\n // Get text preview\n const textContent = Array.from(element.childNodes)\n .filter((node) => node.nodeType === Node.TEXT_NODE)\n .map((node) => node.textContent?.trim())\n .filter(Boolean)\n .join(\" \");\n if (textContent) {\n return textContent.length > 20 ? textContent.slice(0, 20) + \"...\" : textContent;\n }\n }\n return null;\n }\n\n private renderTrack(): TemplateResult | typeof nothing {\n if (!this.element || !isEFTemporal(this.element)) return nothing;\n\n // For timegroups, use skip-children since children get their own rows\n if (this.element instanceof EFTimegroup) {\n // Show filmstrip for root timegroups (no parent timegroup)\n // Use the timegroup's own isRootTimegroup property for reliability\n const showFilmstrip = this.element.isRootTimegroup;\n return html`<ef-timegroup-track\n .element=${this.element}\n pixels-per-ms=${this.pixelsPerMs}\n ?enable-trim=${this.enableTrim}\n ?skip-children=${true}\n ?show-filmstrip=${showFilmstrip}\n .hideSelectors=${this.hideSelectors}\n .showSelectors=${this.showSelectors}\n ></ef-timegroup-track>`;\n }\n\n return html`${renderTrackChildren(\n [this.element as unknown as Element],\n this.pixelsPerMs,\n this.hideSelectors,\n this.showSelectors,\n true, // skipRootFiltering - the row itself handles filtering\n this.enableTrim,\n )}`;\n }\n\n connectedCallback(): void {\n super.connectedCallback();\n this.addEventListener(\"mouseenter\", this.handleMouseEnter);\n this.addEventListener(\"mouseleave\", this.handleMouseLeave);\n this.addEventListener(\"click\", this.handleClick);\n }\n\n disconnectedCallback(): void {\n super.disconnectedCallback();\n this.removeEventListener(\"mouseenter\", this.handleMouseEnter);\n this.removeEventListener(\"mouseleave\", this.handleMouseLeave);\n this.removeEventListener(\"click\", this.handleClick);\n }\n\n render() {\n if (!this.element) return nothing;\n\n const type = this.getElementType(this.element);\n const label = this.getElementLabel(this.element);\n const detail = this.getElementDetail(this.element);\n const typeColor = getElementTypeColor(type, this);\n const icon = this.getElementIcon(type);\n const indentPx = this.depth * INDENT_PX;\n\n return html`\n <div\n class=\"row-label\"\n part=\"label\"\n style=${styleMap({\n paddingLeft: `${indentPx}px`,\n borderLeftColor: typeColor,\n borderLeftWidth: \"3px\",\n borderLeftStyle: \"solid\",\n })}\n >\n <span style=\"color: ${typeColor}; opacity: 0.9; margin-right: 6px; flex-shrink: 0;\">\n ${icon}\n </span>\n <span style=\"flex-shrink: 0;\">${label}</span>\n ${\n detail\n ? html`\n <span style=\"margin-left: 6px; opacity: 0.6; font-size: 10px; overflow: hidden; text-overflow: ellipsis;\">\n ${detail}\n </span>\n `\n : nothing\n }\n </div>\n <div class=\"row-track\" part=\"track\">${this.renderTrack()}</div>\n `;\n }\n}\n\ndeclare global {\n interface HTMLElementTagNameMap {\n \"ef-timeline-row\": EFTimelineRow;\n }\n}\n"],"mappings":"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"}
@@ -1,7 +1,7 @@
1
1
  import { TimelineEditingContext } from "./timelineEditingContext.js";
2
- import * as lit31 from "lit";
2
+ import * as lit35 from "lit";
3
3
  import { LitElement } from "lit";
4
- import * as lit_html29 from "lit-html";
4
+ import * as lit_html32 from "lit-html";
5
5
 
6
6
  //#region src/gui/timeline/TrimHandles.d.ts
7
7
  interface TrimValue {
@@ -16,7 +16,7 @@ interface TrimChangeDetail {
16
16
  declare const EFTrimHandles_base: typeof LitElement;
17
17
  declare class EFTrimHandles extends EFTrimHandles_base {
18
18
  #private;
19
- static styles: lit31.CSSResult[];
19
+ static styles: lit35.CSSResult[];
20
20
  mode: "standalone" | "track";
21
21
  elementId: string;
22
22
  pixelsPerMs: number | null;
@@ -38,7 +38,7 @@ declare class EFTrimHandles extends EFTrimHandles_base {
38
38
  private handleRegionPointerDown;
39
39
  private handlePointerMove;
40
40
  private handlePointerUp;
41
- render(): lit_html29.TemplateResult<1>;
41
+ render(): lit_html32.TemplateResult<1>;
42
42
  }
43
43
  declare global {
44
44
  interface HTMLElementTagNameMap {
@@ -1 +1 @@
1
- {"version":3,"file":"TrimHandles.js","names":["EFTrimHandles","#effectivePixelsPerMs","#regionDragStartTrimStart","#regionDragStartTrimEnd","newValue: TrimValue","#emitChange","newValueMs: number","#seekToTarget","#hostWidth","#resizeObserver"],"sources":["../../../src/gui/timeline/TrimHandles.ts"],"sourcesContent":["import { consume } from \"@lit/context\";\nimport { css, html, LitElement, nothing } from \"lit\";\nimport { customElement, property, state } from \"lit/decorators.js\";\nimport { styleMap } from \"lit/directives/style-map.js\";\nimport { TWMixin } from \"../TWMixin.js\";\nimport {\n timelineEditingContext,\n type TimelineEditingContext,\n} from \"./timelineEditingContext.js\";\n\nexport interface TrimValue {\n startMs: number;\n endMs: number;\n}\n\nexport interface TrimChangeDetail {\n elementId: string;\n type: \"start\" | \"end\" | \"region\";\n value: TrimValue;\n}\n\n@customElement(\"ef-trim-handles\")\nexport class EFTrimHandles extends TWMixin(LitElement) {\n static styles = [\n css`\n :host {\n display: block;\n position: absolute;\n top: 0;\n left: 0;\n right: 0;\n bottom: 0;\n pointer-events: none;\n }\n\n .handle {\n position: absolute;\n top: 0;\n bottom: 0;\n width: var(--trim-handle-width, 8px);\n cursor: ew-resize;\n pointer-events: auto;\n z-index: 10;\n display: flex;\n align-items: center;\n justify-content: center;\n }\n\n .handle-inner {\n width: 100%;\n height: 100%;\n display: flex;\n align-items: center;\n justify-content: center;\n background: var(--trim-handle-color, rgba(255, 255, 255, 0.7));\n transition: background 0.15s ease;\n }\n\n .handle:hover .handle-inner,\n .handle.dragging .handle-inner {\n background: var(--trim-handle-active-color, #3b82f6);\n }\n\n .handle-start .handle-inner {\n border-radius: var(--trim-handle-border-radius-start, 2px 0 0 2px);\n }\n\n .handle-end .handle-inner {\n border-radius: var(--trim-handle-border-radius-end, 0 2px 2px 0);\n }\n\n /* Track mode: handles pinned at container edges */\n :host([mode=\"track\"]) .handle-start {\n left: -4px;\n }\n :host([mode=\"track\"]) .handle-end {\n right: -4px;\n }\n\n .handle.dragging {\n cursor: grabbing;\n }\n\n .trim-overlay {\n position: absolute;\n top: 0;\n bottom: 0;\n background: var(--trim-overlay-color, rgba(0, 0, 0, 0.4));\n pointer-events: none;\n }\n\n .trim-overlay-start {\n left: 0;\n }\n\n .trim-overlay-end {\n right: 0;\n }\n\n .region {\n position: absolute;\n top: 0;\n bottom: 0;\n cursor: grab;\n pointer-events: auto;\n z-index: 5;\n }\n\n .region.dragging {\n cursor: grabbing;\n }\n\n .selected-border {\n position: absolute;\n left: 0;\n right: 0;\n height: var(--trim-selected-border-width, 0px);\n background: var(--trim-selected-border-color, transparent);\n pointer-events: none;\n z-index: 15;\n }\n\n .selected-border-top {\n top: 0;\n }\n\n .selected-border-bottom {\n bottom: 0;\n }\n `,\n ];\n\n @property({ type: String, reflect: true })\n mode: \"standalone\" | \"track\" = \"standalone\";\n\n @property({ type: String, attribute: \"element-id\" })\n elementId = \"\";\n\n @property({ type: Number, attribute: \"pixels-per-ms\" })\n pixelsPerMs: number | null = null;\n\n @property({ attribute: false })\n value: TrimValue = { startMs: 0, endMs: 0 };\n\n @property({ type: Number, attribute: \"intrinsic-duration-ms\" })\n intrinsicDurationMs = 0;\n\n get trimStartMs(): number {\n return this.value.startMs;\n }\n set trimStartMs(v: number) {\n this.value = { ...this.value, startMs: v };\n }\n\n get trimEndMs(): number {\n return this.value.endMs;\n }\n set trimEndMs(v: number) {\n this.value = { ...this.value, endMs: v };\n }\n\n @property({ type: Boolean, attribute: \"show-overlays\" })\n showOverlays = true;\n\n @property({ type: String, attribute: \"seek-target\" })\n seekTarget = \"\";\n\n @consume({ context: timelineEditingContext, subscribe: true })\n editingContext?: TimelineEditingContext;\n\n @state()\n private draggingHandle: \"start\" | \"end\" | \"region\" | null = null;\n\n @state()\n private dragStartX = 0;\n\n @state()\n private dragStartValue = 0;\n\n #regionDragStartTrimStart = 0;\n #regionDragStartTrimEnd = 0;\n #resizeObserver: ResizeObserver | null = null;\n #hostWidth = 0;\n\n #emitChange(type: \"start\" | \"end\" | \"region\", value: TrimValue): void {\n this.dispatchEvent(\n new CustomEvent<TrimChangeDetail>(\"trim-change\", {\n detail: { elementId: this.elementId, type, value },\n bubbles: true,\n composed: true,\n }),\n );\n this.#seekToTarget(type, value);\n }\n\n #seekToTarget(type: \"start\" | \"end\" | \"region\", value: TrimValue): void {\n if (!this.seekTarget) return;\n const target = (this.getRootNode() as Document | ShadowRoot).getElementById(\n this.seekTarget,\n ) as any;\n if (!target || !(\"currentTimeMs\" in target)) return;\n\n if (type === \"end\") {\n target.currentTimeMs =\n this.intrinsicDurationMs - value.startMs - value.endMs;\n } else {\n target.currentTimeMs = 0;\n }\n }\n\n get #effectivePixelsPerMs(): number {\n if (this.pixelsPerMs != null) {\n return this.pixelsPerMs;\n }\n if (this.#hostWidth > 0 && this.intrinsicDurationMs > 0) {\n return this.#hostWidth / this.intrinsicDurationMs;\n }\n return 0.04;\n }\n\n connectedCallback(): void {\n super.connectedCallback();\n this.#resizeObserver = new ResizeObserver((entries) => {\n const entry = entries[0];\n if (!entry) return;\n const width = entry.contentRect.width;\n if (width !== this.#hostWidth) {\n this.#hostWidth = width;\n this.requestUpdate();\n }\n });\n this.#resizeObserver.observe(this);\n }\n\n disconnectedCallback(): void {\n super.disconnectedCallback();\n this.#resizeObserver?.disconnect();\n this.#resizeObserver = null;\n }\n\n private handlePointerDown(e: PointerEvent, type: \"start\" | \"end\"): void {\n e.preventDefault();\n e.stopPropagation();\n\n this.draggingHandle = type;\n this.dragStartX = e.clientX;\n this.dragStartValue = type === \"start\" ? this.trimStartMs : this.trimEndMs;\n\n if (this.editingContext) {\n this.editingContext.setState({\n mode: \"trimming\",\n elementId: this.elementId,\n handle: type,\n });\n }\n\n const target = e.currentTarget as HTMLElement;\n target.setPointerCapture(e.pointerId);\n\n target.addEventListener(\"pointermove\", this.handlePointerMove);\n target.addEventListener(\"pointerup\", this.handlePointerUp);\n target.addEventListener(\"pointercancel\", this.handlePointerUp);\n }\n\n private handleRegionPointerDown(e: PointerEvent): void {\n e.preventDefault();\n e.stopPropagation();\n\n this.draggingHandle = \"region\";\n this.dragStartX = e.clientX;\n this.#regionDragStartTrimStart = this.trimStartMs;\n this.#regionDragStartTrimEnd = this.trimEndMs;\n\n if (this.editingContext) {\n this.editingContext.setState({\n mode: \"trimming\",\n elementId: this.elementId,\n handle: \"start\",\n });\n }\n\n const target = e.currentTarget as HTMLElement;\n target.setPointerCapture(e.pointerId);\n\n target.addEventListener(\"pointermove\", this.handlePointerMove);\n target.addEventListener(\"pointerup\", this.handlePointerUp);\n target.addEventListener(\"pointercancel\", this.handlePointerUp);\n }\n\n private handlePointerMove = (e: PointerEvent): void => {\n if (!this.draggingHandle) return;\n\n const pxPerMs = this.#effectivePixelsPerMs;\n const deltaX = e.clientX - this.dragStartX;\n const deltaMs = deltaX / pxPerMs;\n\n if (this.draggingHandle === \"region\") {\n const clampedDelta = Math.max(\n -this.#regionDragStartTrimStart,\n Math.min(this.#regionDragStartTrimEnd, deltaMs),\n );\n\n const newValue: TrimValue = {\n startMs: this.#regionDragStartTrimStart + clampedDelta,\n endMs: this.#regionDragStartTrimEnd - clampedDelta,\n };\n\n this.#emitChange(\"region\", newValue);\n return;\n }\n\n let newValueMs: number;\n\n if (this.draggingHandle === \"start\") {\n newValueMs = Math.max(0, this.dragStartValue + deltaMs);\n newValueMs = Math.min(\n newValueMs,\n this.intrinsicDurationMs - (this.trimEndMs || 0),\n );\n\n this.#emitChange(\"start\", { startMs: newValueMs, endMs: this.trimEndMs });\n } else {\n newValueMs = Math.max(0, this.dragStartValue - deltaMs);\n newValueMs = Math.min(\n newValueMs,\n this.intrinsicDurationMs - this.trimStartMs,\n );\n\n this.#emitChange(\"end\", { startMs: this.trimStartMs, endMs: newValueMs });\n }\n };\n\n private handlePointerUp = (e: PointerEvent): void => {\n const target = e.currentTarget as HTMLElement;\n target.releasePointerCapture(e.pointerId);\n target.removeEventListener(\"pointermove\", this.handlePointerMove);\n target.removeEventListener(\"pointerup\", this.handlePointerUp);\n target.removeEventListener(\"pointercancel\", this.handlePointerUp);\n\n if (this.draggingHandle) {\n this.dispatchEvent(\n new CustomEvent(\"trim-change-end\", {\n detail: {\n elementId: this.elementId,\n type: this.draggingHandle,\n },\n bubbles: true,\n composed: true,\n }),\n );\n }\n\n this.draggingHandle = null;\n\n if (this.editingContext) {\n this.editingContext.setState({ mode: \"idle\" });\n }\n };\n\n render() {\n const pxPerMs = this.#effectivePixelsPerMs;\n const trimStartPx = this.trimStartMs * pxPerMs;\n const trimEndPx = (this.trimEndMs || 0) * pxPerMs;\n const isStandalone = this.mode === \"standalone\";\n const handleWidthPx =\n parseFloat(\n getComputedStyle(this).getPropertyValue(\"--trim-handle-width\"),\n ) || 8;\n\n return html`\n ${\n this.showOverlays && this.trimStartMs > 0\n ? html`<div\n class=\"trim-overlay trim-overlay-start\"\n style=${styleMap({ width: `${trimStartPx}px` })}\n ></div>`\n : nothing\n }\n ${\n this.showOverlays && this.trimEndMs > 0\n ? html`<div\n class=\"trim-overlay trim-overlay-end\"\n style=${styleMap({ width: `${trimEndPx}px` })}\n ></div>`\n : nothing\n }\n\n ${\n isStandalone\n ? html`\n <div\n class=\"region ${this.draggingHandle === \"region\" ? \"dragging\" : \"\"}\"\n style=${styleMap({\n left: `${trimStartPx + handleWidthPx}px`,\n right: `${trimEndPx + handleWidthPx}px`,\n })}\n @pointerdown=${(e: PointerEvent) => this.handleRegionPointerDown(e)}\n ></div>\n <div class=\"selected-border selected-border-top\"\n style=${styleMap({\n left: `${trimStartPx}px`,\n right: `${trimEndPx}px`,\n })}\n ></div>\n <div class=\"selected-border selected-border-bottom\"\n style=${styleMap({\n left: `${trimStartPx}px`,\n right: `${trimEndPx}px`,\n })}\n ></div>\n `\n : nothing\n }\n\n <div\n class=\"handle handle-start ${this.draggingHandle === \"start\" ? \"dragging\" : \"\"}\"\n style=${isStandalone ? styleMap({ left: `${trimStartPx}px` }) : nothing}\n @pointerdown=${(e: PointerEvent) => this.handlePointerDown(e, \"start\")}\n >\n <div class=\"handle-inner\">\n <slot name=\"handle-start\"></slot>\n </div>\n </div>\n <div\n class=\"handle handle-end ${this.draggingHandle === \"end\" ? \"dragging\" : \"\"}\"\n style=${isStandalone ? styleMap({ right: `${trimEndPx}px` }) : nothing}\n @pointerdown=${(e: PointerEvent) => this.handlePointerDown(e, \"end\")}\n >\n <div class=\"handle-inner\">\n <slot name=\"handle-end\"></slot>\n </div>\n </div>\n `;\n }\n}\n\ndeclare global {\n interface HTMLElementTagNameMap {\n \"ef-trim-handles\": EFTrimHandles;\n }\n}\n"],"mappings":";;;;;;;;;AAsBO,0BAAMA,wBAAsB,QAAQ,WAAW,CAAC;;;cA+GtB;mBAGnB;qBAGiB;eAGV;GAAE,SAAS;GAAG,OAAO;GAAG;6BAGrB;sBAiBP;oBAGF;wBAM+C;oBAGvC;wBAGI;4BAgHI,MAA0B;AACrD,OAAI,CAAC,KAAK,eAAgB;GAE1B,MAAM,UAAU,MAAKC;GAErB,MAAM,WADS,EAAE,UAAU,KAAK,cACP;AAEzB,OAAI,KAAK,mBAAmB,UAAU;IACpC,MAAM,eAAe,KAAK,IACxB,CAAC,MAAKC,0BACN,KAAK,IAAI,MAAKC,wBAAyB,QAAQ,CAChD;IAED,MAAMC,WAAsB;KAC1B,SAAS,MAAKF,2BAA4B;KAC1C,OAAO,MAAKC,yBAA0B;KACvC;AAED,UAAKE,WAAY,UAAU,SAAS;AACpC;;GAGF,IAAIC;AAEJ,OAAI,KAAK,mBAAmB,SAAS;AACnC,iBAAa,KAAK,IAAI,GAAG,KAAK,iBAAiB,QAAQ;AACvD,iBAAa,KAAK,IAChB,YACA,KAAK,uBAAuB,KAAK,aAAa,GAC/C;AAED,UAAKD,WAAY,SAAS;KAAE,SAAS;KAAY,OAAO,KAAK;KAAW,CAAC;UACpE;AACL,iBAAa,KAAK,IAAI,GAAG,KAAK,iBAAiB,QAAQ;AACvD,iBAAa,KAAK,IAChB,YACA,KAAK,sBAAsB,KAAK,YACjC;AAED,UAAKA,WAAY,OAAO;KAAE,SAAS,KAAK;KAAa,OAAO;KAAY,CAAC;;;0BAIlD,MAA0B;GACnD,MAAM,SAAS,EAAE;AACjB,UAAO,sBAAsB,EAAE,UAAU;AACzC,UAAO,oBAAoB,eAAe,KAAK,kBAAkB;AACjE,UAAO,oBAAoB,aAAa,KAAK,gBAAgB;AAC7D,UAAO,oBAAoB,iBAAiB,KAAK,gBAAgB;AAEjE,OAAI,KAAK,eACP,MAAK,cACH,IAAI,YAAY,mBAAmB;IACjC,QAAQ;KACN,WAAW,KAAK;KAChB,MAAM,KAAK;KACZ;IACD,SAAS;IACT,UAAU;IACX,CAAC,CACH;AAGH,QAAK,iBAAiB;AAEtB,OAAI,KAAK,eACP,MAAK,eAAe,SAAS,EAAE,MAAM,QAAQ,CAAC;;;;gBA5UlC,CACd,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;MA0GJ;;CAiBD,IAAI,cAAsB;AACxB,SAAO,KAAK,MAAM;;CAEpB,IAAI,YAAY,GAAW;AACzB,OAAK,QAAQ;GAAE,GAAG,KAAK;GAAO,SAAS;GAAG;;CAG5C,IAAI,YAAoB;AACtB,SAAO,KAAK,MAAM;;CAEpB,IAAI,UAAU,GAAW;AACvB,OAAK,QAAQ;GAAE,GAAG,KAAK;GAAO,OAAO;GAAG;;CAqB1C,4BAA4B;CAC5B,0BAA0B;CAC1B,kBAAyC;CACzC,aAAa;CAEb,YAAY,MAAkC,OAAwB;AACpE,OAAK,cACH,IAAI,YAA8B,eAAe;GAC/C,QAAQ;IAAE,WAAW,KAAK;IAAW;IAAM;IAAO;GAClD,SAAS;GACT,UAAU;GACX,CAAC,CACH;AACD,QAAKE,aAAc,MAAM,MAAM;;CAGjC,cAAc,MAAkC,OAAwB;AACtE,MAAI,CAAC,KAAK,WAAY;EACtB,MAAM,SAAU,KAAK,aAAa,CAA2B,eAC3D,KAAK,WACN;AACD,MAAI,CAAC,UAAU,EAAE,mBAAmB,QAAS;AAE7C,MAAI,SAAS,MACX,QAAO,gBACL,KAAK,sBAAsB,MAAM,UAAU,MAAM;MAEnD,QAAO,gBAAgB;;CAI3B,KAAIN,uBAAgC;AAClC,MAAI,KAAK,eAAe,KACtB,QAAO,KAAK;AAEd,MAAI,MAAKO,YAAa,KAAK,KAAK,sBAAsB,EACpD,QAAO,MAAKA,YAAa,KAAK;AAEhC,SAAO;;CAGT,oBAA0B;AACxB,QAAM,mBAAmB;AACzB,QAAKC,iBAAkB,IAAI,gBAAgB,YAAY;GACrD,MAAM,QAAQ,QAAQ;AACtB,OAAI,CAAC,MAAO;GACZ,MAAM,QAAQ,MAAM,YAAY;AAChC,OAAI,UAAU,MAAKD,WAAY;AAC7B,UAAKA,YAAa;AAClB,SAAK,eAAe;;IAEtB;AACF,QAAKC,eAAgB,QAAQ,KAAK;;CAGpC,uBAA6B;AAC3B,QAAM,sBAAsB;AAC5B,QAAKA,gBAAiB,YAAY;AAClC,QAAKA,iBAAkB;;CAGzB,AAAQ,kBAAkB,GAAiB,MAA6B;AACtE,IAAE,gBAAgB;AAClB,IAAE,iBAAiB;AAEnB,OAAK,iBAAiB;AACtB,OAAK,aAAa,EAAE;AACpB,OAAK,iBAAiB,SAAS,UAAU,KAAK,cAAc,KAAK;AAEjE,MAAI,KAAK,eACP,MAAK,eAAe,SAAS;GAC3B,MAAM;GACN,WAAW,KAAK;GAChB,QAAQ;GACT,CAAC;EAGJ,MAAM,SAAS,EAAE;AACjB,SAAO,kBAAkB,EAAE,UAAU;AAErC,SAAO,iBAAiB,eAAe,KAAK,kBAAkB;AAC9D,SAAO,iBAAiB,aAAa,KAAK,gBAAgB;AAC1D,SAAO,iBAAiB,iBAAiB,KAAK,gBAAgB;;CAGhE,AAAQ,wBAAwB,GAAuB;AACrD,IAAE,gBAAgB;AAClB,IAAE,iBAAiB;AAEnB,OAAK,iBAAiB;AACtB,OAAK,aAAa,EAAE;AACpB,QAAKP,2BAA4B,KAAK;AACtC,QAAKC,yBAA0B,KAAK;AAEpC,MAAI,KAAK,eACP,MAAK,eAAe,SAAS;GAC3B,MAAM;GACN,WAAW,KAAK;GAChB,QAAQ;GACT,CAAC;EAGJ,MAAM,SAAS,EAAE;AACjB,SAAO,kBAAkB,EAAE,UAAU;AAErC,SAAO,iBAAiB,eAAe,KAAK,kBAAkB;AAC9D,SAAO,iBAAiB,aAAa,KAAK,gBAAgB;AAC1D,SAAO,iBAAiB,iBAAiB,KAAK,gBAAgB;;CAyEhE,SAAS;EACP,MAAM,UAAU,MAAKF;EACrB,MAAM,cAAc,KAAK,cAAc;EACvC,MAAM,aAAa,KAAK,aAAa,KAAK;EAC1C,MAAM,eAAe,KAAK,SAAS;EACnC,MAAM,gBACJ,WACE,iBAAiB,KAAK,CAAC,iBAAiB,sBAAsB,CAC/D,IAAI;AAEP,SAAO,IAAI;QAEP,KAAK,gBAAgB,KAAK,cAAc,IACpC,IAAI;;oBAEI,SAAS,EAAE,OAAO,GAAG,YAAY,KAAK,CAAC,CAAC;qBAEhD,QACL;QAEC,KAAK,gBAAgB,KAAK,YAAY,IAClC,IAAI;;oBAEI,SAAS,EAAE,OAAO,GAAG,UAAU,KAAK,CAAC,CAAC;qBAE9C,QACL;;QAGC,eACI,IAAI;;gCAEgB,KAAK,mBAAmB,WAAW,aAAa,GAAG;wBAC3D,SAAS;GACf,MAAM,GAAG,cAAc,cAAc;GACrC,OAAO,GAAG,YAAY,cAAc;GACrC,CAAC,CAAC;gCACa,MAAoB,KAAK,wBAAwB,EAAE,CAAC;;;wBAG5D,SAAS;GACf,MAAM,GAAG,YAAY;GACrB,OAAO,GAAG,UAAU;GACrB,CAAC,CAAC;;;wBAGK,SAAS;GACf,MAAM,GAAG,YAAY;GACrB,OAAO,GAAG,UAAU;GACrB,CAAC,CAAC;;gBAGP,QACL;;;qCAG8B,KAAK,mBAAmB,UAAU,aAAa,GAAG;gBACvE,eAAe,SAAS,EAAE,MAAM,GAAG,YAAY,KAAK,CAAC,GAAG,QAAQ;wBACxD,MAAoB,KAAK,kBAAkB,GAAG,QAAQ,CAAC;;;;;;;mCAO5C,KAAK,mBAAmB,QAAQ,aAAa,GAAG;gBACnE,eAAe,SAAS,EAAE,OAAO,GAAG,UAAU,KAAK,CAAC,GAAG,QAAQ;wBACvD,MAAoB,KAAK,kBAAkB,GAAG,MAAM,CAAC;;;;;;;;;YAtS1E,SAAS;CAAE,MAAM;CAAQ,SAAS;CAAM,CAAC;YAGzC,SAAS;CAAE,MAAM;CAAQ,WAAW;CAAc,CAAC;YAGnD,SAAS;CAAE,MAAM;CAAQ,WAAW;CAAiB,CAAC;YAGtD,SAAS,EAAE,WAAW,OAAO,CAAC;YAG9B,SAAS;CAAE,MAAM;CAAQ,WAAW;CAAyB,CAAC;YAiB9D,SAAS;CAAE,MAAM;CAAS,WAAW;CAAiB,CAAC;YAGvD,SAAS;CAAE,MAAM;CAAQ,WAAW;CAAe,CAAC;YAGpD,QAAQ;CAAE,SAAS;CAAwB,WAAW;CAAM,CAAC;YAG7D,OAAO;YAGP,OAAO;YAGP,OAAO;4BA3JT,cAAc,kBAAkB"}
1
+ {"version":3,"file":"TrimHandles.js","names":["EFTrimHandles","#effectivePixelsPerMs","#regionDragStartTrimStart","#regionDragStartTrimEnd","newValue: TrimValue","#emitChange","newValueMs: number","#seekToTarget","#hostWidth","#resizeObserver"],"sources":["../../../src/gui/timeline/TrimHandles.ts"],"sourcesContent":["import { consume } from \"@lit/context\";\nimport { css, html, LitElement, nothing } from \"lit\";\nimport { customElement, property, state } from \"lit/decorators.js\";\nimport { styleMap } from \"lit/directives/style-map.js\";\nimport { TWMixin } from \"../TWMixin.js\";\nimport { timelineEditingContext, type TimelineEditingContext } from \"./timelineEditingContext.js\";\n\nexport interface TrimValue {\n startMs: number;\n endMs: number;\n}\n\nexport interface TrimChangeDetail {\n elementId: string;\n type: \"start\" | \"end\" | \"region\";\n value: TrimValue;\n}\n\n@customElement(\"ef-trim-handles\")\nexport class EFTrimHandles extends TWMixin(LitElement) {\n static styles = [\n css`\n :host {\n display: block;\n position: absolute;\n top: 0;\n left: 0;\n right: 0;\n bottom: 0;\n pointer-events: none;\n }\n\n .handle {\n position: absolute;\n top: 0;\n bottom: 0;\n width: var(--trim-handle-width, 8px);\n cursor: ew-resize;\n pointer-events: auto;\n z-index: 10;\n display: flex;\n align-items: center;\n justify-content: center;\n }\n\n .handle-inner {\n width: 100%;\n height: 100%;\n display: flex;\n align-items: center;\n justify-content: center;\n background: var(--trim-handle-color, rgba(255, 255, 255, 0.7));\n transition: background 0.15s ease;\n }\n\n .handle:hover .handle-inner,\n .handle.dragging .handle-inner {\n background: var(--trim-handle-active-color, #3b82f6);\n }\n\n .handle-start .handle-inner {\n border-radius: var(--trim-handle-border-radius-start, 2px 0 0 2px);\n }\n\n .handle-end .handle-inner {\n border-radius: var(--trim-handle-border-radius-end, 0 2px 2px 0);\n }\n\n /* Track mode: handles pinned at container edges */\n :host([mode=\"track\"]) .handle-start {\n left: -4px;\n }\n :host([mode=\"track\"]) .handle-end {\n right: -4px;\n }\n\n .handle.dragging {\n cursor: grabbing;\n }\n\n .trim-overlay {\n position: absolute;\n top: 0;\n bottom: 0;\n background: var(--trim-overlay-color, rgba(0, 0, 0, 0.4));\n pointer-events: none;\n }\n\n .trim-overlay-start {\n left: 0;\n }\n\n .trim-overlay-end {\n right: 0;\n }\n\n .region {\n position: absolute;\n top: 0;\n bottom: 0;\n cursor: grab;\n pointer-events: auto;\n z-index: 5;\n }\n\n .region.dragging {\n cursor: grabbing;\n }\n\n .selected-border {\n position: absolute;\n left: 0;\n right: 0;\n height: var(--trim-selected-border-width, 0px);\n background: var(--trim-selected-border-color, transparent);\n pointer-events: none;\n z-index: 15;\n }\n\n .selected-border-top {\n top: 0;\n }\n\n .selected-border-bottom {\n bottom: 0;\n }\n `,\n ];\n\n @property({ type: String, reflect: true })\n mode: \"standalone\" | \"track\" = \"standalone\";\n\n @property({ type: String, attribute: \"element-id\" })\n elementId = \"\";\n\n @property({ type: Number, attribute: \"pixels-per-ms\" })\n pixelsPerMs: number | null = null;\n\n @property({ attribute: false })\n value: TrimValue = { startMs: 0, endMs: 0 };\n\n @property({ type: Number, attribute: \"intrinsic-duration-ms\" })\n intrinsicDurationMs = 0;\n\n get trimStartMs(): number {\n return this.value.startMs;\n }\n set trimStartMs(v: number) {\n this.value = { ...this.value, startMs: v };\n }\n\n get trimEndMs(): number {\n return this.value.endMs;\n }\n set trimEndMs(v: number) {\n this.value = { ...this.value, endMs: v };\n }\n\n @property({ type: Boolean, attribute: \"show-overlays\" })\n showOverlays = true;\n\n @property({ type: String, attribute: \"seek-target\" })\n seekTarget = \"\";\n\n @consume({ context: timelineEditingContext, subscribe: true })\n editingContext?: TimelineEditingContext;\n\n @state()\n private draggingHandle: \"start\" | \"end\" | \"region\" | null = null;\n\n @state()\n private dragStartX = 0;\n\n @state()\n private dragStartValue = 0;\n\n #regionDragStartTrimStart = 0;\n #regionDragStartTrimEnd = 0;\n #resizeObserver: ResizeObserver | null = null;\n #hostWidth = 0;\n\n #emitChange(type: \"start\" | \"end\" | \"region\", value: TrimValue): void {\n this.dispatchEvent(\n new CustomEvent<TrimChangeDetail>(\"trim-change\", {\n detail: { elementId: this.elementId, type, value },\n bubbles: true,\n composed: true,\n }),\n );\n this.#seekToTarget(type, value);\n }\n\n #seekToTarget(type: \"start\" | \"end\" | \"region\", value: TrimValue): void {\n if (!this.seekTarget) return;\n const target = (this.getRootNode() as Document | ShadowRoot).getElementById(\n this.seekTarget,\n ) as any;\n if (!target || !(\"currentTimeMs\" in target)) return;\n\n if (type === \"end\") {\n target.currentTimeMs = this.intrinsicDurationMs - value.startMs - value.endMs;\n } else {\n target.currentTimeMs = 0;\n }\n }\n\n get #effectivePixelsPerMs(): number {\n if (this.pixelsPerMs != null) {\n return this.pixelsPerMs;\n }\n if (this.#hostWidth > 0 && this.intrinsicDurationMs > 0) {\n return this.#hostWidth / this.intrinsicDurationMs;\n }\n return 0.04;\n }\n\n connectedCallback(): void {\n super.connectedCallback();\n this.#resizeObserver = new ResizeObserver((entries) => {\n const entry = entries[0];\n if (!entry) return;\n const width = entry.contentRect.width;\n if (width !== this.#hostWidth) {\n this.#hostWidth = width;\n this.requestUpdate();\n }\n });\n this.#resizeObserver.observe(this);\n }\n\n disconnectedCallback(): void {\n super.disconnectedCallback();\n this.#resizeObserver?.disconnect();\n this.#resizeObserver = null;\n }\n\n private handlePointerDown(e: PointerEvent, type: \"start\" | \"end\"): void {\n e.preventDefault();\n e.stopPropagation();\n\n this.draggingHandle = type;\n this.dragStartX = e.clientX;\n this.dragStartValue = type === \"start\" ? this.trimStartMs : this.trimEndMs;\n\n if (this.editingContext) {\n this.editingContext.setState({\n mode: \"trimming\",\n elementId: this.elementId,\n handle: type,\n });\n }\n\n const target = e.currentTarget as HTMLElement;\n target.setPointerCapture(e.pointerId);\n\n target.addEventListener(\"pointermove\", this.handlePointerMove);\n target.addEventListener(\"pointerup\", this.handlePointerUp);\n target.addEventListener(\"pointercancel\", this.handlePointerUp);\n }\n\n private handleRegionPointerDown(e: PointerEvent): void {\n e.preventDefault();\n e.stopPropagation();\n\n this.draggingHandle = \"region\";\n this.dragStartX = e.clientX;\n this.#regionDragStartTrimStart = this.trimStartMs;\n this.#regionDragStartTrimEnd = this.trimEndMs;\n\n if (this.editingContext) {\n this.editingContext.setState({\n mode: \"trimming\",\n elementId: this.elementId,\n handle: \"start\",\n });\n }\n\n const target = e.currentTarget as HTMLElement;\n target.setPointerCapture(e.pointerId);\n\n target.addEventListener(\"pointermove\", this.handlePointerMove);\n target.addEventListener(\"pointerup\", this.handlePointerUp);\n target.addEventListener(\"pointercancel\", this.handlePointerUp);\n }\n\n private handlePointerMove = (e: PointerEvent): void => {\n if (!this.draggingHandle) return;\n\n const pxPerMs = this.#effectivePixelsPerMs;\n const deltaX = e.clientX - this.dragStartX;\n const deltaMs = deltaX / pxPerMs;\n\n if (this.draggingHandle === \"region\") {\n const clampedDelta = Math.max(\n -this.#regionDragStartTrimStart,\n Math.min(this.#regionDragStartTrimEnd, deltaMs),\n );\n\n const newValue: TrimValue = {\n startMs: this.#regionDragStartTrimStart + clampedDelta,\n endMs: this.#regionDragStartTrimEnd - clampedDelta,\n };\n\n this.#emitChange(\"region\", newValue);\n return;\n }\n\n let newValueMs: number;\n\n if (this.draggingHandle === \"start\") {\n newValueMs = Math.max(0, this.dragStartValue + deltaMs);\n newValueMs = Math.min(newValueMs, this.intrinsicDurationMs - (this.trimEndMs || 0));\n\n this.#emitChange(\"start\", { startMs: newValueMs, endMs: this.trimEndMs });\n } else {\n newValueMs = Math.max(0, this.dragStartValue - deltaMs);\n newValueMs = Math.min(newValueMs, this.intrinsicDurationMs - this.trimStartMs);\n\n this.#emitChange(\"end\", { startMs: this.trimStartMs, endMs: newValueMs });\n }\n };\n\n private handlePointerUp = (e: PointerEvent): void => {\n const target = e.currentTarget as HTMLElement;\n target.releasePointerCapture(e.pointerId);\n target.removeEventListener(\"pointermove\", this.handlePointerMove);\n target.removeEventListener(\"pointerup\", this.handlePointerUp);\n target.removeEventListener(\"pointercancel\", this.handlePointerUp);\n\n if (this.draggingHandle) {\n this.dispatchEvent(\n new CustomEvent(\"trim-change-end\", {\n detail: {\n elementId: this.elementId,\n type: this.draggingHandle,\n },\n bubbles: true,\n composed: true,\n }),\n );\n }\n\n this.draggingHandle = null;\n\n if (this.editingContext) {\n this.editingContext.setState({ mode: \"idle\" });\n }\n };\n\n render() {\n const pxPerMs = this.#effectivePixelsPerMs;\n const trimStartPx = this.trimStartMs * pxPerMs;\n const trimEndPx = (this.trimEndMs || 0) * pxPerMs;\n const isStandalone = this.mode === \"standalone\";\n const handleWidthPx =\n parseFloat(getComputedStyle(this).getPropertyValue(\"--trim-handle-width\")) || 8;\n\n return html`\n ${\n this.showOverlays && this.trimStartMs > 0\n ? html`<div\n class=\"trim-overlay trim-overlay-start\"\n style=${styleMap({ width: `${trimStartPx}px` })}\n ></div>`\n : nothing\n }\n ${\n this.showOverlays && this.trimEndMs > 0\n ? html`<div\n class=\"trim-overlay trim-overlay-end\"\n style=${styleMap({ width: `${trimEndPx}px` })}\n ></div>`\n : nothing\n }\n\n ${\n isStandalone\n ? html`\n <div\n class=\"region ${this.draggingHandle === \"region\" ? \"dragging\" : \"\"}\"\n style=${styleMap({\n left: `${trimStartPx + handleWidthPx}px`,\n right: `${trimEndPx + handleWidthPx}px`,\n })}\n @pointerdown=${(e: PointerEvent) => this.handleRegionPointerDown(e)}\n ></div>\n <div class=\"selected-border selected-border-top\"\n style=${styleMap({\n left: `${trimStartPx}px`,\n right: `${trimEndPx}px`,\n })}\n ></div>\n <div class=\"selected-border selected-border-bottom\"\n style=${styleMap({\n left: `${trimStartPx}px`,\n right: `${trimEndPx}px`,\n })}\n ></div>\n `\n : nothing\n }\n\n <div\n class=\"handle handle-start ${this.draggingHandle === \"start\" ? \"dragging\" : \"\"}\"\n style=${isStandalone ? styleMap({ left: `${trimStartPx}px` }) : nothing}\n @pointerdown=${(e: PointerEvent) => this.handlePointerDown(e, \"start\")}\n >\n <div class=\"handle-inner\">\n <slot name=\"handle-start\"></slot>\n </div>\n </div>\n <div\n class=\"handle handle-end ${this.draggingHandle === \"end\" ? \"dragging\" : \"\"}\"\n style=${isStandalone ? styleMap({ right: `${trimEndPx}px` }) : nothing}\n @pointerdown=${(e: PointerEvent) => this.handlePointerDown(e, \"end\")}\n >\n <div class=\"handle-inner\">\n <slot name=\"handle-end\"></slot>\n </div>\n </div>\n `;\n }\n}\n\ndeclare global {\n interface HTMLElementTagNameMap {\n \"ef-trim-handles\": EFTrimHandles;\n }\n}\n"],"mappings":";;;;;;;;;AAmBO,0BAAMA,wBAAsB,QAAQ,WAAW,CAAC;;;cA+GtB;mBAGnB;qBAGiB;eAGV;GAAE,SAAS;GAAG,OAAO;GAAG;6BAGrB;sBAiBP;oBAGF;wBAM+C;oBAGvC;wBAGI;4BA+GI,MAA0B;AACrD,OAAI,CAAC,KAAK,eAAgB;GAE1B,MAAM,UAAU,MAAKC;GAErB,MAAM,WADS,EAAE,UAAU,KAAK,cACP;AAEzB,OAAI,KAAK,mBAAmB,UAAU;IACpC,MAAM,eAAe,KAAK,IACxB,CAAC,MAAKC,0BACN,KAAK,IAAI,MAAKC,wBAAyB,QAAQ,CAChD;IAED,MAAMC,WAAsB;KAC1B,SAAS,MAAKF,2BAA4B;KAC1C,OAAO,MAAKC,yBAA0B;KACvC;AAED,UAAKE,WAAY,UAAU,SAAS;AACpC;;GAGF,IAAIC;AAEJ,OAAI,KAAK,mBAAmB,SAAS;AACnC,iBAAa,KAAK,IAAI,GAAG,KAAK,iBAAiB,QAAQ;AACvD,iBAAa,KAAK,IAAI,YAAY,KAAK,uBAAuB,KAAK,aAAa,GAAG;AAEnF,UAAKD,WAAY,SAAS;KAAE,SAAS;KAAY,OAAO,KAAK;KAAW,CAAC;UACpE;AACL,iBAAa,KAAK,IAAI,GAAG,KAAK,iBAAiB,QAAQ;AACvD,iBAAa,KAAK,IAAI,YAAY,KAAK,sBAAsB,KAAK,YAAY;AAE9E,UAAKA,WAAY,OAAO;KAAE,SAAS,KAAK;KAAa,OAAO;KAAY,CAAC;;;0BAIlD,MAA0B;GACnD,MAAM,SAAS,EAAE;AACjB,UAAO,sBAAsB,EAAE,UAAU;AACzC,UAAO,oBAAoB,eAAe,KAAK,kBAAkB;AACjE,UAAO,oBAAoB,aAAa,KAAK,gBAAgB;AAC7D,UAAO,oBAAoB,iBAAiB,KAAK,gBAAgB;AAEjE,OAAI,KAAK,eACP,MAAK,cACH,IAAI,YAAY,mBAAmB;IACjC,QAAQ;KACN,WAAW,KAAK;KAChB,MAAM,KAAK;KACZ;IACD,SAAS;IACT,UAAU;IACX,CAAC,CACH;AAGH,QAAK,iBAAiB;AAEtB,OAAI,KAAK,eACP,MAAK,eAAe,SAAS,EAAE,MAAM,QAAQ,CAAC;;;;gBArUlC,CACd,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;MA0GJ;;CAiBD,IAAI,cAAsB;AACxB,SAAO,KAAK,MAAM;;CAEpB,IAAI,YAAY,GAAW;AACzB,OAAK,QAAQ;GAAE,GAAG,KAAK;GAAO,SAAS;GAAG;;CAG5C,IAAI,YAAoB;AACtB,SAAO,KAAK,MAAM;;CAEpB,IAAI,UAAU,GAAW;AACvB,OAAK,QAAQ;GAAE,GAAG,KAAK;GAAO,OAAO;GAAG;;CAqB1C,4BAA4B;CAC5B,0BAA0B;CAC1B,kBAAyC;CACzC,aAAa;CAEb,YAAY,MAAkC,OAAwB;AACpE,OAAK,cACH,IAAI,YAA8B,eAAe;GAC/C,QAAQ;IAAE,WAAW,KAAK;IAAW;IAAM;IAAO;GAClD,SAAS;GACT,UAAU;GACX,CAAC,CACH;AACD,QAAKE,aAAc,MAAM,MAAM;;CAGjC,cAAc,MAAkC,OAAwB;AACtE,MAAI,CAAC,KAAK,WAAY;EACtB,MAAM,SAAU,KAAK,aAAa,CAA2B,eAC3D,KAAK,WACN;AACD,MAAI,CAAC,UAAU,EAAE,mBAAmB,QAAS;AAE7C,MAAI,SAAS,MACX,QAAO,gBAAgB,KAAK,sBAAsB,MAAM,UAAU,MAAM;MAExE,QAAO,gBAAgB;;CAI3B,KAAIN,uBAAgC;AAClC,MAAI,KAAK,eAAe,KACtB,QAAO,KAAK;AAEd,MAAI,MAAKO,YAAa,KAAK,KAAK,sBAAsB,EACpD,QAAO,MAAKA,YAAa,KAAK;AAEhC,SAAO;;CAGT,oBAA0B;AACxB,QAAM,mBAAmB;AACzB,QAAKC,iBAAkB,IAAI,gBAAgB,YAAY;GACrD,MAAM,QAAQ,QAAQ;AACtB,OAAI,CAAC,MAAO;GACZ,MAAM,QAAQ,MAAM,YAAY;AAChC,OAAI,UAAU,MAAKD,WAAY;AAC7B,UAAKA,YAAa;AAClB,SAAK,eAAe;;IAEtB;AACF,QAAKC,eAAgB,QAAQ,KAAK;;CAGpC,uBAA6B;AAC3B,QAAM,sBAAsB;AAC5B,QAAKA,gBAAiB,YAAY;AAClC,QAAKA,iBAAkB;;CAGzB,AAAQ,kBAAkB,GAAiB,MAA6B;AACtE,IAAE,gBAAgB;AAClB,IAAE,iBAAiB;AAEnB,OAAK,iBAAiB;AACtB,OAAK,aAAa,EAAE;AACpB,OAAK,iBAAiB,SAAS,UAAU,KAAK,cAAc,KAAK;AAEjE,MAAI,KAAK,eACP,MAAK,eAAe,SAAS;GAC3B,MAAM;GACN,WAAW,KAAK;GAChB,QAAQ;GACT,CAAC;EAGJ,MAAM,SAAS,EAAE;AACjB,SAAO,kBAAkB,EAAE,UAAU;AAErC,SAAO,iBAAiB,eAAe,KAAK,kBAAkB;AAC9D,SAAO,iBAAiB,aAAa,KAAK,gBAAgB;AAC1D,SAAO,iBAAiB,iBAAiB,KAAK,gBAAgB;;CAGhE,AAAQ,wBAAwB,GAAuB;AACrD,IAAE,gBAAgB;AAClB,IAAE,iBAAiB;AAEnB,OAAK,iBAAiB;AACtB,OAAK,aAAa,EAAE;AACpB,QAAKP,2BAA4B,KAAK;AACtC,QAAKC,yBAA0B,KAAK;AAEpC,MAAI,KAAK,eACP,MAAK,eAAe,SAAS;GAC3B,MAAM;GACN,WAAW,KAAK;GAChB,QAAQ;GACT,CAAC;EAGJ,MAAM,SAAS,EAAE;AACjB,SAAO,kBAAkB,EAAE,UAAU;AAErC,SAAO,iBAAiB,eAAe,KAAK,kBAAkB;AAC9D,SAAO,iBAAiB,aAAa,KAAK,gBAAgB;AAC1D,SAAO,iBAAiB,iBAAiB,KAAK,gBAAgB;;CAmEhE,SAAS;EACP,MAAM,UAAU,MAAKF;EACrB,MAAM,cAAc,KAAK,cAAc;EACvC,MAAM,aAAa,KAAK,aAAa,KAAK;EAC1C,MAAM,eAAe,KAAK,SAAS;EACnC,MAAM,gBACJ,WAAW,iBAAiB,KAAK,CAAC,iBAAiB,sBAAsB,CAAC,IAAI;AAEhF,SAAO,IAAI;QAEP,KAAK,gBAAgB,KAAK,cAAc,IACpC,IAAI;;oBAEI,SAAS,EAAE,OAAO,GAAG,YAAY,KAAK,CAAC,CAAC;qBAEhD,QACL;QAEC,KAAK,gBAAgB,KAAK,YAAY,IAClC,IAAI;;oBAEI,SAAS,EAAE,OAAO,GAAG,UAAU,KAAK,CAAC,CAAC;qBAE9C,QACL;;QAGC,eACI,IAAI;;gCAEgB,KAAK,mBAAmB,WAAW,aAAa,GAAG;wBAC3D,SAAS;GACf,MAAM,GAAG,cAAc,cAAc;GACrC,OAAO,GAAG,YAAY,cAAc;GACrC,CAAC,CAAC;gCACa,MAAoB,KAAK,wBAAwB,EAAE,CAAC;;;wBAG5D,SAAS;GACf,MAAM,GAAG,YAAY;GACrB,OAAO,GAAG,UAAU;GACrB,CAAC,CAAC;;;wBAGK,SAAS;GACf,MAAM,GAAG,YAAY;GACrB,OAAO,GAAG,UAAU;GACrB,CAAC,CAAC;;gBAGP,QACL;;;qCAG8B,KAAK,mBAAmB,UAAU,aAAa,GAAG;gBACvE,eAAe,SAAS,EAAE,MAAM,GAAG,YAAY,KAAK,CAAC,GAAG,QAAQ;wBACxD,MAAoB,KAAK,kBAAkB,GAAG,QAAQ,CAAC;;;;;;;mCAO5C,KAAK,mBAAmB,QAAQ,aAAa,GAAG;gBACnE,eAAe,SAAS,EAAE,OAAO,GAAG,UAAU,KAAK,CAAC,GAAG,QAAQ;wBACvD,MAAoB,KAAK,kBAAkB,GAAG,MAAM,CAAC;;;;;;;;;YA7R1E,SAAS;CAAE,MAAM;CAAQ,SAAS;CAAM,CAAC;YAGzC,SAAS;CAAE,MAAM;CAAQ,WAAW;CAAc,CAAC;YAGnD,SAAS;CAAE,MAAM;CAAQ,WAAW;CAAiB,CAAC;YAGtD,SAAS,EAAE,WAAW,OAAO,CAAC;YAG9B,SAAS;CAAE,MAAM;CAAQ,WAAW;CAAyB,CAAC;YAiB9D,SAAS;CAAE,MAAM;CAAS,WAAW;CAAiB,CAAC;YAGvD,SAAS;CAAE,MAAM;CAAQ,WAAW;CAAe,CAAC;YAGpD,QAAQ;CAAE,SAAS;CAAwB,WAAW;CAAM,CAAC;YAG7D,OAAO;YAGP,OAAO;YAGP,OAAO;4BA3JT,cAAc,kBAAkB"}
@@ -1 +1 @@
1
- {"version":3,"file":"flattenHierarchy.js","names":["rows: TimelineRowModel[]"],"sources":["../../../src/gui/timeline/flattenHierarchy.ts"],"sourcesContent":["import {\n isEFTemporal,\n type TemporalMixinInterface,\n} from \"../../elements/EFTemporal.js\";\nimport { EFTimegroup } from \"../../elements/EFTimegroup.js\";\n\nexport interface TimelineRowModel {\n element: TemporalMixinInterface & Element;\n depth: number;\n}\n\n/**\n * Flattens a hierarchical temporal element tree into a flat array of rows.\n * Each row contains the element and its depth in the hierarchy.\n *\n * @param root - The root temporal element to flatten\n * @param startDepth - Starting depth (default 0)\n * @returns Array of {element, depth} in depth-first order\n */\nexport function flattenHierarchy(\n root: TemporalMixinInterface & Element,\n startDepth = 0,\n): TimelineRowModel[] {\n const rows: TimelineRowModel[] = [{ element: root, depth: startDepth }];\n\n if (root instanceof EFTimegroup) {\n for (const child of root.children) {\n if (isEFTemporal(child)) {\n // Skip child elements that are consolidated into their parent track\n const tagName = (child as Element).tagName?.toUpperCase();\n\n // Skip captions child elements - they're shown inline in the captions track\n if (\n tagName === \"EF-CAPTIONS-ACTIVE-WORD\" ||\n tagName === \"EF-CAPTIONS-SEGMENT\" ||\n tagName === \"EF-CAPTIONS-BEFORE-ACTIVE-WORD\" ||\n tagName === \"EF-CAPTIONS-AFTER-ACTIVE-WORD\"\n ) {\n continue;\n }\n\n // Skip text segments - they're shown inline in the text track\n if (tagName === \"EF-TEXT-SEGMENT\") {\n continue;\n }\n\n rows.push(\n ...flattenHierarchy(\n child as TemporalMixinInterface & Element,\n startDepth + 1,\n ),\n );\n }\n }\n }\n\n return rows;\n}\n"],"mappings":";;;;;;;;;;;;AAmBA,SAAgB,iBACd,MACA,aAAa,GACO;CACpB,MAAMA,OAA2B,CAAC;EAAE,SAAS;EAAM,OAAO;EAAY,CAAC;AAEvE,KAAI,gBAAgB,aAClB;OAAK,MAAM,SAAS,KAAK,SACvB,KAAI,aAAa,MAAM,EAAE;GAEvB,MAAM,UAAW,MAAkB,SAAS,aAAa;AAGzD,OACE,YAAY,6BACZ,YAAY,yBACZ,YAAY,oCACZ,YAAY,gCAEZ;AAIF,OAAI,YAAY,kBACd;AAGF,QAAK,KACH,GAAG,iBACD,OACA,aAAa,EACd,CACF;;;AAKP,QAAO"}
1
+ {"version":3,"file":"flattenHierarchy.js","names":["rows: TimelineRowModel[]"],"sources":["../../../src/gui/timeline/flattenHierarchy.ts"],"sourcesContent":["import { isEFTemporal, type TemporalMixinInterface } from \"../../elements/EFTemporal.js\";\nimport { EFTimegroup } from \"../../elements/EFTimegroup.js\";\n\nexport interface TimelineRowModel {\n element: TemporalMixinInterface & Element;\n depth: number;\n}\n\n/**\n * Flattens a hierarchical temporal element tree into a flat array of rows.\n * Each row contains the element and its depth in the hierarchy.\n *\n * @param root - The root temporal element to flatten\n * @param startDepth - Starting depth (default 0)\n * @returns Array of {element, depth} in depth-first order\n */\nexport function flattenHierarchy(\n root: TemporalMixinInterface & Element,\n startDepth = 0,\n): TimelineRowModel[] {\n const rows: TimelineRowModel[] = [{ element: root, depth: startDepth }];\n\n if (root instanceof EFTimegroup) {\n for (const child of root.children) {\n if (isEFTemporal(child)) {\n // Skip child elements that are consolidated into their parent track\n const tagName = (child as Element).tagName?.toUpperCase();\n\n // Skip captions child elements - they're shown inline in the captions track\n if (\n tagName === \"EF-CAPTIONS-ACTIVE-WORD\" ||\n tagName === \"EF-CAPTIONS-SEGMENT\" ||\n tagName === \"EF-CAPTIONS-BEFORE-ACTIVE-WORD\" ||\n tagName === \"EF-CAPTIONS-AFTER-ACTIVE-WORD\"\n ) {\n continue;\n }\n\n // Skip text segments - they're shown inline in the text track\n if (tagName === \"EF-TEXT-SEGMENT\") {\n continue;\n }\n\n rows.push(...flattenHierarchy(child as TemporalMixinInterface & Element, startDepth + 1));\n }\n }\n }\n\n return rows;\n}\n"],"mappings":";;;;;;;;;;;;AAgBA,SAAgB,iBACd,MACA,aAAa,GACO;CACpB,MAAMA,OAA2B,CAAC;EAAE,SAAS;EAAM,OAAO;EAAY,CAAC;AAEvE,KAAI,gBAAgB,aAClB;OAAK,MAAM,SAAS,KAAK,SACvB,KAAI,aAAa,MAAM,EAAE;GAEvB,MAAM,UAAW,MAAkB,SAAS,aAAa;AAGzD,OACE,YAAY,6BACZ,YAAY,yBACZ,YAAY,oCACZ,YAAY,gCAEZ;AAIF,OAAI,YAAY,kBACd;AAGF,QAAK,KAAK,GAAG,iBAAiB,OAA2C,aAAa,EAAE,CAAC;;;AAK/F,QAAO"}
@@ -1 +1 @@
1
- {"version":3,"file":"timelineStateContext.js","names":[],"sources":["../../../src/gui/timeline/timelineStateContext.ts"],"sourcesContent":["import { createContext } from \"@lit/context\";\n\n/**\n * The core invariant of the timeline system.\n * Everything else (ruler positions, track positions, playhead position) derives from this.\n */\nexport interface TimelineState {\n /** Pixels per millisecond - the single zoom value */\n pixelsPerMs: number;\n /** Current playhead position in milliseconds */\n currentTimeMs: number;\n /** Total duration in milliseconds */\n durationMs: number;\n /** Viewport scroll position in pixels - single source of truth for visible time range */\n viewportScrollLeft: number;\n /** Viewport width in pixels - for calculating visible time range */\n viewportWidth: number;\n /** Seek to a specific time */\n seek: (timeMs: number) => void;\n /** Zoom in */\n zoomIn: () => void;\n /** Zoom out */\n zoomOut: () => void;\n}\n\nexport const timelineStateContext =\n createContext<TimelineState>(\"timeline-state\");\n\n/**\n * Convert time to pixel position\n */\nexport function timeToPx(timeMs: number, pixelsPerMs: number): number {\n return timeMs * pixelsPerMs;\n}\n\n/**\n * Convert pixel position to time\n */\nexport function pxToTime(px: number, pixelsPerMs: number): number {\n return px / pixelsPerMs;\n}\n\n/**\n * Default pixels per ms at 100% zoom (100 pixels per second)\n */\nexport const DEFAULT_PIXELS_PER_MS = 0.1;\n\n/**\n * Timeline row height in pixels - must match --timeline-row-height CSS variable\n */\nexport const TIMELINE_ROW_HEIGHT = 28;\n\n/**\n * Timeline track content height in pixels - must match --timeline-track-height CSS variable\n */\nexport const TIMELINE_TRACK_HEIGHT = 22;\n\n/**\n * Vertical padding within a row (row height - track height) / 2\n */\nexport const TIMELINE_ROW_PADDING =\n (TIMELINE_ROW_HEIGHT - TIMELINE_TRACK_HEIGHT) / 2;\n\n/**\n * Calculate pixels per ms from a zoom scale\n */\nexport function zoomToPixelsPerMs(zoomScale: number): number {\n return DEFAULT_PIXELS_PER_MS * zoomScale;\n}\n\n/**\n * Calculate zoom scale from pixels per ms\n */\nexport function pixelsPerMsToZoom(pixelsPerMs: number): number {\n return pixelsPerMs / DEFAULT_PIXELS_PER_MS;\n}\n"],"mappings":";;;AAyBA,MAAa,uBACX,cAA6B,iBAAiB;;;;AAKhD,SAAgB,SAAS,QAAgB,aAA6B;AACpE,QAAO,SAAS;;;;;AAMlB,SAAgB,SAAS,IAAY,aAA6B;AAChE,QAAO,KAAK;;;;;AAMd,MAAa,wBAAwB;;;;AAKrC,MAAa,sBAAsB;;;;AAKnC,MAAa,wBAAwB;;;;AAKrC,MAAa,wBACV,sBAAsB,yBAAyB;;;;AAYlD,SAAgB,kBAAkB,aAA6B;AAC7D,QAAO,cAAc"}
1
+ {"version":3,"file":"timelineStateContext.js","names":[],"sources":["../../../src/gui/timeline/timelineStateContext.ts"],"sourcesContent":["import { createContext } from \"@lit/context\";\n\n/**\n * The core invariant of the timeline system.\n * Everything else (ruler positions, track positions, playhead position) derives from this.\n */\nexport interface TimelineState {\n /** Pixels per millisecond - the single zoom value */\n pixelsPerMs: number;\n /** Current playhead position in milliseconds */\n currentTimeMs: number;\n /** Total duration in milliseconds */\n durationMs: number;\n /** Viewport scroll position in pixels - single source of truth for visible time range */\n viewportScrollLeft: number;\n /** Viewport width in pixels - for calculating visible time range */\n viewportWidth: number;\n /** Seek to a specific time */\n seek: (timeMs: number) => void;\n /** Zoom in */\n zoomIn: () => void;\n /** Zoom out */\n zoomOut: () => void;\n}\n\nexport const timelineStateContext = createContext<TimelineState>(\"timeline-state\");\n\n/**\n * Convert time to pixel position\n */\nexport function timeToPx(timeMs: number, pixelsPerMs: number): number {\n return timeMs * pixelsPerMs;\n}\n\n/**\n * Convert pixel position to time\n */\nexport function pxToTime(px: number, pixelsPerMs: number): number {\n return px / pixelsPerMs;\n}\n\n/**\n * Default pixels per ms at 100% zoom (100 pixels per second)\n */\nexport const DEFAULT_PIXELS_PER_MS = 0.1;\n\n/**\n * Timeline row height in pixels - must match --timeline-row-height CSS variable\n */\nexport const TIMELINE_ROW_HEIGHT = 28;\n\n/**\n * Timeline track content height in pixels - must match --timeline-track-height CSS variable\n */\nexport const TIMELINE_TRACK_HEIGHT = 22;\n\n/**\n * Vertical padding within a row (row height - track height) / 2\n */\nexport const TIMELINE_ROW_PADDING = (TIMELINE_ROW_HEIGHT - TIMELINE_TRACK_HEIGHT) / 2;\n\n/**\n * Calculate pixels per ms from a zoom scale\n */\nexport function zoomToPixelsPerMs(zoomScale: number): number {\n return DEFAULT_PIXELS_PER_MS * zoomScale;\n}\n\n/**\n * Calculate zoom scale from pixels per ms\n */\nexport function pixelsPerMsToZoom(pixelsPerMs: number): number {\n return pixelsPerMs / DEFAULT_PIXELS_PER_MS;\n}\n"],"mappings":";;;AAyBA,MAAa,uBAAuB,cAA6B,iBAAiB;;;;AAKlF,SAAgB,SAAS,QAAgB,aAA6B;AACpE,QAAO,SAAS;;;;;AAMlB,SAAgB,SAAS,IAAY,aAA6B;AAChE,QAAO,KAAK;;;;;AAMd,MAAa,wBAAwB;;;;AAKrC,MAAa,sBAAsB;;;;AAKnC,MAAa,wBAAwB;;;;AAKrC,MAAa,wBAAwB,sBAAsB,yBAAyB;;;;AAYpF,SAAgB,kBAAkB,aAA6B;AAC7D,QAAO,cAAc"}
@@ -1 +1 @@
1
- {"version":3,"file":"AudioTrack.js","names":["EFAudioTrack","#loadWaveformData","#lastSrc","#abortController","#scheduleRender","#renderRequested","#renderWaveform","#getTrackPositionInfo","#hostHeight","#drawWaveformRegion","#resizeObserver","#renderPlaceholder"],"sources":["../../../../src/gui/timeline/tracks/AudioTrack.ts"],"sourcesContent":["import { consume } from \"@lit/context\";\nimport { css, html, nothing } from \"lit\";\nimport { customElement, state } from \"lit/decorators.js\";\nimport { createRef, ref } from \"lit/directives/ref.js\";\nimport { EFAudio } from \"../../../elements/EFAudio.js\";\nimport { TrackItem } from \"./TrackItem.js\";\nimport { extractWaveformData, type WaveformData } from \"./waveformUtils.js\";\nimport {\n timelineStateContext,\n type TimelineState,\n} from \"../timelineStateContext.js\";\n\n/** Padding in pixels to render beyond visible area (for smooth scrolling) */\nconst VIRTUAL_RENDER_PADDING_PX = 100;\n\n@customElement(\"ef-audio-track\")\nexport class EFAudioTrack extends TrackItem {\n static styles = [\n ...TrackItem.styles,\n css`\n .waveform-host {\n position: absolute;\n left: 0;\n top: 2px;\n right: 0;\n bottom: 2px;\n overflow: hidden;\n }\n .waveform-canvas {\n display: block;\n position: absolute;\n top: 0;\n height: 100%;\n pointer-events: none;\n }\n .shimmer-placeholder {\n position: absolute;\n left: 0;\n top: 2px;\n bottom: 2px;\n right: 0;\n background: linear-gradient(\n 90deg,\n color-mix(in srgb, var(--ef-color-type-audio, #10b981) 20%, transparent) 0%,\n color-mix(in srgb, var(--ef-color-type-audio, #10b981) 42%, transparent) 50%,\n color-mix(in srgb, var(--ef-color-type-audio, #10b981) 20%, transparent) 100%\n );\n background-size: 200% 100%;\n border-radius: 2px;\n }\n .shimmer-placeholder.is-loading {\n animation: shimmer var(--ef-loading-shimmer-duration, 1.5s) linear infinite;\n }\n @keyframes shimmer {\n 0% { background-position: 200% 0; }\n 100% { background-position: -200% 0; }\n }\n `,\n ];\n\n canvasRef = createRef<HTMLCanvasElement>();\n\n /** Timeline state context for viewport info */\n @consume({ context: timelineStateContext, subscribe: true })\n @state()\n private _timelineState?: TimelineState;\n\n @state()\n private _waveformData: WaveformData | null = null;\n\n @state()\n private _isLoading = false;\n\n #lastSrc: string | null = null;\n #abortController: AbortController | null = null;\n #resizeObserver?: ResizeObserver;\n #renderRequested = false;\n #hostHeight = 0;\n\n /**\n * Load waveform data when the audio source changes\n */\n async #loadWaveformData(): Promise<void> {\n const audio = this.element as EFAudio;\n const src = audio?.src;\n\n // Skip if no source or same source already loaded\n if (!src || src === this.#lastSrc) {\n return;\n }\n\n this.#lastSrc = src;\n\n // Cancel any in-progress load\n this.#abortController?.abort();\n this.#abortController = new AbortController();\n\n this._isLoading = true;\n\n try {\n const waveformData = await extractWaveformData(\n audio,\n this.#abortController.signal,\n );\n\n if (waveformData) {\n this._waveformData = waveformData;\n this.#scheduleRender();\n }\n } catch (error) {\n if (!(error instanceof DOMException && error.name === \"AbortError\")) {\n console.warn(\"Failed to load waveform data:\", error);\n }\n } finally {\n this._isLoading = false;\n }\n }\n\n /**\n * Schedule a canvas render on the next animation frame\n */\n #scheduleRender(): void {\n if (this.#renderRequested) return;\n this.#renderRequested = true;\n\n requestAnimationFrame(() => {\n this.#renderRequested = false;\n this.#renderWaveform();\n });\n }\n\n /**\n * Get the track's position info relative to timeline scroll\n */\n #getTrackPositionInfo(): {\n trackStartPx: number;\n trackWidthPx: number;\n viewportScrollLeft: number;\n viewportWidth: number;\n pixelsPerMs: number;\n } | null {\n const audio = this.element as EFAudio;\n const durationMs = audio.durationMs ?? 0;\n if (durationMs === 0) return null;\n\n const pixelsPerMs = this._timelineState?.pixelsPerMs ?? this.pixelsPerMs;\n const trackWidthPx = durationMs * pixelsPerMs;\n\n // Get track's absolute position from startTimeMs\n const trackStartMs = audio.startTimeMs ?? 0;\n const trackStartPx = trackStartMs * pixelsPerMs;\n\n // Get viewport info from context\n const viewportScrollLeft = this._timelineState?.viewportScrollLeft ?? 0;\n const viewportWidth = this._timelineState?.viewportWidth ?? 800;\n\n return {\n trackStartPx,\n trackWidthPx,\n viewportScrollLeft,\n viewportWidth,\n pixelsPerMs,\n };\n }\n\n /**\n * Render the waveform to canvas with virtual rendering.\n *\n * The approach:\n * 1. Calculate the visible portion of the track (intersection of track and viewport)\n * 2. Position the canvas at that visible portion within the track\n * 3. Draw only the waveform data for that visible time range\n * 4. Update position and content as scroll/zoom changes\n */\n #renderWaveform(): void {\n const canvas = this.canvasRef.value;\n const waveformData = this._waveformData;\n\n if (!canvas || !waveformData) return;\n\n const positionInfo = this.#getTrackPositionInfo();\n if (!positionInfo) return;\n\n const {\n trackStartPx,\n trackWidthPx,\n viewportScrollLeft,\n viewportWidth,\n pixelsPerMs,\n } = positionInfo;\n\n // Calculate visible region in absolute pixels (with padding for smooth scrolling)\n const visibleLeftPx = viewportScrollLeft - VIRTUAL_RENDER_PADDING_PX;\n const visibleRightPx =\n viewportScrollLeft + viewportWidth + VIRTUAL_RENDER_PADDING_PX;\n\n // Track boundaries in absolute pixels\n const trackEndPx = trackStartPx + trackWidthPx;\n\n // Check if track is visible at all\n if (trackEndPx < visibleLeftPx || trackStartPx > visibleRightPx) {\n // Track not visible, hide canvas\n canvas.style.display = \"none\";\n return;\n }\n canvas.style.display = \"block\";\n\n // Calculate the intersection: what part of the track is visible\n // All coordinates are now relative to the track's left edge (0 = track start)\n const visibleStartInTrack = Math.max(0, visibleLeftPx - trackStartPx);\n const visibleEndInTrack = Math.min(\n trackWidthPx,\n visibleRightPx - trackStartPx,\n );\n const visibleWidthPx = visibleEndInTrack - visibleStartInTrack;\n\n if (visibleWidthPx <= 0) return;\n\n const height = this.#hostHeight || 18;\n\n // Set canvas size with DPR\n const dpr = window.devicePixelRatio || 1;\n const targetWidth = Math.ceil(visibleWidthPx * dpr);\n const targetHeight = Math.ceil(height * dpr);\n\n if (canvas.width !== targetWidth || canvas.height !== targetHeight) {\n canvas.width = targetWidth;\n canvas.height = targetHeight;\n }\n\n // Position canvas at the visible portion within the track\n canvas.style.left = `${visibleStartInTrack}px`;\n canvas.style.width = `${visibleWidthPx}px`;\n canvas.style.height = `${height}px`;\n\n const ctx = canvas.getContext(\"2d\");\n if (!ctx) return;\n\n ctx.setTransform(dpr, 0, 0, dpr, 0, 0);\n ctx.clearRect(0, 0, visibleWidthPx, height);\n\n // Calculate what time range to render\n const audio = this.element as EFAudio;\n const sourceInMs = audio.sourceStartMs ?? 0;\n\n // Convert visible pixel range to time range\n const timeStartMs = sourceInMs + visibleStartInTrack / pixelsPerMs;\n const timeEndMs = sourceInMs + visibleEndInTrack / pixelsPerMs;\n\n // Draw the waveform for the visible portion\n this.#drawWaveformRegion(\n ctx,\n waveformData,\n 0, // Start drawing at x=0 of canvas (canvas is already positioned)\n visibleWidthPx,\n height,\n timeStartMs,\n timeEndMs,\n );\n }\n\n /**\n * Draw a region of the waveform to canvas\n */\n #drawWaveformRegion(\n ctx: CanvasRenderingContext2D,\n waveformData: WaveformData,\n x: number,\n width: number,\n height: number,\n startMs: number,\n endMs: number,\n ): void {\n const { peaks, samplesPerSecond } = waveformData;\n\n // Calculate sample range\n const startSample = Math.floor((startMs / 1000) * samplesPerSecond);\n const endSample = Math.ceil((endMs / 1000) * samplesPerSecond);\n const sampleCount = endSample - startSample;\n\n if (sampleCount <= 0 || width <= 0) return;\n\n const centerY = height / 2;\n const halfHeight = height / 2 - 2; // Leave 2px padding top/bottom\n const color = this.getElementTypeColor();\n\n ctx.fillStyle = color;\n ctx.globalAlpha = 0.8;\n ctx.beginPath();\n\n // Draw top half (max values) left to right\n const pixelsPerSample = width / sampleCount;\n\n for (let i = 0; i <= sampleCount; i++) {\n const sampleIndex = startSample + i;\n const peakIndex = sampleIndex * 2;\n\n // Clamp to valid range\n if (peakIndex + 1 >= peaks.length) break;\n\n const maxValue = peaks[peakIndex + 1] ?? 0;\n const px = x + i * pixelsPerSample;\n const py = centerY - maxValue * halfHeight;\n\n if (i === 0) {\n ctx.moveTo(px, py);\n } else {\n ctx.lineTo(px, py);\n }\n }\n\n // Draw bottom half (min values) right to left\n for (let i = sampleCount; i >= 0; i--) {\n const sampleIndex = startSample + i;\n const peakIndex = sampleIndex * 2;\n\n // Clamp to valid range\n if (peakIndex >= peaks.length) continue;\n\n const minValue = peaks[peakIndex] ?? 0;\n const px = x + i * pixelsPerSample;\n const py = centerY - minValue * halfHeight;\n\n ctx.lineTo(px, py);\n }\n\n ctx.closePath();\n ctx.fill();\n\n // Draw center line\n ctx.globalAlpha = 0.3;\n ctx.strokeStyle = color;\n ctx.lineWidth = 1;\n ctx.beginPath();\n ctx.moveTo(x, centerY);\n ctx.lineTo(x + width, centerY);\n ctx.stroke();\n\n ctx.globalAlpha = 1;\n }\n\n connectedCallback(): void {\n super.connectedCallback();\n\n // Start loading waveform data\n this.#loadWaveformData();\n\n // Observe size changes\n this.#resizeObserver = new ResizeObserver((entries) => {\n for (const entry of entries) {\n this.#hostHeight =\n entry.borderBoxSize?.[0]?.blockSize ?? entry.contentRect.height;\n this.#scheduleRender();\n }\n });\n }\n\n disconnectedCallback(): void {\n super.disconnectedCallback();\n this.#abortController?.abort();\n this.#resizeObserver?.disconnect();\n }\n\n updated(changedProperties: Map<string | number | symbol, unknown>): void {\n super.updated(changedProperties);\n\n // Check if we need to reload waveform data\n const audio = this.element as EFAudio;\n if (audio?.src !== this.#lastSrc) {\n this.#loadWaveformData();\n }\n\n // Re-render when timeline state changes (scroll, zoom)\n if (changedProperties.has(\"_timelineState\")) {\n this.#scheduleRender();\n }\n\n // Attach resize observer to track container once rendered\n if (this.canvasRef.value && this.#resizeObserver) {\n const container = this.canvasRef.value.parentElement;\n if (container) {\n this.#resizeObserver.disconnect();\n this.#resizeObserver.observe(container);\n }\n }\n\n // Always schedule render after update to catch any changes\n this.#scheduleRender();\n }\n\n contents() {\n const audio = this.element as EFAudio;\n if (!(audio instanceof EFAudio)) {\n return nothing;\n }\n\n const durationMs = audio.durationMs ?? 0;\n if (durationMs === 0) {\n return nothing;\n }\n\n // Show loading placeholder if no waveform data yet\n if (!this._waveformData) {\n return this.#renderPlaceholder();\n }\n\n // The host fills the track container, canvas is positioned within it\n return html`\n <div class=\"waveform-host\">\n <canvas ${ref(this.canvasRef)} class=\"waveform-canvas\"></canvas>\n </div>\n `;\n }\n\n #renderPlaceholder() {\n return html`<div\n class=\"shimmer-placeholder ${this._isLoading ? \"is-loading\" : \"\"}\"\n ></div>`;\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"AudioTrack.js","names":["EFAudioTrack","#loadWaveformData","#lastSrc","#abortController","#scheduleRender","#renderRequested","#renderWaveform","#getTrackPositionInfo","#hostHeight","#drawWaveformRegion","#resizeObserver","#renderPlaceholder"],"sources":["../../../../src/gui/timeline/tracks/AudioTrack.ts"],"sourcesContent":["import { consume } from \"@lit/context\";\nimport { css, html, nothing } from \"lit\";\nimport { customElement, state } from \"lit/decorators.js\";\nimport { createRef, ref } from \"lit/directives/ref.js\";\nimport { EFAudio } from \"../../../elements/EFAudio.js\";\nimport { TrackItem } from \"./TrackItem.js\";\nimport { extractWaveformData, type WaveformData } from \"./waveformUtils.js\";\nimport { timelineStateContext, type TimelineState } from \"../timelineStateContext.js\";\n\n/** Padding in pixels to render beyond visible area (for smooth scrolling) */\nconst VIRTUAL_RENDER_PADDING_PX = 100;\n\n@customElement(\"ef-audio-track\")\nexport class EFAudioTrack extends TrackItem {\n static styles = [\n ...TrackItem.styles,\n css`\n .waveform-host {\n position: absolute;\n left: 0;\n top: 2px;\n right: 0;\n bottom: 2px;\n overflow: hidden;\n }\n .waveform-canvas {\n display: block;\n position: absolute;\n top: 0;\n height: 100%;\n pointer-events: none;\n }\n .shimmer-placeholder {\n position: absolute;\n left: 0;\n top: 2px;\n bottom: 2px;\n right: 0;\n background: linear-gradient(\n 90deg,\n color-mix(in srgb, var(--ef-color-type-audio, #10b981) 20%, transparent) 0%,\n color-mix(in srgb, var(--ef-color-type-audio, #10b981) 42%, transparent) 50%,\n color-mix(in srgb, var(--ef-color-type-audio, #10b981) 20%, transparent) 100%\n );\n background-size: 200% 100%;\n border-radius: 2px;\n }\n .shimmer-placeholder.is-loading {\n animation: shimmer var(--ef-loading-shimmer-duration, 1.5s) linear infinite;\n }\n @keyframes shimmer {\n 0% { background-position: 200% 0; }\n 100% { background-position: -200% 0; }\n }\n `,\n ];\n\n canvasRef = createRef<HTMLCanvasElement>();\n\n /** Timeline state context for viewport info */\n @consume({ context: timelineStateContext, subscribe: true })\n @state()\n private _timelineState?: TimelineState;\n\n @state()\n private _waveformData: WaveformData | null = null;\n\n @state()\n private _isLoading = false;\n\n #lastSrc: string | null = null;\n #abortController: AbortController | null = null;\n #resizeObserver?: ResizeObserver;\n #renderRequested = false;\n #hostHeight = 0;\n\n /**\n * Load waveform data when the audio source changes\n */\n async #loadWaveformData(): Promise<void> {\n const audio = this.element as EFAudio;\n const src = audio?.src;\n\n // Skip if no source or same source already loaded\n if (!src || src === this.#lastSrc) {\n return;\n }\n\n this.#lastSrc = src;\n\n // Cancel any in-progress load\n this.#abortController?.abort();\n this.#abortController = new AbortController();\n\n this._isLoading = true;\n\n try {\n const waveformData = await extractWaveformData(audio, this.#abortController.signal);\n\n if (waveformData) {\n this._waveformData = waveformData;\n this.#scheduleRender();\n }\n } catch (error) {\n if (!(error instanceof DOMException && error.name === \"AbortError\")) {\n console.warn(\"Failed to load waveform data:\", error);\n }\n } finally {\n this._isLoading = false;\n }\n }\n\n /**\n * Schedule a canvas render on the next animation frame\n */\n #scheduleRender(): void {\n if (this.#renderRequested) return;\n this.#renderRequested = true;\n\n requestAnimationFrame(() => {\n this.#renderRequested = false;\n this.#renderWaveform();\n });\n }\n\n /**\n * Get the track's position info relative to timeline scroll\n */\n #getTrackPositionInfo(): {\n trackStartPx: number;\n trackWidthPx: number;\n viewportScrollLeft: number;\n viewportWidth: number;\n pixelsPerMs: number;\n } | null {\n const audio = this.element as EFAudio;\n const durationMs = audio.durationMs ?? 0;\n if (durationMs === 0) return null;\n\n const pixelsPerMs = this._timelineState?.pixelsPerMs ?? this.pixelsPerMs;\n const trackWidthPx = durationMs * pixelsPerMs;\n\n // Get track's absolute position from startTimeMs\n const trackStartMs = audio.startTimeMs ?? 0;\n const trackStartPx = trackStartMs * pixelsPerMs;\n\n // Get viewport info from context\n const viewportScrollLeft = this._timelineState?.viewportScrollLeft ?? 0;\n const viewportWidth = this._timelineState?.viewportWidth ?? 800;\n\n return {\n trackStartPx,\n trackWidthPx,\n viewportScrollLeft,\n viewportWidth,\n pixelsPerMs,\n };\n }\n\n /**\n * Render the waveform to canvas with virtual rendering.\n *\n * The approach:\n * 1. Calculate the visible portion of the track (intersection of track and viewport)\n * 2. Position the canvas at that visible portion within the track\n * 3. Draw only the waveform data for that visible time range\n * 4. Update position and content as scroll/zoom changes\n */\n #renderWaveform(): void {\n const canvas = this.canvasRef.value;\n const waveformData = this._waveformData;\n\n if (!canvas || !waveformData) return;\n\n const positionInfo = this.#getTrackPositionInfo();\n if (!positionInfo) return;\n\n const { trackStartPx, trackWidthPx, viewportScrollLeft, viewportWidth, pixelsPerMs } =\n positionInfo;\n\n // Calculate visible region in absolute pixels (with padding for smooth scrolling)\n const visibleLeftPx = viewportScrollLeft - VIRTUAL_RENDER_PADDING_PX;\n const visibleRightPx = viewportScrollLeft + viewportWidth + VIRTUAL_RENDER_PADDING_PX;\n\n // Track boundaries in absolute pixels\n const trackEndPx = trackStartPx + trackWidthPx;\n\n // Check if track is visible at all\n if (trackEndPx < visibleLeftPx || trackStartPx > visibleRightPx) {\n // Track not visible, hide canvas\n canvas.style.display = \"none\";\n return;\n }\n canvas.style.display = \"block\";\n\n // Calculate the intersection: what part of the track is visible\n // All coordinates are now relative to the track's left edge (0 = track start)\n const visibleStartInTrack = Math.max(0, visibleLeftPx - trackStartPx);\n const visibleEndInTrack = Math.min(trackWidthPx, visibleRightPx - trackStartPx);\n const visibleWidthPx = visibleEndInTrack - visibleStartInTrack;\n\n if (visibleWidthPx <= 0) return;\n\n const height = this.#hostHeight || 18;\n\n // Set canvas size with DPR\n const dpr = window.devicePixelRatio || 1;\n const targetWidth = Math.ceil(visibleWidthPx * dpr);\n const targetHeight = Math.ceil(height * dpr);\n\n if (canvas.width !== targetWidth || canvas.height !== targetHeight) {\n canvas.width = targetWidth;\n canvas.height = targetHeight;\n }\n\n // Position canvas at the visible portion within the track\n canvas.style.left = `${visibleStartInTrack}px`;\n canvas.style.width = `${visibleWidthPx}px`;\n canvas.style.height = `${height}px`;\n\n const ctx = canvas.getContext(\"2d\");\n if (!ctx) return;\n\n ctx.setTransform(dpr, 0, 0, dpr, 0, 0);\n ctx.clearRect(0, 0, visibleWidthPx, height);\n\n // Calculate what time range to render\n const audio = this.element as EFAudio;\n const sourceInMs = audio.sourceStartMs ?? 0;\n\n // Convert visible pixel range to time range\n const timeStartMs = sourceInMs + visibleStartInTrack / pixelsPerMs;\n const timeEndMs = sourceInMs + visibleEndInTrack / pixelsPerMs;\n\n // Draw the waveform for the visible portion\n this.#drawWaveformRegion(\n ctx,\n waveformData,\n 0, // Start drawing at x=0 of canvas (canvas is already positioned)\n visibleWidthPx,\n height,\n timeStartMs,\n timeEndMs,\n );\n }\n\n /**\n * Draw a region of the waveform to canvas\n */\n #drawWaveformRegion(\n ctx: CanvasRenderingContext2D,\n waveformData: WaveformData,\n x: number,\n width: number,\n height: number,\n startMs: number,\n endMs: number,\n ): void {\n const { peaks, samplesPerSecond } = waveformData;\n\n // Calculate sample range\n const startSample = Math.floor((startMs / 1000) * samplesPerSecond);\n const endSample = Math.ceil((endMs / 1000) * samplesPerSecond);\n const sampleCount = endSample - startSample;\n\n if (sampleCount <= 0 || width <= 0) return;\n\n const centerY = height / 2;\n const halfHeight = height / 2 - 2; // Leave 2px padding top/bottom\n const color = this.getElementTypeColor();\n\n ctx.fillStyle = color;\n ctx.globalAlpha = 0.8;\n ctx.beginPath();\n\n // Draw top half (max values) left to right\n const pixelsPerSample = width / sampleCount;\n\n for (let i = 0; i <= sampleCount; i++) {\n const sampleIndex = startSample + i;\n const peakIndex = sampleIndex * 2;\n\n // Clamp to valid range\n if (peakIndex + 1 >= peaks.length) break;\n\n const maxValue = peaks[peakIndex + 1] ?? 0;\n const px = x + i * pixelsPerSample;\n const py = centerY - maxValue * halfHeight;\n\n if (i === 0) {\n ctx.moveTo(px, py);\n } else {\n ctx.lineTo(px, py);\n }\n }\n\n // Draw bottom half (min values) right to left\n for (let i = sampleCount; i >= 0; i--) {\n const sampleIndex = startSample + i;\n const peakIndex = sampleIndex * 2;\n\n // Clamp to valid range\n if (peakIndex >= peaks.length) continue;\n\n const minValue = peaks[peakIndex] ?? 0;\n const px = x + i * pixelsPerSample;\n const py = centerY - minValue * halfHeight;\n\n ctx.lineTo(px, py);\n }\n\n ctx.closePath();\n ctx.fill();\n\n // Draw center line\n ctx.globalAlpha = 0.3;\n ctx.strokeStyle = color;\n ctx.lineWidth = 1;\n ctx.beginPath();\n ctx.moveTo(x, centerY);\n ctx.lineTo(x + width, centerY);\n ctx.stroke();\n\n ctx.globalAlpha = 1;\n }\n\n connectedCallback(): void {\n super.connectedCallback();\n\n // Start loading waveform data\n this.#loadWaveformData();\n\n // Observe size changes\n this.#resizeObserver = new ResizeObserver((entries) => {\n for (const entry of entries) {\n this.#hostHeight = entry.borderBoxSize?.[0]?.blockSize ?? entry.contentRect.height;\n this.#scheduleRender();\n }\n });\n }\n\n disconnectedCallback(): void {\n super.disconnectedCallback();\n this.#abortController?.abort();\n this.#resizeObserver?.disconnect();\n }\n\n updated(changedProperties: Map<string | number | symbol, unknown>): void {\n super.updated(changedProperties);\n\n // Check if we need to reload waveform data\n const audio = this.element as EFAudio;\n if (audio?.src !== this.#lastSrc) {\n this.#loadWaveformData();\n }\n\n // Re-render when timeline state changes (scroll, zoom)\n if (changedProperties.has(\"_timelineState\")) {\n this.#scheduleRender();\n }\n\n // Attach resize observer to track container once rendered\n if (this.canvasRef.value && this.#resizeObserver) {\n const container = this.canvasRef.value.parentElement;\n if (container) {\n this.#resizeObserver.disconnect();\n this.#resizeObserver.observe(container);\n }\n }\n\n // Always schedule render after update to catch any changes\n this.#scheduleRender();\n }\n\n contents() {\n const audio = this.element as EFAudio;\n if (!(audio instanceof EFAudio)) {\n return nothing;\n }\n\n const durationMs = audio.durationMs ?? 0;\n if (durationMs === 0) {\n return nothing;\n }\n\n // Show loading placeholder if no waveform data yet\n if (!this._waveformData) {\n return this.#renderPlaceholder();\n }\n\n // The host fills the track container, canvas is positioned within it\n return html`\n <div class=\"waveform-host\">\n <canvas ${ref(this.canvasRef)} class=\"waveform-canvas\"></canvas>\n </div>\n `;\n }\n\n #renderPlaceholder() {\n return html`<div\n class=\"shimmer-placeholder ${this._isLoading ? \"is-loading\" : \"\"}\"\n ></div>`;\n }\n}\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"CaptionsTrack.js","names":["measurementCanvas: HTMLCanvasElement | null","measurementContext: CanvasRenderingContext2D | null","wordWidths: Array<{\n textWidth: number;\n timeWidth: number;\n startPx: number;\n endPx: number;\n }>","EFCaptionsTrack","EFCaptionsActiveWordTrack","EFCaptionsSegmentTrack","EFCaptionsBeforeWordTrack","EFCaptionsAfterWordTrack"],"sources":["../../../../src/gui/timeline/tracks/CaptionsTrack.ts"],"sourcesContent":["import { consume } from \"@lit/context\";\nimport { css, html, nothing, type TemplateResult } from \"lit\";\nimport { customElement } from \"lit/decorators.js\";\nimport { styleMap } from \"lit/directives/style-map.js\";\nimport {\n type Caption,\n EFCaptions,\n type WordSegment,\n} from \"../../../elements/EFCaptions.js\";\nimport { phosphorIcon, ICONS } from \"../../icons.js\";\nimport { currentTimeContext } from \"../../currentTimeContext.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 { getElementTypeColor } from \"../../theme.js\";\n\n// Shared canvas context for text measurement (avoids creating new canvas each time)\nlet measurementCanvas: HTMLCanvasElement | null = null;\nlet measurementContext: CanvasRenderingContext2D | null = null;\n// Cache for text measurements: key is \"text:fontSize:fontWeight\"\nconst textMeasurementCache = new Map<string, number>();\nconst MAX_CACHE_SIZE = 500;\n\n/**\n * Measure text width accurately using canvas.\n * Matches the actual font used in word elements (font-weight: 500).\n * Results are cached to avoid repeated measurements of the same text.\n */\nfunction measureTextWidth(\n text: string,\n fontSize: number,\n fontWeight: number = 500,\n): number {\n // Check cache first\n const cacheKey = `${text}:${fontSize}:${fontWeight}`;\n const cached = textMeasurementCache.get(cacheKey);\n if (cached !== undefined) {\n return cached;\n }\n\n // Initialize shared canvas context if needed\n if (!measurementCanvas || !measurementContext) {\n measurementCanvas = document.createElement(\"canvas\");\n measurementContext = measurementCanvas.getContext(\"2d\");\n }\n\n if (!measurementContext) {\n return text.length * fontSize * 0.6; // Fallback estimate\n }\n\n // Match the actual font used in word elements\n const fontFamily =\n getComputedStyle(document.body).fontFamily || \"system-ui, sans-serif\";\n measurementContext.font = `${fontWeight} ${fontSize}px ${fontFamily}`;\n const width = measurementContext.measureText(text).width;\n\n // Cache the result (with size limit to prevent memory leaks)\n if (textMeasurementCache.size >= MAX_CACHE_SIZE) {\n // Clear oldest entries (simple strategy: clear half the cache)\n const keysToDelete = Array.from(textMeasurementCache.keys()).slice(\n 0,\n MAX_CACHE_SIZE / 2,\n );\n for (const key of keysToDelete) {\n textMeasurementCache.delete(key);\n }\n }\n textMeasurementCache.set(cacheKey, width);\n\n return width;\n}\n\n/**\n * Check if words can fit individually within a segment when positioned by time\n *\n * Strategy: Allow overlaps as long as all words can be rendered within the container.\n * Only use compact mode when words are so cramped they can't be displayed at all.\n */\nfunction canWordsFitIndividually(\n words: WordSegment[],\n segmentStart: number,\n segmentWidthPx: number,\n pixelsPerMs: number,\n): { fits: boolean; reason?: string } {\n if (words.length === 0) {\n return { fits: false, reason: \"no words\" };\n }\n\n // Measure total text width of all words (as if rendered sequentially)\n let totalTextWidth = 0;\n const wordWidths: Array<{\n textWidth: number;\n timeWidth: number;\n startPx: number;\n endPx: number;\n }> = [];\n\n for (const word of words) {\n if (!word) continue;\n\n // Measure actual text width (with padding: 2px left + 2px right = 4px total)\n const textWidth = measureTextWidth(word.text.trim(), 9, 500) + 4;\n\n // Calculate time-based position and width\n const startPx = pixelsPerMs * (word.start - segmentStart) * 1000;\n const endPx = pixelsPerMs * (word.end - segmentStart) * 1000;\n const timeWidth = endPx - startPx;\n\n wordWidths.push({ textWidth, timeWidth, startPx, endPx });\n totalTextWidth += textWidth;\n }\n\n // Key insight: If total text width fits in segment, we can render words individually\n // even if they overlap based on their time positions\n // Use 90% threshold to account for some spacing/overlap\n if (totalTextWidth <= segmentWidthPx * 0.9) {\n // All words can fit - use positioned mode (overlaps are okay)\n return { fits: true };\n }\n\n // If total text doesn't fit, check if individual words are too narrow to be readable\n // If any word's time-based width is less than 30% of its text width, it's unreadable\n for (const { textWidth, timeWidth } of wordWidths) {\n if (timeWidth < textWidth * 0.3) {\n return {\n fits: false,\n reason: `word too narrow (${timeWidth.toFixed(1)}px < ${(textWidth * 0.3).toFixed(1)}px)`,\n };\n }\n }\n\n // If words are readable individually but total text is too wide,\n // check if they can still fit with overlaps\n // Find the maximum right edge of all words\n const maxEndPx = Math.max(...wordWidths.map((w) => w.endPx));\n\n // If the rightmost word fits within the segment, allow overlaps\n if (maxEndPx <= segmentWidthPx * 1.1) {\n return { fits: true };\n }\n\n // Words don't fit - use compact mode\n return {\n fits: false,\n reason: `words exceed segment (total text: ${totalTextWidth.toFixed(1)}px, segment: ${segmentWidthPx.toFixed(1)}px)`,\n };\n}\n\n@customElement(\"ef-captions-track\")\nexport class EFCaptionsTrack extends TrackItem {\n static styles = [\n ...TrackItem.styles,\n css`\n .segment-block {\n position: absolute;\n border-radius: 3px;\n transition: box-shadow 0.15s ease, z-index 0.15s ease;\n cursor: pointer;\n overflow: visible;\n }\n \n .segment-block:hover {\n box-shadow: 0 1px 4px rgba(0, 0, 0, 0.3);\n z-index: 5;\n }\n \n .word-element {\n position: absolute;\n font-size: 9px;\n line-height: 1.2;\n white-space: nowrap;\n font-weight: 500;\n top: 50%;\n transform: translateY(-50%);\n padding: 2px 4px;\n border-radius: 2px;\n transition: all 0.1s ease;\n background: var(--ef-color-bg-elevated);\n color: var(--ef-color-text);\n z-index: 1;\n }\n \n .word-element.active {\n background: var(--ef-color-success);\n color: rgb(20, 30, 20);\n font-weight: 700;\n font-size: 10px;\n z-index: 10;\n box-shadow: 0 1px 3px rgba(0, 0, 0, 0.25);\n }\n \n .word-element.future {\n background: var(--ef-color-bg-inset);\n color: var(--ef-color-text);\n z-index: 5;\n }\n \n .segment-block.active .word-element:not(.active):not(.future) {\n color: var(--ef-color-text-muted);\n background: var(--ef-color-bg-elevated);\n }\n \n /* Compact text mode - when words are too small to position individually */\n .segment-block.compact-text {\n display: flex;\n align-items: center;\n padding: 0 4px;\n overflow: hidden;\n /* Keep position: absolute from .segment-block for correct time-based positioning */\n }\n \n /* Allow overflow on hover for compact text */\n .segment-block.compact-text:hover {\n overflow: visible;\n z-index: 100;\n /* Expand to fit content on hover */\n width: max-content !important;\n min-width: max-content;\n background: var(--ef-color-bg-elevated) !important;\n box-shadow: 0 2px 8px rgba(0, 0, 0, 0.4);\n }\n \n .segment-text-compact {\n font-size: 10px;\n line-height: 1.2;\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n color: var(--ef-color-text);\n width: 100%;\n }\n \n /* On hover, show full text */\n .segment-block.compact-text:hover .segment-text-compact {\n overflow: visible;\n text-overflow: clip;\n }\n \n .segment-block.compact-text.active .segment-text-compact {\n color: var(--ef-color-text-muted);\n font-weight: 500;\n }\n \n .segment-duration-indicator {\n position: absolute;\n bottom: 0;\n left: 0;\n right: 0;\n height: 2px;\n background: currentColor;\n opacity: 0.2;\n border-radius: 0 0 3px 3px;\n }\n \n .segment-block.active .segment-duration-indicator {\n opacity: 0.4;\n height: 2px;\n }\n \n .word-marker {\n position: absolute;\n bottom: 0;\n width: 1px;\n height: 30%;\n background: var(--ef-color-border-subtle);\n pointer-events: none;\n }\n \n .word-marker.active {\n background: var(--ef-color-text);\n height: 50%;\n width: 2px;\n }\n `,\n ];\n\n @consume({ context: currentTimeContext, subscribe: true })\n contextCurrentTimeMs = 0;\n\n private lastPixelsPerMs = 0;\n\n protected updated(\n changedProperties: Map<string | number | symbol, unknown>,\n ): void {\n super.updated(changedProperties);\n\n // Re-render when pixelsPerMs changes (zoom level changes)\n if (changedProperties.has(\"pixelsPerMs\")) {\n const currentPixelsPerMs = this.pixelsPerMs;\n if (currentPixelsPerMs !== this.lastPixelsPerMs) {\n this.lastPixelsPerMs = currentPixelsPerMs;\n // Force update to recalculate layout mode\n this.requestUpdate();\n }\n }\n }\n\n render() {\n const captions = this.element as EFCaptions;\n const captionsData = captions.unifiedCaptionsDataTask.value;\n\n return html`<div style=${styleMap(this.gutterStyles)}>\n <div\n class=\"relative\"\n style=\"background-color: var(--filmstrip-bg);\"\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 relative mb-0 block text-nowrap border text-sm overflow-visible\"\n style=${styleMap({\n ...this.trimPortionStyles,\n height: \"var(--timeline-track-height, 22px)\",\n backgroundColor: this.isFocused\n ? \"var(--filmstrip-item-focused)\"\n : \"var(--filmstrip-item-bg)\",\n borderColor: \"var(--filmstrip-border)\",\n borderLeftColor: this.getElementTypeColor(),\n borderLeftWidth: \"3px\",\n minHeight: \"22px\",\n })}\n >\n ${this.renderCaptionsData(captionsData)}\n ${\n this.enableTrim\n ? html`<ef-trim-handles\n element-id=${(this.element as HTMLElement).id || \"\"}\n pixels-per-ms=${this.pixelsPerMs}\n trim-start-ms=${this.element.trimStartMs ?? 0}\n trim-end-ms=${this.element.trimEndMs ?? 0}\n intrinsic-duration-ms=${this.element.intrinsicDurationMs ?? this.element.durationMs}\n @trim-change=${this.handleTrimChange}\n ></ef-trim-handles>`\n : nothing\n }\n </div>\n </div>\n ${this.renderChildren()}\n </div>`;\n }\n\n renderCaptionsData(captionsData: Caption | null | undefined) {\n if (!captionsData) {\n return html``;\n }\n\n const captions = this.element as EFCaptions;\n const rootTimegroup = captions.rootTimegroup;\n // Use context current time for reactivity, fallback to rootTimegroup\n const currentTimeMs =\n this.contextCurrentTimeMs || rootTimegroup?.currentTimeMs || 0;\n const captionsLocalTimeMs = currentTimeMs - captions.startTimeMs;\n const captionsLocalTimeSec = captionsLocalTimeMs / 1000;\n\n // Get element type color for captions using shared theme utility\n const captionColor = getElementTypeColor(\"captions\", this);\n\n // Find active word for highlighting\n const activeWord = captionsData.word_segments.find(\n (word) =>\n captionsLocalTimeSec >= word.start && captionsLocalTimeSec < word.end,\n );\n\n // Render word markers for visual density indication (subtle)\n const wordMarkers = captionsData.word_segments.map((word) => {\n const wordStartPx = this.pixelsPerMs * word.start * 1000;\n const wordWidth = this.pixelsPerMs * (word.end - word.start) * 1000;\n const isActive = word === activeWord;\n\n // Only show markers if they're wide enough to be visible\n if (wordWidth < 1.5) return nothing;\n\n return html`<div\n class=\"word-marker ${isActive ? \"active\" : \"\"}\"\n style=${styleMap({\n left: `${wordStartPx}px`,\n })}\n ></div>`;\n });\n\n // Render semantic segment blocks with words positioned by their actual timing\n const segmentElements = captionsData.segments.map((segment) => {\n const isActiveSegment =\n captionsLocalTimeSec >= segment.start &&\n captionsLocalTimeSec < segment.end;\n\n const segmentStartPx = this.pixelsPerMs * segment.start * 1000;\n const segmentWidth =\n this.pixelsPerMs * (segment.end - segment.start) * 1000;\n const segmentDuration = (segment.end - segment.start) * 1000;\n\n // Get words in this segment, sorted by start time\n const wordsInSegment = captionsData.word_segments\n .filter(\n (word) => word.start >= segment.start && word.end <= segment.end,\n )\n .sort((a, b) => a.start - b.start);\n\n // Calculate visual density based on word count\n const density = Math.min(wordsInSegment.length / 10, 1);\n\n // Use actual measurement to determine if words can fit individually\n // Allow overlaps - only use compact mode when words can't be rendered at all\n const measurementResult = canWordsFitIndividually(\n wordsInSegment,\n segment.start,\n segmentWidth,\n this.pixelsPerMs,\n );\n\n const useCompactText = !measurementResult.fits;\n let avgSpacing = 0;\n\n // Calculate average spacing for font scaling (only if using positioned mode)\n if (!useCompactText && wordsInSegment.length > 1) {\n let totalSpacing = 0;\n let spacingCount = 0;\n\n for (let i = 0; i < wordsInSegment.length - 1; i++) {\n const word1 = wordsInSegment[i];\n const word2 = wordsInSegment[i + 1];\n if (!word1 || !word2) continue;\n\n const word1EndPx =\n this.pixelsPerMs * (word1.end - segment.start) * 1000;\n const word2StartPx =\n this.pixelsPerMs * (word2.start - segment.start) * 1000;\n const spacing = word2StartPx - word1EndPx;\n\n if (spacing > 0) {\n totalSpacing += spacing;\n spacingCount++;\n }\n }\n\n avgSpacing = spacingCount > 0 ? totalSpacing / spacingCount : 0;\n }\n\n // Calculate optimal font size for positioned words (if not using compact mode)\n const MIN_READABLE_FONT_SIZE = 6; // Minimum readable font size in pixels\n const baseFontSize = 9;\n const activeFontSize = 10;\n let scaledFontSize = baseFontSize;\n let scaledActiveFontSize = activeFontSize;\n\n if (!useCompactText && wordsInSegment.length > 1 && avgSpacing < 8) {\n // Scale down font size proportionally, but don't go below minimum\n const scaleFactor = Math.max(\n MIN_READABLE_FONT_SIZE / baseFontSize,\n avgSpacing / 8,\n );\n scaledFontSize = Math.max(\n MIN_READABLE_FONT_SIZE,\n baseFontSize * scaleFactor,\n );\n scaledActiveFontSize = Math.max(\n MIN_READABLE_FONT_SIZE,\n activeFontSize * scaleFactor,\n );\n }\n\n // Render words positioned by their actual timing within the segment\n const renderWords = () => {\n if (useCompactText) {\n // Compact mode: show text that can overflow on hover\n return html`\n <span class=\"segment-text-compact\">${segment.text}</span>\n `;\n }\n\n // Positioned mode: render words at their time positions\n return wordsInSegment.map((word) => {\n // Position relative to segment start\n const wordOffsetFromSegmentStart =\n (word.start - segment.start) * 1000;\n const wordLeftPx = this.pixelsPerMs * wordOffsetFromSegmentStart;\n const wordWidthPx = this.pixelsPerMs * (word.end - word.start) * 1000;\n const isActive = word === activeWord;\n\n // Determine if word is in the future (after active word)\n const isFuture = activeWord && word.start > activeWord.end;\n\n return html`\n <span\n class=\"word-element ${isActive ? \"active\" : \"\"} ${isFuture ? \"future\" : \"\"}\"\n style=${styleMap({\n left: `${wordLeftPx}px`,\n minWidth: `${Math.max(wordWidthPx, 8)}px`,\n fontSize: isActive\n ? `${scaledActiveFontSize}px`\n : `${scaledFontSize}px`,\n top: \"50%\",\n })}\n title=\"Word: '${word.text}' (${word.start.toFixed(2)}s - ${word.end.toFixed(2)}s)\"\n >\n ${word.text.trim()}\n </span>\n `;\n });\n };\n\n return html`<div\n class=\"segment-block ${isActiveSegment ? \"active\" : \"\"} ${useCompactText ? \"compact-text\" : \"\"}\"\n style=${styleMap({\n left: `${segmentStartPx}px`,\n width: `${Math.max(segmentWidth, 4)}px`,\n height: \"100%\",\n top: \"0px\",\n backgroundColor: isActiveSegment\n ? `color-mix(in srgb, var(--ef-color-type-captions) ${30 + density * 20}%, transparent)`\n : `color-mix(in srgb, var(--ef-color-type-captions) ${10 + density * 10}%, transparent)`,\n borderColor: isActiveSegment\n ? captionColor\n : `color-mix(in srgb, var(--ef-color-type-captions) 40%, transparent)`,\n minWidth: segmentWidth < 20 ? \"20px\" : \"auto\",\n })}\n title=${\n useCompactText\n ? `Caption: '${segment.text}'\\nDuration: ${this.formatDuration(segmentDuration)}\\nTime: ${segment.start.toFixed(2)}s - ${segment.end.toFixed(2)}s`\n : `Caption: '${segment.text}'\\nDuration: ${this.formatDuration(segmentDuration)}\\nTime: ${segment.start.toFixed(2)}s - ${segment.end.toFixed(2)}s\\nWords: ${wordsInSegment.length}`\n }\n @click=${(e: MouseEvent) => {\n e.stopPropagation();\n // Affordance: Click to seek to segment start\n if (rootTimegroup) {\n const absoluteStartTime =\n captions.startTimeMs + segment.start * 1000;\n rootTimegroup.currentTimeMs = absoluteStartTime;\n }\n }}\n >\n ${renderWords()}\n ${!useCompactText ? html`<div class=\"segment-duration-indicator\"></div>` : nothing}\n </div>`;\n });\n\n return html`\n ${wordMarkers}\n ${segmentElements}\n `;\n }\n\n renderChildren(): Array<TemplateResult<1> | typeof nothing> | typeof nothing {\n // Don't render child tracks - captions are consolidated into a single track\n // Child elements (active-word, segment, before-word, after-word) are handled\n // inline within the main captions track visualization\n return nothing;\n }\n}\n\n@customElement(\"ef-captions-active-word-track\")\nexport class EFCaptionsActiveWordTrack extends TrackItem {\n get captionsTrackStyles() {\n const parentCaptions = this.element.closest(\"ef-captions\") as EFCaptions;\n return {\n position: \"relative\",\n left: `${this.pixelsPerMs * (parentCaptions?.startTimeWithinParentMs || 0)}px`,\n width: `${this.pixelsPerMs * (parentCaptions?.durationMs || 0)}px`,\n };\n }\n\n render() {\n const parentCaptions = this.element.closest(\"ef-captions\") as EFCaptions;\n const captionsData = parentCaptions?.unifiedCaptionsDataTask.value;\n\n if (!captionsData) {\n return html`<div style=${styleMap(this.captionsTrackStyles)}>\n <div class=\"border h-[1.1rem] mb-[1px] text-xs\" style=\"background-color: var(--filmstrip-bg); border-color: var(--filmstrip-border);\">\n ${phosphorIcon(ICONS.microphone)} Active Word\n </div>\n </div>`;\n }\n\n const rootTimegroup = parentCaptions.rootTimegroup;\n const currentTimeMs = rootTimegroup?.currentTimeMs || 0;\n const captionsLocalTimeMs = currentTimeMs - parentCaptions.startTimeMs;\n const captionsLocalTimeSec = captionsLocalTimeMs / 1000;\n\n return html`<div style=${styleMap(this.captionsTrackStyles)}>\n <div class=\"relative border h-[1.1rem] mb-[1px] w-full\" style=\"background-color: var(--filmstrip-bg); border-color: var(--filmstrip-border);\">\n ${captionsData.word_segments.map((word) => {\n const isCurrentlyActive =\n captionsLocalTimeSec >= word.start &&\n captionsLocalTimeSec < word.end;\n\n return html`<div\n class=\"absolute border text-xs overflow-visible flex items-center ${isCurrentlyActive ? \"font-bold z-[5]\" : \"\"}\"\n style=${styleMap({\n left: `${this.pixelsPerMs * word.start * 1000}px`,\n width: `${this.pixelsPerMs * (word.end - word.start) * 1000}px`,\n height: \"100%\",\n top: \"0px\",\n backgroundColor: isCurrentlyActive\n ? \"var(--filmstrip-caption-bg)\"\n : \"var(--filmstrip-item-bg)\",\n borderColor: isCurrentlyActive\n ? \"var(--filmstrip-caption-border)\"\n : \"var(--filmstrip-border)\",\n })}\n title=\"Word: '${word.text}' (${word.start}s - ${word.end}s)\"\n >\n ${isCurrentlyActive ? html`<span class=\"px-0.5 text-[8px] font-bold whitespace-nowrap\" style=\"background-color: var(--filmstrip-caption-bg);\">${word.text.trim()}</span>` : \"\"}\n </div>`;\n })}\n </div>\n </div>`;\n }\n}\n\n@customElement(\"ef-captions-segment-track\")\nexport class EFCaptionsSegmentTrack extends TrackItem {\n get captionsTrackStyles() {\n const parentCaptions = this.element.closest(\"ef-captions\") as EFCaptions;\n return {\n position: \"relative\",\n left: `${this.pixelsPerMs * (parentCaptions?.startTimeWithinParentMs || 0)}px`,\n width: `${this.pixelsPerMs * (parentCaptions?.durationMs || 0)}px`,\n };\n }\n\n render() {\n const parentCaptions = this.element.closest(\"ef-captions\") as EFCaptions;\n const captionsData = parentCaptions?.unifiedCaptionsDataTask.value;\n\n if (!captionsData) {\n return html`<div style=${styleMap(this.captionsTrackStyles)}>\n <div class=\"border h-[1.1rem] mb-[1px] text-xs\" style=\"background-color: var(--filmstrip-bg); border-color: var(--filmstrip-border);\">\n ${phosphorIcon(ICONS.textT)} Segment\n </div>\n </div>`;\n }\n\n const rootTimegroup = parentCaptions.rootTimegroup;\n const currentTimeMs = rootTimegroup?.currentTimeMs || 0;\n const captionsLocalTimeMs = currentTimeMs - parentCaptions.startTimeMs;\n const captionsLocalTimeSec = captionsLocalTimeMs / 1000;\n\n return html`<div style=${styleMap(this.captionsTrackStyles)}>\n <div class=\"relative border h-[1.1rem] mb-[1px] w-full\" style=\"background-color: var(--filmstrip-bg); border-color: var(--filmstrip-border);\">\n ${captionsData.segments.map((segment) => {\n const isCurrentlyActive =\n captionsLocalTimeSec >= segment.start &&\n captionsLocalTimeSec < segment.end;\n\n return html`<div\n class=\"absolute border text-xs overflow-visible flex items-center ${isCurrentlyActive ? \"font-bold z-[5]\" : \"\"}\"\n style=${styleMap({\n left: `${this.pixelsPerMs * segment.start * 1000}px`,\n width: `${this.pixelsPerMs * (segment.end - segment.start) * 1000}px`,\n height: \"100%\",\n top: \"0px\",\n backgroundColor: isCurrentlyActive\n ? \"var(--filmstrip-segment-bg)\"\n : \"var(--filmstrip-item-bg)\",\n borderColor: isCurrentlyActive\n ? \"var(--filmstrip-segment-border)\"\n : \"var(--filmstrip-border)\",\n })}\n title=\"Segment: '${segment.text}' (${segment.start}s - ${segment.end}s)\"\n >\n ${isCurrentlyActive ? html`<span class=\"px-0.5 text-[8px] font-bold whitespace-nowrap\" style=\"background-color: var(--filmstrip-segment-bg);\">${segment.text}</span>` : \"\"}\n </div>`;\n })}\n </div>\n </div>`;\n }\n}\n\n@customElement(\"ef-captions-before-word-track\")\nexport class EFCaptionsBeforeWordTrack extends TrackItem {\n get captionsTrackStyles() {\n const parentCaptions = this.element.closest(\"ef-captions\") as EFCaptions;\n return {\n position: \"relative\",\n left: `${this.pixelsPerMs * (parentCaptions?.startTimeWithinParentMs || 0)}px`,\n width: `${this.pixelsPerMs * (parentCaptions?.durationMs || 0)}px`,\n };\n }\n\n render() {\n const parentCaptions = this.element.closest(\"ef-captions\") as EFCaptions;\n const captionsData = parentCaptions?.unifiedCaptionsDataTask.value;\n\n if (!captionsData) {\n return html`<div style=${styleMap(this.captionsTrackStyles)}>\n <div class=\"border h-[1.1rem] mb-[1px] text-xs\" style=\"background-color: var(--filmstrip-bg); border-color: var(--filmstrip-border);\">\n ${phosphorIcon(ICONS.arrowLeft)} Before\n </div>\n </div>`;\n }\n\n const rootTimegroup = parentCaptions.rootTimegroup;\n const currentTimeMs = rootTimegroup?.currentTimeMs || 0;\n const captionsLocalTimeMs = currentTimeMs - parentCaptions.startTimeMs;\n const captionsLocalTimeSec = captionsLocalTimeMs / 1000;\n\n return html`<div style=${styleMap(this.captionsTrackStyles)}>\n <div class=\"relative border h-[1.1rem] mb-[1px] w-full\" style=\"background-color: var(--filmstrip-bg); border-color: var(--filmstrip-border);\">\n ${captionsData.word_segments.map((word) => {\n const isCurrentlyActive =\n captionsLocalTimeSec >= word.start &&\n captionsLocalTimeSec < word.end;\n\n return html`<div\n class=\"absolute border text-xs overflow-visible flex items-center ${isCurrentlyActive ? \"font-bold z-[5]\" : \"\"}\"\n style=${styleMap({\n left: `${this.pixelsPerMs * word.start * 1000}px`,\n width: `${this.pixelsPerMs * (word.end - word.start) * 1000}px`,\n height: \"100%\",\n top: \"0px\",\n backgroundColor: isCurrentlyActive\n ? \"var(--filmstrip-caption-bg)\"\n : \"var(--filmstrip-waveform-bg)\",\n borderColor: isCurrentlyActive\n ? \"var(--filmstrip-caption-border)\"\n : \"var(--filmstrip-waveform-border)\",\n })}\n title=\"Word: '${word.text}' (${word.start}s - ${word.end}s)\"\n >\n </div>`;\n })}\n </div>\n </div>`;\n }\n}\n\n@customElement(\"ef-captions-after-word-track\")\nexport class EFCaptionsAfterWordTrack extends TrackItem {\n get captionsTrackStyles() {\n const parentCaptions = this.element.closest(\"ef-captions\") as EFCaptions;\n return {\n position: \"relative\",\n left: `${this.pixelsPerMs * (parentCaptions?.startTimeWithinParentMs || 0)}px`,\n width: `${this.pixelsPerMs * (parentCaptions?.durationMs || 0)}px`,\n };\n }\n\n render() {\n const parentCaptions = this.element.closest(\"ef-captions\") as EFCaptions;\n const captionsData = parentCaptions?.unifiedCaptionsDataTask.value;\n\n if (!captionsData) {\n return html`<div style=${styleMap(this.captionsTrackStyles)}>\n <div class=\"border h-[1.1rem] mb-[1px] text-xs\" style=\"background-color: var(--filmstrip-bg); border-color: var(--filmstrip-border);\">\n ${phosphorIcon(ICONS.arrowRight)} After\n </div>\n </div>`;\n }\n\n const rootTimegroup = parentCaptions.rootTimegroup;\n const currentTimeMs = rootTimegroup?.currentTimeMs || 0;\n const captionsLocalTimeMs = currentTimeMs - parentCaptions.startTimeMs;\n const captionsLocalTimeSec = captionsLocalTimeMs / 1000;\n\n return html`<div style=${styleMap(this.captionsTrackStyles)}>\n <div class=\"relative border h-[1.1rem] mb-[1px] w-full\" style=\"background-color: var(--filmstrip-bg); border-color: var(--filmstrip-border);\">\n ${captionsData.word_segments.map((word) => {\n const isCurrentlyActive =\n captionsLocalTimeSec >= word.start &&\n captionsLocalTimeSec < word.end;\n\n return html`<div\n class=\"absolute border text-xs overflow-visible flex items-center ${isCurrentlyActive ? \"font-bold z-[5]\" : \"\"}\"\n style=${styleMap({\n left: `${this.pixelsPerMs * word.start * 1000}px`,\n width: `${this.pixelsPerMs * (word.end - word.start) * 1000}px`,\n height: \"100%\",\n top: \"0px\",\n backgroundColor: isCurrentlyActive\n ? \"var(--filmstrip-caption-bg)\"\n : \"var(--filmstrip-waveform-bg)\",\n borderColor: isCurrentlyActive\n ? \"var(--filmstrip-caption-border)\"\n : \"var(--filmstrip-waveform-border)\",\n })}\n title=\"Word: '${word.text}' (${word.start}s - ${word.end}s)\"\n >\n </div>`;\n })}\n </div>\n </div>`;\n }\n}\n\ndeclare global {\n interface HTMLElementTagNameMap {\n \"ef-captions-track\": EFCaptionsTrack;\n \"ef-captions-active-word-track\": EFCaptionsActiveWordTrack;\n \"ef-captions-segment-track\": EFCaptionsSegmentTrack;\n \"ef-captions-before-word-track\": EFCaptionsBeforeWordTrack;\n \"ef-captions-after-word-track\": EFCaptionsAfterWordTrack;\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"CaptionsTrack.js","names":["measurementCanvas: HTMLCanvasElement | null","measurementContext: CanvasRenderingContext2D | null","wordWidths: Array<{\n textWidth: number;\n timeWidth: number;\n startPx: number;\n endPx: number;\n }>","EFCaptionsTrack","EFCaptionsActiveWordTrack","EFCaptionsSegmentTrack","EFCaptionsBeforeWordTrack","EFCaptionsAfterWordTrack"],"sources":["../../../../src/gui/timeline/tracks/CaptionsTrack.ts"],"sourcesContent":["import { consume } from \"@lit/context\";\nimport { css, html, nothing, type TemplateResult } from \"lit\";\nimport { customElement } from \"lit/decorators.js\";\nimport { styleMap } from \"lit/directives/style-map.js\";\nimport { type Caption, EFCaptions, type WordSegment } from \"../../../elements/EFCaptions.js\";\nimport { phosphorIcon, ICONS } from \"../../icons.js\";\nimport { currentTimeContext } from \"../../currentTimeContext.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 { getElementTypeColor } from \"../../theme.js\";\n\n// Shared canvas context for text measurement (avoids creating new canvas each time)\nlet measurementCanvas: HTMLCanvasElement | null = null;\nlet measurementContext: CanvasRenderingContext2D | null = null;\n// Cache for text measurements: key is \"text:fontSize:fontWeight\"\nconst textMeasurementCache = new Map<string, number>();\nconst MAX_CACHE_SIZE = 500;\n\n/**\n * Measure text width accurately using canvas.\n * Matches the actual font used in word elements (font-weight: 500).\n * Results are cached to avoid repeated measurements of the same text.\n */\nfunction measureTextWidth(text: string, fontSize: number, fontWeight: number = 500): number {\n // Check cache first\n const cacheKey = `${text}:${fontSize}:${fontWeight}`;\n const cached = textMeasurementCache.get(cacheKey);\n if (cached !== undefined) {\n return cached;\n }\n\n // Initialize shared canvas context if needed\n if (!measurementCanvas || !measurementContext) {\n measurementCanvas = document.createElement(\"canvas\");\n measurementContext = measurementCanvas.getContext(\"2d\");\n }\n\n if (!measurementContext) {\n return text.length * fontSize * 0.6; // Fallback estimate\n }\n\n // Match the actual font used in word elements\n const fontFamily = getComputedStyle(document.body).fontFamily || \"system-ui, sans-serif\";\n measurementContext.font = `${fontWeight} ${fontSize}px ${fontFamily}`;\n const width = measurementContext.measureText(text).width;\n\n // Cache the result (with size limit to prevent memory leaks)\n if (textMeasurementCache.size >= MAX_CACHE_SIZE) {\n // Clear oldest entries (simple strategy: clear half the cache)\n const keysToDelete = Array.from(textMeasurementCache.keys()).slice(0, MAX_CACHE_SIZE / 2);\n for (const key of keysToDelete) {\n textMeasurementCache.delete(key);\n }\n }\n textMeasurementCache.set(cacheKey, width);\n\n return width;\n}\n\n/**\n * Check if words can fit individually within a segment when positioned by time\n *\n * Strategy: Allow overlaps as long as all words can be rendered within the container.\n * Only use compact mode when words are so cramped they can't be displayed at all.\n */\nfunction canWordsFitIndividually(\n words: WordSegment[],\n segmentStart: number,\n segmentWidthPx: number,\n pixelsPerMs: number,\n): { fits: boolean; reason?: string } {\n if (words.length === 0) {\n return { fits: false, reason: \"no words\" };\n }\n\n // Measure total text width of all words (as if rendered sequentially)\n let totalTextWidth = 0;\n const wordWidths: Array<{\n textWidth: number;\n timeWidth: number;\n startPx: number;\n endPx: number;\n }> = [];\n\n for (const word of words) {\n if (!word) continue;\n\n // Measure actual text width (with padding: 2px left + 2px right = 4px total)\n const textWidth = measureTextWidth(word.text.trim(), 9, 500) + 4;\n\n // Calculate time-based position and width\n const startPx = pixelsPerMs * (word.start - segmentStart) * 1000;\n const endPx = pixelsPerMs * (word.end - segmentStart) * 1000;\n const timeWidth = endPx - startPx;\n\n wordWidths.push({ textWidth, timeWidth, startPx, endPx });\n totalTextWidth += textWidth;\n }\n\n // Key insight: If total text width fits in segment, we can render words individually\n // even if they overlap based on their time positions\n // Use 90% threshold to account for some spacing/overlap\n if (totalTextWidth <= segmentWidthPx * 0.9) {\n // All words can fit - use positioned mode (overlaps are okay)\n return { fits: true };\n }\n\n // If total text doesn't fit, check if individual words are too narrow to be readable\n // If any word's time-based width is less than 30% of its text width, it's unreadable\n for (const { textWidth, timeWidth } of wordWidths) {\n if (timeWidth < textWidth * 0.3) {\n return {\n fits: false,\n reason: `word too narrow (${timeWidth.toFixed(1)}px < ${(textWidth * 0.3).toFixed(1)}px)`,\n };\n }\n }\n\n // If words are readable individually but total text is too wide,\n // check if they can still fit with overlaps\n // Find the maximum right edge of all words\n const maxEndPx = Math.max(...wordWidths.map((w) => w.endPx));\n\n // If the rightmost word fits within the segment, allow overlaps\n if (maxEndPx <= segmentWidthPx * 1.1) {\n return { fits: true };\n }\n\n // Words don't fit - use compact mode\n return {\n fits: false,\n reason: `words exceed segment (total text: ${totalTextWidth.toFixed(1)}px, segment: ${segmentWidthPx.toFixed(1)}px)`,\n };\n}\n\n@customElement(\"ef-captions-track\")\nexport class EFCaptionsTrack extends TrackItem {\n static styles = [\n ...TrackItem.styles,\n css`\n .segment-block {\n position: absolute;\n border-radius: 3px;\n transition: box-shadow 0.15s ease, z-index 0.15s ease;\n cursor: pointer;\n overflow: visible;\n }\n \n .segment-block:hover {\n box-shadow: 0 1px 4px rgba(0, 0, 0, 0.3);\n z-index: 5;\n }\n \n .word-element {\n position: absolute;\n font-size: 9px;\n line-height: 1.2;\n white-space: nowrap;\n font-weight: 500;\n top: 50%;\n transform: translateY(-50%);\n padding: 2px 4px;\n border-radius: 2px;\n transition: all 0.1s ease;\n background: var(--ef-color-bg-elevated);\n color: var(--ef-color-text);\n z-index: 1;\n }\n \n .word-element.active {\n background: var(--ef-color-success);\n color: rgb(20, 30, 20);\n font-weight: 700;\n font-size: 10px;\n z-index: 10;\n box-shadow: 0 1px 3px rgba(0, 0, 0, 0.25);\n }\n \n .word-element.future {\n background: var(--ef-color-bg-inset);\n color: var(--ef-color-text);\n z-index: 5;\n }\n \n .segment-block.active .word-element:not(.active):not(.future) {\n color: var(--ef-color-text-muted);\n background: var(--ef-color-bg-elevated);\n }\n \n /* Compact text mode - when words are too small to position individually */\n .segment-block.compact-text {\n display: flex;\n align-items: center;\n padding: 0 4px;\n overflow: hidden;\n /* Keep position: absolute from .segment-block for correct time-based positioning */\n }\n \n /* Allow overflow on hover for compact text */\n .segment-block.compact-text:hover {\n overflow: visible;\n z-index: 100;\n /* Expand to fit content on hover */\n width: max-content !important;\n min-width: max-content;\n background: var(--ef-color-bg-elevated) !important;\n box-shadow: 0 2px 8px rgba(0, 0, 0, 0.4);\n }\n \n .segment-text-compact {\n font-size: 10px;\n line-height: 1.2;\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n color: var(--ef-color-text);\n width: 100%;\n }\n \n /* On hover, show full text */\n .segment-block.compact-text:hover .segment-text-compact {\n overflow: visible;\n text-overflow: clip;\n }\n \n .segment-block.compact-text.active .segment-text-compact {\n color: var(--ef-color-text-muted);\n font-weight: 500;\n }\n \n .segment-duration-indicator {\n position: absolute;\n bottom: 0;\n left: 0;\n right: 0;\n height: 2px;\n background: currentColor;\n opacity: 0.2;\n border-radius: 0 0 3px 3px;\n }\n \n .segment-block.active .segment-duration-indicator {\n opacity: 0.4;\n height: 2px;\n }\n \n .word-marker {\n position: absolute;\n bottom: 0;\n width: 1px;\n height: 30%;\n background: var(--ef-color-border-subtle);\n pointer-events: none;\n }\n \n .word-marker.active {\n background: var(--ef-color-text);\n height: 50%;\n width: 2px;\n }\n `,\n ];\n\n @consume({ context: currentTimeContext, subscribe: true })\n contextCurrentTimeMs = 0;\n\n private lastPixelsPerMs = 0;\n\n protected updated(changedProperties: Map<string | number | symbol, unknown>): void {\n super.updated(changedProperties);\n\n // Re-render when pixelsPerMs changes (zoom level changes)\n if (changedProperties.has(\"pixelsPerMs\")) {\n const currentPixelsPerMs = this.pixelsPerMs;\n if (currentPixelsPerMs !== this.lastPixelsPerMs) {\n this.lastPixelsPerMs = currentPixelsPerMs;\n // Force update to recalculate layout mode\n this.requestUpdate();\n }\n }\n }\n\n render() {\n const captions = this.element as EFCaptions;\n const captionsData = captions.unifiedCaptionsDataTask.value;\n\n return html`<div style=${styleMap(this.gutterStyles)}>\n <div\n class=\"relative\"\n style=\"background-color: var(--filmstrip-bg);\"\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 relative mb-0 block text-nowrap border text-sm overflow-visible\"\n style=${styleMap({\n ...this.trimPortionStyles,\n height: \"var(--timeline-track-height, 22px)\",\n backgroundColor: this.isFocused\n ? \"var(--filmstrip-item-focused)\"\n : \"var(--filmstrip-item-bg)\",\n borderColor: \"var(--filmstrip-border)\",\n borderLeftColor: this.getElementTypeColor(),\n borderLeftWidth: \"3px\",\n minHeight: \"22px\",\n })}\n >\n ${this.renderCaptionsData(captionsData)}\n ${\n this.enableTrim\n ? html`<ef-trim-handles\n element-id=${(this.element as HTMLElement).id || \"\"}\n pixels-per-ms=${this.pixelsPerMs}\n trim-start-ms=${this.element.trimStartMs ?? 0}\n trim-end-ms=${this.element.trimEndMs ?? 0}\n intrinsic-duration-ms=${this.element.intrinsicDurationMs ?? this.element.durationMs}\n @trim-change=${this.handleTrimChange}\n ></ef-trim-handles>`\n : nothing\n }\n </div>\n </div>\n ${this.renderChildren()}\n </div>`;\n }\n\n renderCaptionsData(captionsData: Caption | null | undefined) {\n if (!captionsData) {\n return html``;\n }\n\n const captions = this.element as EFCaptions;\n const rootTimegroup = captions.rootTimegroup;\n // Use context current time for reactivity, fallback to rootTimegroup\n const currentTimeMs = this.contextCurrentTimeMs || rootTimegroup?.currentTimeMs || 0;\n const captionsLocalTimeMs = currentTimeMs - captions.startTimeMs;\n const captionsLocalTimeSec = captionsLocalTimeMs / 1000;\n\n // Get element type color for captions using shared theme utility\n const captionColor = getElementTypeColor(\"captions\", this);\n\n // Find active word for highlighting\n const activeWord = captionsData.word_segments.find(\n (word) => captionsLocalTimeSec >= word.start && captionsLocalTimeSec < word.end,\n );\n\n // Render word markers for visual density indication (subtle)\n const wordMarkers = captionsData.word_segments.map((word) => {\n const wordStartPx = this.pixelsPerMs * word.start * 1000;\n const wordWidth = this.pixelsPerMs * (word.end - word.start) * 1000;\n const isActive = word === activeWord;\n\n // Only show markers if they're wide enough to be visible\n if (wordWidth < 1.5) return nothing;\n\n return html`<div\n class=\"word-marker ${isActive ? \"active\" : \"\"}\"\n style=${styleMap({\n left: `${wordStartPx}px`,\n })}\n ></div>`;\n });\n\n // Render semantic segment blocks with words positioned by their actual timing\n const segmentElements = captionsData.segments.map((segment) => {\n const isActiveSegment =\n captionsLocalTimeSec >= segment.start && captionsLocalTimeSec < segment.end;\n\n const segmentStartPx = this.pixelsPerMs * segment.start * 1000;\n const segmentWidth = this.pixelsPerMs * (segment.end - segment.start) * 1000;\n const segmentDuration = (segment.end - segment.start) * 1000;\n\n // Get words in this segment, sorted by start time\n const wordsInSegment = captionsData.word_segments\n .filter((word) => word.start >= segment.start && word.end <= segment.end)\n .sort((a, b) => a.start - b.start);\n\n // Calculate visual density based on word count\n const density = Math.min(wordsInSegment.length / 10, 1);\n\n // Use actual measurement to determine if words can fit individually\n // Allow overlaps - only use compact mode when words can't be rendered at all\n const measurementResult = canWordsFitIndividually(\n wordsInSegment,\n segment.start,\n segmentWidth,\n this.pixelsPerMs,\n );\n\n const useCompactText = !measurementResult.fits;\n let avgSpacing = 0;\n\n // Calculate average spacing for font scaling (only if using positioned mode)\n if (!useCompactText && wordsInSegment.length > 1) {\n let totalSpacing = 0;\n let spacingCount = 0;\n\n for (let i = 0; i < wordsInSegment.length - 1; i++) {\n const word1 = wordsInSegment[i];\n const word2 = wordsInSegment[i + 1];\n if (!word1 || !word2) continue;\n\n const word1EndPx = this.pixelsPerMs * (word1.end - segment.start) * 1000;\n const word2StartPx = this.pixelsPerMs * (word2.start - segment.start) * 1000;\n const spacing = word2StartPx - word1EndPx;\n\n if (spacing > 0) {\n totalSpacing += spacing;\n spacingCount++;\n }\n }\n\n avgSpacing = spacingCount > 0 ? totalSpacing / spacingCount : 0;\n }\n\n // Calculate optimal font size for positioned words (if not using compact mode)\n const MIN_READABLE_FONT_SIZE = 6; // Minimum readable font size in pixels\n const baseFontSize = 9;\n const activeFontSize = 10;\n let scaledFontSize = baseFontSize;\n let scaledActiveFontSize = activeFontSize;\n\n if (!useCompactText && wordsInSegment.length > 1 && avgSpacing < 8) {\n // Scale down font size proportionally, but don't go below minimum\n const scaleFactor = Math.max(MIN_READABLE_FONT_SIZE / baseFontSize, avgSpacing / 8);\n scaledFontSize = Math.max(MIN_READABLE_FONT_SIZE, baseFontSize * scaleFactor);\n scaledActiveFontSize = Math.max(MIN_READABLE_FONT_SIZE, activeFontSize * scaleFactor);\n }\n\n // Render words positioned by their actual timing within the segment\n const renderWords = () => {\n if (useCompactText) {\n // Compact mode: show text that can overflow on hover\n return html`\n <span class=\"segment-text-compact\">${segment.text}</span>\n `;\n }\n\n // Positioned mode: render words at their time positions\n return wordsInSegment.map((word) => {\n // Position relative to segment start\n const wordOffsetFromSegmentStart = (word.start - segment.start) * 1000;\n const wordLeftPx = this.pixelsPerMs * wordOffsetFromSegmentStart;\n const wordWidthPx = this.pixelsPerMs * (word.end - word.start) * 1000;\n const isActive = word === activeWord;\n\n // Determine if word is in the future (after active word)\n const isFuture = activeWord && word.start > activeWord.end;\n\n return html`\n <span\n class=\"word-element ${isActive ? \"active\" : \"\"} ${isFuture ? \"future\" : \"\"}\"\n style=${styleMap({\n left: `${wordLeftPx}px`,\n minWidth: `${Math.max(wordWidthPx, 8)}px`,\n fontSize: isActive ? `${scaledActiveFontSize}px` : `${scaledFontSize}px`,\n top: \"50%\",\n })}\n title=\"Word: '${word.text}' (${word.start.toFixed(2)}s - ${word.end.toFixed(2)}s)\"\n >\n ${word.text.trim()}\n </span>\n `;\n });\n };\n\n return html`<div\n class=\"segment-block ${isActiveSegment ? \"active\" : \"\"} ${useCompactText ? \"compact-text\" : \"\"}\"\n style=${styleMap({\n left: `${segmentStartPx}px`,\n width: `${Math.max(segmentWidth, 4)}px`,\n height: \"100%\",\n top: \"0px\",\n backgroundColor: isActiveSegment\n ? `color-mix(in srgb, var(--ef-color-type-captions) ${30 + density * 20}%, transparent)`\n : `color-mix(in srgb, var(--ef-color-type-captions) ${10 + density * 10}%, transparent)`,\n borderColor: isActiveSegment\n ? captionColor\n : `color-mix(in srgb, var(--ef-color-type-captions) 40%, transparent)`,\n minWidth: segmentWidth < 20 ? \"20px\" : \"auto\",\n })}\n title=${\n useCompactText\n ? `Caption: '${segment.text}'\\nDuration: ${this.formatDuration(segmentDuration)}\\nTime: ${segment.start.toFixed(2)}s - ${segment.end.toFixed(2)}s`\n : `Caption: '${segment.text}'\\nDuration: ${this.formatDuration(segmentDuration)}\\nTime: ${segment.start.toFixed(2)}s - ${segment.end.toFixed(2)}s\\nWords: ${wordsInSegment.length}`\n }\n @click=${(e: MouseEvent) => {\n e.stopPropagation();\n // Affordance: Click to seek to segment start\n if (rootTimegroup) {\n const absoluteStartTime = captions.startTimeMs + segment.start * 1000;\n rootTimegroup.currentTimeMs = absoluteStartTime;\n }\n }}\n >\n ${renderWords()}\n ${!useCompactText ? html`<div class=\"segment-duration-indicator\"></div>` : nothing}\n </div>`;\n });\n\n return html`\n ${wordMarkers}\n ${segmentElements}\n `;\n }\n\n renderChildren(): Array<TemplateResult<1> | typeof nothing> | typeof nothing {\n // Don't render child tracks - captions are consolidated into a single track\n // Child elements (active-word, segment, before-word, after-word) are handled\n // inline within the main captions track visualization\n return nothing;\n }\n}\n\n@customElement(\"ef-captions-active-word-track\")\nexport class EFCaptionsActiveWordTrack extends TrackItem {\n get captionsTrackStyles() {\n const parentCaptions = this.element.closest(\"ef-captions\") as EFCaptions;\n return {\n position: \"relative\",\n left: `${this.pixelsPerMs * (parentCaptions?.startTimeWithinParentMs || 0)}px`,\n width: `${this.pixelsPerMs * (parentCaptions?.durationMs || 0)}px`,\n };\n }\n\n render() {\n const parentCaptions = this.element.closest(\"ef-captions\") as EFCaptions;\n const captionsData = parentCaptions?.unifiedCaptionsDataTask.value;\n\n if (!captionsData) {\n return html`<div style=${styleMap(this.captionsTrackStyles)}>\n <div class=\"border h-[1.1rem] mb-[1px] text-xs\" style=\"background-color: var(--filmstrip-bg); border-color: var(--filmstrip-border);\">\n ${phosphorIcon(ICONS.microphone)} Active Word\n </div>\n </div>`;\n }\n\n const rootTimegroup = parentCaptions.rootTimegroup;\n const currentTimeMs = rootTimegroup?.currentTimeMs || 0;\n const captionsLocalTimeMs = currentTimeMs - parentCaptions.startTimeMs;\n const captionsLocalTimeSec = captionsLocalTimeMs / 1000;\n\n return html`<div style=${styleMap(this.captionsTrackStyles)}>\n <div class=\"relative border h-[1.1rem] mb-[1px] w-full\" style=\"background-color: var(--filmstrip-bg); border-color: var(--filmstrip-border);\">\n ${captionsData.word_segments.map((word) => {\n const isCurrentlyActive =\n captionsLocalTimeSec >= word.start && captionsLocalTimeSec < word.end;\n\n return html`<div\n class=\"absolute border text-xs overflow-visible flex items-center ${isCurrentlyActive ? \"font-bold z-[5]\" : \"\"}\"\n style=${styleMap({\n left: `${this.pixelsPerMs * word.start * 1000}px`,\n width: `${this.pixelsPerMs * (word.end - word.start) * 1000}px`,\n height: \"100%\",\n top: \"0px\",\n backgroundColor: isCurrentlyActive\n ? \"var(--filmstrip-caption-bg)\"\n : \"var(--filmstrip-item-bg)\",\n borderColor: isCurrentlyActive\n ? \"var(--filmstrip-caption-border)\"\n : \"var(--filmstrip-border)\",\n })}\n title=\"Word: '${word.text}' (${word.start}s - ${word.end}s)\"\n >\n ${isCurrentlyActive ? html`<span class=\"px-0.5 text-[8px] font-bold whitespace-nowrap\" style=\"background-color: var(--filmstrip-caption-bg);\">${word.text.trim()}</span>` : \"\"}\n </div>`;\n })}\n </div>\n </div>`;\n }\n}\n\n@customElement(\"ef-captions-segment-track\")\nexport class EFCaptionsSegmentTrack extends TrackItem {\n get captionsTrackStyles() {\n const parentCaptions = this.element.closest(\"ef-captions\") as EFCaptions;\n return {\n position: \"relative\",\n left: `${this.pixelsPerMs * (parentCaptions?.startTimeWithinParentMs || 0)}px`,\n width: `${this.pixelsPerMs * (parentCaptions?.durationMs || 0)}px`,\n };\n }\n\n render() {\n const parentCaptions = this.element.closest(\"ef-captions\") as EFCaptions;\n const captionsData = parentCaptions?.unifiedCaptionsDataTask.value;\n\n if (!captionsData) {\n return html`<div style=${styleMap(this.captionsTrackStyles)}>\n <div class=\"border h-[1.1rem] mb-[1px] text-xs\" style=\"background-color: var(--filmstrip-bg); border-color: var(--filmstrip-border);\">\n ${phosphorIcon(ICONS.textT)} Segment\n </div>\n </div>`;\n }\n\n const rootTimegroup = parentCaptions.rootTimegroup;\n const currentTimeMs = rootTimegroup?.currentTimeMs || 0;\n const captionsLocalTimeMs = currentTimeMs - parentCaptions.startTimeMs;\n const captionsLocalTimeSec = captionsLocalTimeMs / 1000;\n\n return html`<div style=${styleMap(this.captionsTrackStyles)}>\n <div class=\"relative border h-[1.1rem] mb-[1px] w-full\" style=\"background-color: var(--filmstrip-bg); border-color: var(--filmstrip-border);\">\n ${captionsData.segments.map((segment) => {\n const isCurrentlyActive =\n captionsLocalTimeSec >= segment.start && captionsLocalTimeSec < segment.end;\n\n return html`<div\n class=\"absolute border text-xs overflow-visible flex items-center ${isCurrentlyActive ? \"font-bold z-[5]\" : \"\"}\"\n style=${styleMap({\n left: `${this.pixelsPerMs * segment.start * 1000}px`,\n width: `${this.pixelsPerMs * (segment.end - segment.start) * 1000}px`,\n height: \"100%\",\n top: \"0px\",\n backgroundColor: isCurrentlyActive\n ? \"var(--filmstrip-segment-bg)\"\n : \"var(--filmstrip-item-bg)\",\n borderColor: isCurrentlyActive\n ? \"var(--filmstrip-segment-border)\"\n : \"var(--filmstrip-border)\",\n })}\n title=\"Segment: '${segment.text}' (${segment.start}s - ${segment.end}s)\"\n >\n ${isCurrentlyActive ? html`<span class=\"px-0.5 text-[8px] font-bold whitespace-nowrap\" style=\"background-color: var(--filmstrip-segment-bg);\">${segment.text}</span>` : \"\"}\n </div>`;\n })}\n </div>\n </div>`;\n }\n}\n\n@customElement(\"ef-captions-before-word-track\")\nexport class EFCaptionsBeforeWordTrack extends TrackItem {\n get captionsTrackStyles() {\n const parentCaptions = this.element.closest(\"ef-captions\") as EFCaptions;\n return {\n position: \"relative\",\n left: `${this.pixelsPerMs * (parentCaptions?.startTimeWithinParentMs || 0)}px`,\n width: `${this.pixelsPerMs * (parentCaptions?.durationMs || 0)}px`,\n };\n }\n\n render() {\n const parentCaptions = this.element.closest(\"ef-captions\") as EFCaptions;\n const captionsData = parentCaptions?.unifiedCaptionsDataTask.value;\n\n if (!captionsData) {\n return html`<div style=${styleMap(this.captionsTrackStyles)}>\n <div class=\"border h-[1.1rem] mb-[1px] text-xs\" style=\"background-color: var(--filmstrip-bg); border-color: var(--filmstrip-border);\">\n ${phosphorIcon(ICONS.arrowLeft)} Before\n </div>\n </div>`;\n }\n\n const rootTimegroup = parentCaptions.rootTimegroup;\n const currentTimeMs = rootTimegroup?.currentTimeMs || 0;\n const captionsLocalTimeMs = currentTimeMs - parentCaptions.startTimeMs;\n const captionsLocalTimeSec = captionsLocalTimeMs / 1000;\n\n return html`<div style=${styleMap(this.captionsTrackStyles)}>\n <div class=\"relative border h-[1.1rem] mb-[1px] w-full\" style=\"background-color: var(--filmstrip-bg); border-color: var(--filmstrip-border);\">\n ${captionsData.word_segments.map((word) => {\n const isCurrentlyActive =\n captionsLocalTimeSec >= word.start && captionsLocalTimeSec < word.end;\n\n return html`<div\n class=\"absolute border text-xs overflow-visible flex items-center ${isCurrentlyActive ? \"font-bold z-[5]\" : \"\"}\"\n style=${styleMap({\n left: `${this.pixelsPerMs * word.start * 1000}px`,\n width: `${this.pixelsPerMs * (word.end - word.start) * 1000}px`,\n height: \"100%\",\n top: \"0px\",\n backgroundColor: isCurrentlyActive\n ? \"var(--filmstrip-caption-bg)\"\n : \"var(--filmstrip-waveform-bg)\",\n borderColor: isCurrentlyActive\n ? \"var(--filmstrip-caption-border)\"\n : \"var(--filmstrip-waveform-border)\",\n })}\n title=\"Word: '${word.text}' (${word.start}s - ${word.end}s)\"\n >\n </div>`;\n })}\n </div>\n </div>`;\n }\n}\n\n@customElement(\"ef-captions-after-word-track\")\nexport class EFCaptionsAfterWordTrack extends TrackItem {\n get captionsTrackStyles() {\n const parentCaptions = this.element.closest(\"ef-captions\") as EFCaptions;\n return {\n position: \"relative\",\n left: `${this.pixelsPerMs * (parentCaptions?.startTimeWithinParentMs || 0)}px`,\n width: `${this.pixelsPerMs * (parentCaptions?.durationMs || 0)}px`,\n };\n }\n\n render() {\n const parentCaptions = this.element.closest(\"ef-captions\") as EFCaptions;\n const captionsData = parentCaptions?.unifiedCaptionsDataTask.value;\n\n if (!captionsData) {\n return html`<div style=${styleMap(this.captionsTrackStyles)}>\n <div class=\"border h-[1.1rem] mb-[1px] text-xs\" style=\"background-color: var(--filmstrip-bg); border-color: var(--filmstrip-border);\">\n ${phosphorIcon(ICONS.arrowRight)} After\n </div>\n </div>`;\n }\n\n const rootTimegroup = parentCaptions.rootTimegroup;\n const currentTimeMs = rootTimegroup?.currentTimeMs || 0;\n const captionsLocalTimeMs = currentTimeMs - parentCaptions.startTimeMs;\n const captionsLocalTimeSec = captionsLocalTimeMs / 1000;\n\n return html`<div style=${styleMap(this.captionsTrackStyles)}>\n <div class=\"relative border h-[1.1rem] mb-[1px] w-full\" style=\"background-color: var(--filmstrip-bg); border-color: var(--filmstrip-border);\">\n ${captionsData.word_segments.map((word) => {\n const isCurrentlyActive =\n captionsLocalTimeSec >= word.start && captionsLocalTimeSec < word.end;\n\n return html`<div\n class=\"absolute border text-xs overflow-visible flex items-center ${isCurrentlyActive ? \"font-bold z-[5]\" : \"\"}\"\n style=${styleMap({\n left: `${this.pixelsPerMs * word.start * 1000}px`,\n width: `${this.pixelsPerMs * (word.end - word.start) * 1000}px`,\n height: \"100%\",\n top: \"0px\",\n backgroundColor: isCurrentlyActive\n ? \"var(--filmstrip-caption-bg)\"\n : \"var(--filmstrip-waveform-bg)\",\n borderColor: isCurrentlyActive\n ? \"var(--filmstrip-caption-border)\"\n : \"var(--filmstrip-waveform-border)\",\n })}\n title=\"Word: '${word.text}' (${word.start}s - ${word.end}s)\"\n >\n </div>`;\n })}\n </div>\n </div>`;\n }\n}\n\ndeclare global {\n interface HTMLElementTagNameMap {\n \"ef-captions-track\": EFCaptionsTrack;\n \"ef-captions-active-word-track\": EFCaptionsActiveWordTrack;\n \"ef-captions-segment-track\": EFCaptionsSegmentTrack;\n \"ef-captions-before-word-track\": EFCaptionsBeforeWordTrack;\n \"ef-captions-after-word-track\": EFCaptionsAfterWordTrack;\n }\n}\n"],"mappings":"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"}