@editframe/elements 0.49.6 → 0.50.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. package/dist/attachContextRoot.js +1 -1
  2. package/dist/attachContextRoot.js.map +1 -1
  3. package/dist/elements/EFCaptions.js +3 -3
  4. package/dist/elements/EFCaptions.js.map +1 -1
  5. package/dist/elements/EFTemporal.js +3 -1
  6. package/dist/elements/EFTemporal.js.map +1 -1
  7. package/dist/elements/EFText.js +1 -1
  8. package/dist/elements/EFTimegroup.d.ts +10 -0
  9. package/dist/elements/EFTimegroup.js +27 -2
  10. package/dist/elements/EFTimegroup.js.map +1 -1
  11. package/dist/elements/EFVideo.js +1 -1
  12. package/dist/gui/ContextMixin.js +1 -1
  13. package/dist/gui/EFControls.js +2 -2
  14. package/dist/gui/EFPause.js +2 -2
  15. package/dist/gui/EFPlay.js +2 -2
  16. package/dist/gui/EFScrubber.js +1 -1
  17. package/dist/gui/EFTimeDisplay.js +1 -1
  18. package/dist/gui/EFTimelineRuler.js +1 -1
  19. package/dist/gui/EFTogglePlay.js +2 -2
  20. package/dist/gui/EFWorkbench.js +1 -1
  21. package/dist/gui/PlaybackController.d.ts +1 -0
  22. package/dist/gui/PlaybackController.js +5 -4
  23. package/dist/gui/PlaybackController.js.map +1 -1
  24. package/dist/gui/TargetOrContextMixin.js +1 -1
  25. package/dist/gui/timeline/EFTimeline.d.ts +16 -11
  26. package/dist/gui/timeline/EFTimeline.js +163 -86
  27. package/dist/gui/timeline/EFTimeline.js.map +1 -1
  28. package/dist/gui/timeline/EFTimelineRow.js +9 -9
  29. package/dist/gui/timeline/tracks/CaptionsTrack.js +7 -6
  30. package/dist/gui/timeline/tracks/CaptionsTrack.js.map +1 -1
  31. package/dist/gui/timeline/tracks/EFThumbnailStrip.js +24 -10
  32. package/dist/gui/timeline/tracks/EFThumbnailStrip.js.map +1 -1
  33. package/dist/gui/timeline/tracks/TextTrack.js +26 -23
  34. package/dist/gui/timeline/tracks/TextTrack.js.map +1 -1
  35. package/dist/gui/timeline/tracks/TimegroupTrack.js +10 -21
  36. package/dist/gui/timeline/tracks/TimegroupTrack.js.map +1 -1
  37. package/dist/preview/renderTimegroupToCanvas.js +37 -5
  38. package/dist/preview/renderTimegroupToCanvas.js.map +1 -1
  39. package/dist/preview/rendering/renderToImageNative.js +19 -7
  40. package/dist/preview/rendering/renderToImageNative.js.map +1 -1
  41. package/dist/preview/rendering/serializeTimelineDirect.js +2 -0
  42. package/dist/preview/rendering/serializeTimelineDirect.js.map +1 -1
  43. package/dist/version.js +1 -1
  44. package/package.json +3 -3
  45. package/dist/node_modules/@lit/reactive-element/css-tag.js +0 -40
  46. package/dist/node_modules/@lit/reactive-element/css-tag.js.map +0 -1
  47. package/dist/node_modules/@lit/reactive-element/reactive-element.js +0 -234
  48. package/dist/node_modules/@lit/reactive-element/reactive-element.js.map +0 -1
@@ -69,6 +69,7 @@ declare class EFTimeline extends EFTimeline_base {
69
69
  * This should be set to the canvas element.
70
70
  */
71
71
  private targetElement;
72
+ private _highlightedElement;
72
73
  private currentTimeMs;
73
74
  private isPlaying;
74
75
  private isLooping;
@@ -81,6 +82,7 @@ declare class EFTimeline extends EFTimeline_base {
81
82
  private playheadRef;
82
83
  private playheadHandleRef;
83
84
  private frameHighlightRef;
85
+ private playheadLayerRef;
84
86
  private animationFrameId?;
85
87
  private selectionChangeHandler?;
86
88
  private scrollHandler?;
@@ -112,12 +114,13 @@ declare class EFTimeline extends EFTimeline_base {
112
114
  private getCanvas;
113
115
  private getCanvasSelectionContext;
114
116
  /**
115
- * Get the currently highlighted element from the canvas.
117
+ * Get the currently highlighted element.
118
+ * Local state is the source of truth; canvas is kept in sync as a side effect.
116
119
  */
117
120
  getHighlightedElement(): HTMLElement | null;
118
121
  /**
119
- * Set the highlighted element on the canvas.
120
- * Called when user hovers a row in the timeline.
122
+ * Set the highlighted element.
123
+ * Always updates local state (triggers re-render); also syncs to canvas when present.
121
124
  */
122
125
  setHighlightedElement(element: HTMLElement | null): void;
123
126
  get targetTemporal(): TemporalMixinInterface | null;
@@ -170,6 +173,12 @@ declare class EFTimeline extends EFTimeline_base {
170
173
  protected updated(changedProperties: PropertyValues): void;
171
174
  private setupSelectionListener;
172
175
  private removeSelectionListener;
176
+ /**
177
+ * Single source-of-truth for applying a scroll position change.
178
+ * Keeps ruler-outer, viewportScrollLeft, playhead, and clip-path all in sync
179
+ * immediately — without relying on the async scroll event.
180
+ */
181
+ private applyScrollLeft;
173
182
  private setupScrollListener;
174
183
  private removeScrollListener;
175
184
  private setupKeyboardListener;
@@ -251,14 +260,10 @@ declare class EFTimeline extends EFTimeline_base {
251
260
  private renderTimeDisplay;
252
261
  private renderZoomControls;
253
262
  /**
254
- * Calculate frame highlight bounds (semantics).
255
- * Returns null if frame markers aren't visible or duration is invalid.
256
- */
257
- private calculateFrameHighlightBounds;
258
- /**
259
- * Render frame highlight (mechanism).
260
- * Shows the current frame as a rectangle to indicate frames have duration.
261
- * Only rendered when frame markers are visible (zoom level high enough).
263
+ * Render the frame highlight placeholder.
264
+ * Position and visibility are controlled exclusively by updatePlayheadPositionDirect
265
+ * via the frameHighlightRef so that Lit re-renders never clobber the 60fps rAF
266
+ * position with the throttled this.currentTimeMs value.
262
267
  */
263
268
  private renderFrameHighlight;
264
269
  private renderControls;
@@ -1,8 +1,8 @@
1
1
  import { TWMixin } from "../TWMixin2.js";
2
+ import { __decorate } from "../../_virtual/_@oxc-project_runtime@0.122.0/helpers/decorate.js";
2
3
  import { currentTimeContext } from "../currentTimeContext.js";
3
4
  import { durationContext } from "../durationContext.js";
4
5
  import { loopContext, playingContext } from "../playingContext.js";
5
- import { __decorate } from "../../_virtual/_@oxc-project_runtime@0.122.0/helpers/decorate.js";
6
6
  import { isEFTemporal } from "../../elements/EFTemporal.js";
7
7
  import { targetTemporalContext } from "../ContextMixin.js";
8
8
  import { TargetController } from "../../elements/TargetController.js";
@@ -20,7 +20,6 @@ import { EFTimegroup } from "../../elements/EFTimegroup.js";
20
20
  import { consume, provide } from "@lit/context";
21
21
  import { LitElement, css, html, nothing } from "lit";
22
22
  import { customElement, eventOptions, property, state } from "lit/decorators.js";
23
- import { styleMap } from "lit/directives/style-map.js";
24
23
  import { createRef, ref } from "lit/directives/ref.js";
25
24
  import { repeat } from "lit/directives/repeat.js";
26
25
  //#region src/gui/timeline/EFTimeline.ts
@@ -46,6 +45,7 @@ let EFTimeline = class EFTimeline extends TWMixin(LitElement) {
46
45
  this.hide = "";
47
46
  this.show = "";
48
47
  this.targetElement = null;
48
+ this._highlightedElement = null;
49
49
  this.currentTimeMs = 0;
50
50
  this.isPlaying = false;
51
51
  this.isLooping = false;
@@ -67,6 +67,7 @@ let EFTimeline = class EFTimeline extends TWMixin(LitElement) {
67
67
  this.playheadRef = createRef();
68
68
  this.playheadHandleRef = createRef();
69
69
  this.frameHighlightRef = createRef();
70
+ this.playheadLayerRef = createRef();
70
71
  this.isDraggingPlayhead = false;
71
72
  this.cachedViewportWidth = 800;
72
73
  this.saveZoomScrollDebounceTimer = null;
@@ -83,13 +84,14 @@ let EFTimeline = class EFTimeline extends TWMixin(LitElement) {
83
84
  width: 100%;
84
85
  height: 100%;
85
86
  min-height: 100px;
87
+ user-select: none;
86
88
 
87
89
  /* Layout coordination via CSS custom properties */
88
90
  --timeline-hierarchy-width: var(--ef-hierarchy-width, 200px);
89
91
  --timeline-row-height: var(--ef-row-height, 24px);
90
92
  --timeline-track-height: var(--ef-track-height, 24px);
91
93
 
92
- /* Component tokens (reference globals from ef-theme.css) */
94
+ /* Component alias tokens resolved from ef-color-* regardless of theme */
93
95
  --timeline-bg: var(--ef-color-bg);
94
96
  --timeline-border: var(--ef-color-border);
95
97
  --timeline-header-bg: var(--ef-color-bg-panel);
@@ -98,6 +100,78 @@ let EFTimeline = class EFTimeline extends TWMixin(LitElement) {
98
100
  --timeline-track-bg: var(--ef-color-bg-inset);
99
101
  --timeline-track-hover: var(--ef-color-hover);
100
102
  --timeline-playhead: var(--ef-color-playhead);
103
+
104
+ /* Filmstrip alias tokens */
105
+ --filmstrip-bg: var(--ef-color-bg-inset);
106
+ --filmstrip-item-focused: rgba(229, 57, 53, 0.3);
107
+ --filmstrip-border: var(--ef-color-border);
108
+ --filmstrip-caption-bg: rgba(76, 175, 80, 0.15);
109
+ --filmstrip-caption-border: rgba(76, 175, 80, 0.5);
110
+ --filmstrip-segment-bg: rgba(76, 175, 80, 0.3);
111
+ --filmstrip-segment-border: rgba(76, 175, 80, 0.6);
112
+ --filmstrip-timegroup-focused: var(--ef-color-focused);
113
+ --filmstrip-animation-bg: rgba(229, 57, 53, 0.2);
114
+ --filmstrip-keyframe-bg: rgba(229, 57, 53, 0.4);
115
+ }
116
+
117
+ :host-context(.dark) {
118
+ --ef-color-bg: #0a0a0a;
119
+ --ef-color-bg-panel: #111111;
120
+ --ef-color-bg-elevated: #1a1a1a;
121
+ --ef-color-bg-inset: #050505;
122
+ --ef-color-border: rgba(255, 255, 255, 0.15);
123
+ --ef-color-border-subtle: rgba(255, 255, 255, 0.08);
124
+ --ef-color-text: #fafafa;
125
+ --ef-color-text-muted: #9ca3af;
126
+ --ef-color-text-subtle: #6b7280;
127
+ --ef-color-primary: #3b82f6;
128
+ --ef-color-primary-subtle: rgba(59, 130, 246, 0.15);
129
+ --ef-color-hover: rgba(255, 255, 255, 0.06);
130
+ --ef-color-selected: rgba(59, 130, 246, 0.2);
131
+ --ef-color-selected-subtle: rgba(59, 130, 246, 0.1);
132
+ --ef-color-focused: rgba(59, 130, 246, 0.25);
133
+ --ef-color-success: #059669;
134
+ --ef-color-error: #dc2626;
135
+ --ef-color-playhead: #dc2626;
136
+ --ef-color-type-video: #3b82f6;
137
+ --ef-color-type-audio: #10b981;
138
+ --ef-color-type-image: #8b5cf6;
139
+ --ef-color-type-text: #f59e0b;
140
+ --ef-color-type-captions: #14b8a6;
141
+ --ef-color-type-timegroup: #64748b;
142
+ --filmstrip-item-bg: rgba(17, 17, 17, 0.9);
143
+ --filmstrip-waveform-bg: rgba(0, 0, 0, 0.4);
144
+ --filmstrip-waveform-border: rgba(255, 255, 255, 0.15);
145
+ }
146
+
147
+ :host-context(.light) {
148
+ --ef-color-bg: #ffffff;
149
+ --ef-color-bg-panel: #f8f8f8;
150
+ --ef-color-bg-elevated: #ffffff;
151
+ --ef-color-bg-inset: #eeeeee;
152
+ --ef-color-border: rgba(0, 0, 0, 0.20);
153
+ --ef-color-border-subtle: rgba(0, 0, 0, 0.10);
154
+ --ef-color-text: #0a0a0a;
155
+ --ef-color-text-muted: #404040;
156
+ --ef-color-text-subtle: #666666;
157
+ --ef-color-primary: #2563eb;
158
+ --ef-color-primary-subtle: rgba(37, 99, 235, 0.12);
159
+ --ef-color-hover: rgba(0, 0, 0, 0.06);
160
+ --ef-color-selected: rgba(37, 99, 235, 0.18);
161
+ --ef-color-selected-subtle: rgba(37, 99, 235, 0.10);
162
+ --ef-color-focused: rgba(37, 99, 235, 0.25);
163
+ --ef-color-success: #059669;
164
+ --ef-color-error: #dc2626;
165
+ --ef-color-playhead: #dc2626;
166
+ --ef-color-type-video: #2563eb;
167
+ --ef-color-type-audio: #059669;
168
+ --ef-color-type-image: #7c3aed;
169
+ --ef-color-type-text: #ea580c;
170
+ --ef-color-type-captions: #0891b2;
171
+ --ef-color-type-timegroup: #475569;
172
+ --filmstrip-item-bg: rgba(238, 238, 238, 0.95);
173
+ --filmstrip-waveform-bg: rgba(0, 0, 0, 0.06);
174
+ --filmstrip-waveform-border: rgba(0, 0, 0, 0.12);
101
175
  }
102
176
 
103
177
  .timeline-container {
@@ -140,8 +214,8 @@ let EFTimeline = class EFTimeline extends TWMixin(LitElement) {
140
214
  min-width: 32px;
141
215
  height: 32px;
142
216
  padding: 6px 10px;
143
- background: var(--ef-color-bg-inset);
144
- border: 1px solid var(--ef-color-border-subtle);
217
+ background: var(--ef-color-bg-inset, #050505);
218
+ border: 1px solid var(--ef-color-border-subtle, rgba(255, 255, 255, 0.08));
145
219
  border-radius: 6px;
146
220
  color: inherit;
147
221
  font-size: 14px;
@@ -153,8 +227,8 @@ let EFTimeline = class EFTimeline extends TWMixin(LitElement) {
153
227
  }
154
228
 
155
229
  .control-btn:hover:not(:disabled) {
156
- background: var(--ef-color-hover);
157
- border-color: var(--ef-color-border);
230
+ background: var(--ef-color-hover, rgba(255, 255, 255, 0.06));
231
+ border-color: var(--ef-color-border, rgba(255, 255, 255, 0.15));
158
232
  }
159
233
 
160
234
  .control-btn:disabled {
@@ -163,8 +237,8 @@ let EFTimeline = class EFTimeline extends TWMixin(LitElement) {
163
237
  }
164
238
 
165
239
  .control-btn.active {
166
- background: var(--ef-color-primary-subtle);
167
- border-color: var(--ef-color-primary);
240
+ background: var(--ef-color-primary-subtle, rgba(59, 130, 246, 0.15));
241
+ border-color: var(--ef-color-primary, #3b82f6);
168
242
  }
169
243
 
170
244
  .time-display {
@@ -172,7 +246,7 @@ let EFTimeline = class EFTimeline extends TWMixin(LitElement) {
172
246
  font-size: 13px;
173
247
  font-weight: 500;
174
248
  padding: 6px 12px;
175
- background: var(--ef-color-bg-elevated);
249
+ background: var(--ef-color-bg-elevated, #1a1a1a);
176
250
  border-radius: 6px;
177
251
  letter-spacing: 0.5px;
178
252
  }
@@ -189,8 +263,8 @@ let EFTimeline = class EFTimeline extends TWMixin(LitElement) {
189
263
  display: flex;
190
264
  align-items: center;
191
265
  justify-content: center;
192
- background: var(--ef-color-bg-inset);
193
- border: 1px solid var(--ef-color-border-subtle);
266
+ background: var(--ef-color-bg-inset, #050505);
267
+ border: 1px solid var(--ef-color-border-subtle, rgba(255, 255, 255, 0.08));
194
268
  border-radius: 6px;
195
269
  color: inherit;
196
270
  font-size: 18px;
@@ -200,8 +274,8 @@ let EFTimeline = class EFTimeline extends TWMixin(LitElement) {
200
274
  }
201
275
 
202
276
  .zoom-btn:hover {
203
- background: var(--ef-color-hover);
204
- border-color: var(--ef-color-border);
277
+ background: var(--ef-color-hover, rgba(255, 255, 255, 0.06));
278
+ border-color: var(--ef-color-border, rgba(255, 255, 255, 0.15));
205
279
  transform: scale(1.05);
206
280
  }
207
281
 
@@ -220,8 +294,8 @@ let EFTimeline = class EFTimeline extends TWMixin(LitElement) {
220
294
  }
221
295
 
222
296
  .zoom-label:hover {
223
- background: var(--ef-color-hover);
224
- border-color: var(--ef-color-border-subtle);
297
+ background: var(--ef-color-hover, rgba(255, 255, 255, 0.06));
298
+ border-color: var(--ef-color-border-subtle, rgba(255, 255, 255, 0.08));
225
299
  }
226
300
 
227
301
  .zoom-btn:disabled {
@@ -239,13 +313,15 @@ let EFTimeline = class EFTimeline extends TWMixin(LitElement) {
239
313
  overflow: hidden;
240
314
  }
241
315
 
316
+ /* Clips the ruler row during horizontal scroll — scrollLeft synced via JS */
242
317
  .ruler-row {
243
318
  display: flex;
244
319
  height: 24px;
245
320
  background: var(--timeline-ruler-bg);
246
321
  border-bottom: 1px solid var(--timeline-border);
247
322
  flex-shrink: 0;
248
- /* Sticky positioning for native scroll sync */
323
+ /* Sticky inside tracks-scroll so the ruler stays visible during vertical scroll.
324
+ The ruler scrolls horizontally with tracks-scroll natively — no sync needed. */
249
325
  position: sticky;
250
326
  top: 0;
251
327
  z-index: 10;
@@ -348,6 +424,20 @@ let EFTimeline = class EFTimeline extends TWMixin(LitElement) {
348
424
  display: flex;
349
425
  flex-direction: column;
350
426
  }
427
+
428
+ /* Fills the label column background for the full tracks height, covering
429
+ empty space below the last row. Sits in the same grid cell as the rows
430
+ layer but at z-index 0 so actual row labels (z-index: 8) paint above it. */
431
+ .label-column-bg {
432
+ align-self: stretch;
433
+ position: sticky;
434
+ left: 0;
435
+ width: var(--timeline-hierarchy-width, 200px);
436
+ background: var(--ef-color-bg-panel);
437
+ border-right: 1px solid var(--ef-color-border-subtle);
438
+ z-index: 0;
439
+ pointer-events: none;
440
+ }
351
441
 
352
442
  /* === PLAYHEAD (sticky layer that stays visible during vertical scroll) === */
353
443
  .playhead-layer {
@@ -358,6 +448,8 @@ let EFTimeline = class EFTimeline extends TWMixin(LitElement) {
358
448
  /* Below sticky labels (z-index 10-11) but above tracks */
359
449
  z-index: 5;
360
450
  overflow: hidden;
451
+ /* clip-path is set dynamically in updatePlayheadPositionDirect so it
452
+ tracks horizontal scroll and always aligns with the label column edge. */
361
453
  }
362
454
 
363
455
  .playhead {
@@ -385,8 +477,8 @@ let EFTimeline = class EFTimeline extends TWMixin(LitElement) {
385
477
  position: absolute;
386
478
  top: 0;
387
479
  bottom: 0;
388
- background: var(--ef-color-primary-subtle);
389
- border-left: 2px solid var(--ef-color-primary);
480
+ background: var(--ef-color-primary-subtle, rgba(59, 130, 246, 0.15));
481
+ border-left: 2px solid var(--ef-color-primary, #3b82f6);
390
482
  pointer-events: none;
391
483
  }
392
484
 
@@ -395,7 +487,7 @@ let EFTimeline = class EFTimeline extends TWMixin(LitElement) {
395
487
  align-items: center;
396
488
  justify-content: center;
397
489
  height: 100%;
398
- color: var(--ef-color-text-subtle);
490
+ color: var(--ef-color-text-subtle, #6b7280);
399
491
  font-style: italic;
400
492
  }
401
493
  `];
@@ -475,16 +567,18 @@ let EFTimeline = class EFTimeline extends TWMixin(LitElement) {
475
567
  return this.getCanvas()?.selectionContext;
476
568
  }
477
569
  /**
478
- * Get the currently highlighted element from the canvas.
570
+ * Get the currently highlighted element.
571
+ * Local state is the source of truth; canvas is kept in sync as a side effect.
479
572
  */
480
573
  getHighlightedElement() {
481
- return this.getCanvas()?.highlightedElement ?? null;
574
+ return this._highlightedElement;
482
575
  }
483
576
  /**
484
- * Set the highlighted element on the canvas.
485
- * Called when user hovers a row in the timeline.
577
+ * Set the highlighted element.
578
+ * Always updates local state (triggers re-render); also syncs to canvas when present.
486
579
  */
487
580
  setHighlightedElement(element) {
581
+ this._highlightedElement = element;
488
582
  this.getCanvas()?.setHighlightedElement(element);
489
583
  }
490
584
  get targetTemporal() {
@@ -581,11 +675,7 @@ let EFTimeline = class EFTimeline extends TWMixin(LitElement) {
581
675
  if (typeof state.viewportScrollLeft === "number" && state.viewportScrollLeft >= 0) {
582
676
  const scrollLeft = state.viewportScrollLeft;
583
677
  this.updateComplete.then(() => {
584
- const tracksScroll = this.tracksScrollRef.value;
585
- if (tracksScroll) {
586
- tracksScroll.scrollLeft = scrollLeft;
587
- this.viewportScrollLeft = scrollLeft;
588
- }
678
+ if (this.tracksScrollRef.value) this.applyScrollLeft(scrollLeft);
589
679
  });
590
680
  }
591
681
  } catch (error) {
@@ -783,6 +873,23 @@ let EFTimeline = class EFTimeline extends TWMixin(LitElement) {
783
873
  this.canvasActiveRootTemporalChangeHandler = void 0;
784
874
  }
785
875
  }
876
+ /**
877
+ * Single source-of-truth for applying a scroll position change.
878
+ * Keeps ruler-outer, viewportScrollLeft, playhead, and clip-path all in sync
879
+ * immediately — without relying on the async scroll event.
880
+ */
881
+ applyScrollLeft(scrollLeft) {
882
+ const tracksScroll = this.tracksScrollRef.value;
883
+ if (!tracksScroll) return;
884
+ tracksScroll.scrollLeft = scrollLeft;
885
+ const actual = tracksScroll.scrollLeft;
886
+ if (actual !== this.viewportScrollLeft) {
887
+ this.viewportScrollLeft = actual;
888
+ this.updatePlayheadPositionDirect(this.currentTimeMs);
889
+ this.updateTimelineState();
890
+ this.debouncedSaveTimelineState();
891
+ }
892
+ }
786
893
  setupScrollListener() {
787
894
  if (this.tracksScrollRef.value) {
788
895
  this.scrollHandler = () => {
@@ -790,6 +897,7 @@ let EFTimeline = class EFTimeline extends TWMixin(LitElement) {
790
897
  const newScrollLeft = this.tracksScrollRef.value.scrollLeft;
791
898
  if (newScrollLeft !== this.viewportScrollLeft) {
792
899
  this.viewportScrollLeft = newScrollLeft;
900
+ this.updatePlayheadPositionDirect(this.currentTimeMs);
793
901
  this.updateTimelineState();
794
902
  this.debouncedSaveTimelineState();
795
903
  }
@@ -827,6 +935,8 @@ let EFTimeline = class EFTimeline extends TWMixin(LitElement) {
827
935
  const rawTime = this.targetTemporal.currentTimeMs ?? 0;
828
936
  const duration = this.targetTemporal.durationMs ?? 0;
829
937
  const newTimeMs = Math.max(0, Math.min(rawTime, duration));
938
+ if (this.isPlaying && !this.isDraggingPlayhead) this.followPlayhead(newTimeMs);
939
+ else this.isFollowingPlayhead = false;
830
940
  this.updatePlayheadPositionDirect(newTimeMs);
831
941
  const newIsPlaying = this.targetTemporal.playing ?? false;
832
942
  const newIsLooping = this.targetTemporal.loop ?? false;
@@ -843,8 +953,6 @@ let EFTimeline = class EFTimeline extends TWMixin(LitElement) {
843
953
  this.currentTimeMs = newTimeMs;
844
954
  this.lastContextUpdateTime = now;
845
955
  }
846
- if (this.isPlaying && !this.isDraggingPlayhead) this.followPlayhead(newTimeMs);
847
- else this.isFollowingPlayhead = false;
848
956
  }
849
957
  } else if (this.isPlaying) {
850
958
  console.warn("[EFTimeline] Lost targetTemporal during playback. Stopping playback state.", "controlTarget:", this.controlTarget, "target:", this.target);
@@ -864,6 +972,9 @@ let EFTimeline = class EFTimeline extends TWMixin(LitElement) {
864
972
  const hierarchyWidth = this.showHierarchy ? _EFTimeline.HIERARCHY_WIDTH : 0;
865
973
  const playheadPx = timeToPx(timeMs, this.pixelsPerMs);
866
974
  const playheadLeft = hierarchyWidth + playheadPx;
975
+ const scrollLeft = this.viewportScrollLeft;
976
+ const playheadLayer = this.playheadLayerRef.value;
977
+ if (playheadLayer) playheadLayer.style.clipPath = hierarchyWidth > 0 ? `inset(0 0 0 ${hierarchyWidth + scrollLeft}px)` : "";
867
978
  const playhead = this.playheadRef.value;
868
979
  if (playhead) playhead.style.left = `${playheadLeft - 1}px`;
869
980
  const handle = this.playheadHandleRef.value;
@@ -985,6 +1096,7 @@ let EFTimeline = class EFTimeline extends TWMixin(LitElement) {
985
1096
  return;
986
1097
  }
987
1098
  this.targetTemporal.play();
1099
+ this.isPlaying = true;
988
1100
  }
989
1101
  handlePause() {
990
1102
  if (!this.targetTemporal) {
@@ -992,6 +1104,7 @@ let EFTimeline = class EFTimeline extends TWMixin(LitElement) {
992
1104
  return;
993
1105
  }
994
1106
  this.targetTemporal.pause();
1107
+ this.isPlaying = false;
995
1108
  }
996
1109
  handleToggleLoop() {
997
1110
  if (!this.targetTemporal) {
@@ -1024,22 +1137,12 @@ let EFTimeline = class EFTimeline extends TWMixin(LitElement) {
1024
1137
  handleZoomOut() {
1025
1138
  const newPixelsPerMs = Math.max(this.effectiveMinPixelsPerMs, this.pixelsPerMs / 1.25);
1026
1139
  this.pixelsPerMs = newPixelsPerMs;
1027
- if (newPixelsPerMs <= this.fitPixelsPerMs + 1e-9) {
1028
- const tracksScroll = this.tracksScrollRef.value;
1029
- if (tracksScroll) {
1030
- tracksScroll.scrollLeft = 0;
1031
- this.viewportScrollLeft = 0;
1032
- }
1033
- }
1140
+ if (newPixelsPerMs <= this.fitPixelsPerMs + 1e-9) this.applyScrollLeft(0);
1034
1141
  this.updateTimelineState();
1035
1142
  }
1036
1143
  handleFitToViewport() {
1037
1144
  this.pixelsPerMs = this.fitPixelsPerMs;
1038
- const tracksScroll = this.tracksScrollRef.value;
1039
- if (tracksScroll) {
1040
- tracksScroll.scrollLeft = 0;
1041
- this.viewportScrollLeft = 0;
1042
- }
1145
+ this.applyScrollLeft(0);
1043
1146
  this.updateTimelineState();
1044
1147
  }
1045
1148
  /**
@@ -1063,8 +1166,7 @@ let EFTimeline = class EFTimeline extends TWMixin(LitElement) {
1063
1166
  const newScrollLeft = timeToPx(timeAtCursor, newPixelsPerMs) - cursorXInViewport;
1064
1167
  this.pixelsPerMs = newPixelsPerMs;
1065
1168
  const maxScroll = Math.max(0, timeToPx(this.durationMs, newPixelsPerMs) - (rect.width - hierarchyWidth));
1066
- tracksScroll.scrollLeft = Math.max(0, Math.min(maxScroll, newScrollLeft));
1067
- this.viewportScrollLeft = tracksScroll.scrollLeft;
1169
+ this.applyScrollLeft(Math.max(0, Math.min(maxScroll, newScrollLeft)));
1068
1170
  this.updateTimelineState();
1069
1171
  }
1070
1172
  /**
@@ -1152,7 +1254,10 @@ let EFTimeline = class EFTimeline extends TWMixin(LitElement) {
1152
1254
  startTimeMs: this.currentTimeMs
1153
1255
  });
1154
1256
  const tracksScroll = this.tracksScrollRef.value;
1155
- if (tracksScroll) this.viewportScrollLeft = tracksScroll.scrollLeft;
1257
+ if (tracksScroll) {
1258
+ this.viewportScrollLeft = tracksScroll.scrollLeft;
1259
+ this.updatePlayheadPositionDirect(this.currentTimeMs);
1260
+ }
1156
1261
  const hierarchyWidth = this.showHierarchy ? _EFTimeline.HIERARCHY_WIDTH : 0;
1157
1262
  let lastClientX = e.clientX;
1158
1263
  let edgeScrollAnimationId = null;
@@ -1186,8 +1291,7 @@ let EFTimeline = class EFTimeline extends TWMixin(LitElement) {
1186
1291
  if (scrollDelta !== 0) {
1187
1292
  const maxScroll = tracksScroll.scrollWidth - tracksScroll.clientWidth;
1188
1293
  const newScrollLeft = Math.max(0, Math.min(maxScroll, tracksScroll.scrollLeft + scrollDelta));
1189
- tracksScroll.scrollLeft = newScrollLeft;
1190
- this.viewportScrollLeft = newScrollLeft;
1294
+ this.applyScrollLeft(newScrollLeft);
1191
1295
  updatePlayheadFromMouse();
1192
1296
  }
1193
1297
  edgeScrollAnimationId = requestAnimationFrame(edgeScrollLoop);
@@ -1254,43 +1358,13 @@ let EFTimeline = class EFTimeline extends TWMixin(LitElement) {
1254
1358
  `;
1255
1359
  }
1256
1360
  /**
1257
- * Calculate frame highlight bounds (semantics).
1258
- * Returns null if frame markers aren't visible or duration is invalid.
1259
- */
1260
- calculateFrameHighlightBounds() {
1261
- if (!this.showFrameMarkers || this.durationMs <= 0) return null;
1262
- const fps = this.fps;
1263
- const frameDurationMs = 1e3 / fps;
1264
- const frameStartMs = quantizeToFrameTimeMs(this.currentTimeMs, fps);
1265
- const frameEndMs = Math.min(frameStartMs + frameDurationMs, this.durationMs);
1266
- const startPx = timeToPx(frameStartMs, this.pixelsPerMs);
1267
- const widthPx = timeToPx(frameEndMs, this.pixelsPerMs) - startPx;
1268
- if (widthPx <= 0 || startPx < 0) return null;
1269
- return {
1270
- startPx,
1271
- widthPx
1272
- };
1273
- }
1274
- /**
1275
- * Render frame highlight (mechanism).
1276
- * Shows the current frame as a rectangle to indicate frames have duration.
1277
- * Only rendered when frame markers are visible (zoom level high enough).
1361
+ * Render the frame highlight placeholder.
1362
+ * Position and visibility are controlled exclusively by updatePlayheadPositionDirect
1363
+ * via the frameHighlightRef so that Lit re-renders never clobber the 60fps rAF
1364
+ * position with the throttled this.currentTimeMs value.
1278
1365
  */
1279
1366
  renderFrameHighlight() {
1280
- if (!this.showFrameMarkers) return nothing;
1281
- const bounds = this.calculateFrameHighlightBounds();
1282
- if (!bounds) return nothing;
1283
- const hierarchyWidth = this.showHierarchy ? _EFTimeline.HIERARCHY_WIDTH : 0;
1284
- return html`
1285
- <div
1286
- ${ref(this.frameHighlightRef)}
1287
- class="frame-highlight"
1288
- style=${styleMap({
1289
- left: `${hierarchyWidth + bounds.startPx}px`,
1290
- width: `${bounds.widthPx}px`
1291
- })}
1292
- ></div>
1293
- `;
1367
+ return html`<div ${ref(this.frameHighlightRef)} class="frame-highlight" style="display:none"></div>`;
1294
1368
  }
1295
1369
  renderControls() {
1296
1370
  if (!this.showControls) return nothing;
@@ -1375,14 +1449,14 @@ let EFTimeline = class EFTimeline extends TWMixin(LitElement) {
1375
1449
  <div class="timeline-container" tabindex="0" ${ref(this.containerRef)}>
1376
1450
  ${this.renderControls()}
1377
1451
  <div class="timeline-area">
1378
- <!-- Tracks Viewport - Single scrollable container -->
1452
+ <!-- Tracks Viewport single scroll container for ruler + tracks -->
1379
1453
  <div class="tracks-viewport" part="tracks">
1380
1454
  <div
1381
1455
  class="tracks-scroll"
1382
1456
  ${ref(this.tracksScrollRef)}
1383
1457
  @pointerdown=${this.handleTracksPointerDown}
1384
1458
  >
1385
- <!-- Ruler Row - Inside scroll container with sticky positioning -->
1459
+ <!-- Ruler Row sticky inside tracks-scroll, scrolls horizontally with it natively -->
1386
1460
  ${this.showRuler ? html`
1387
1461
  <div class="ruler-row" style="width: ${this.contentWidthPx + hierarchyWidth}px;">
1388
1462
  ${this.showHierarchy ? html`<div class="ruler-spacer"></div>` : nothing}
@@ -1408,13 +1482,15 @@ let EFTimeline = class EFTimeline extends TWMixin(LitElement) {
1408
1482
  </div>
1409
1483
  ` : nothing}
1410
1484
  <div class="tracks-content" style="min-width: ${this.contentWidthPx + hierarchyWidth}px;">
1485
+ <!-- Label column background — fills full height below the last row -->
1486
+ ${hierarchyWidth > 0 ? html`<div class="label-column-bg"></div>` : nothing}
1411
1487
  <!-- Track rows layer -->
1412
1488
  <div class="tracks-rows-layer">
1413
1489
  ${this.renderRows(target)}
1414
1490
  </div>
1415
1491
 
1416
1492
  <!-- Playhead layer - sticky to stay visible during vertical scroll -->
1417
- <div class="playhead-layer">
1493
+ <div ${ref(this.playheadLayerRef)} class="playhead-layer">
1418
1494
  ${this.renderFrameHighlight()}
1419
1495
  ${this.showPlayhead ? html`
1420
1496
  <div ${ref(this.playheadRef)} class="playhead" part="playhead" style="left: ${playheadLeft - 1}px;">
@@ -1478,6 +1554,7 @@ __decorate([property({
1478
1554
  __decorate([property({ type: String })], EFTimeline.prototype, "hide", void 0);
1479
1555
  __decorate([property({ type: String })], EFTimeline.prototype, "show", void 0);
1480
1556
  __decorate([state()], EFTimeline.prototype, "targetElement", void 0);
1557
+ __decorate([state()], EFTimeline.prototype, "_highlightedElement", void 0);
1481
1558
  __decorate([state()], EFTimeline.prototype, "currentTimeMs", void 0);
1482
1559
  __decorate([state()], EFTimeline.prototype, "isPlaying", void 0);
1483
1560
  __decorate([state()], EFTimeline.prototype, "isLooping", void 0);