@kteneyck/cesium-timeline-angular 0.8.0 → 0.10.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.
@@ -1,7 +1,7 @@
1
1
  import * as i0 from '@angular/core';
2
2
  import { EventEmitter, ViewChild, Output, Input, ChangeDetectionStrategy, Component } from '@angular/core';
3
3
  import * as Cesium from 'cesium';
4
- import { splitForDisplay, formatDateTime, getTimezoneAbbr, DEFAULT_LABELS, resolveLabel, MIN_SPAN_MS, MAX_SPAN_MS, drawTimeline, hitTestSwimLane, hitTestLaneLabel, isInSwimLaneRegion, zoomRange, DEFAULT_LANE_HEIGHT, LANE_GAP, totalSwimLaneHeight, TICK_AREA_HEIGHT, SWIM_LANE_SCROLL_SPEED, zoomAroundMs, defaultTheme, toJulianDate } from '@kteneyck/cesium-timeline-core';
4
+ import { splitForDisplay, formatDateTime, getTimezoneAbbr, DEFAULT_LABELS, resolveLabel, MIN_SPAN_MS, MAX_SPAN_MS, drawTimeline, hitTestSwimLane, hitTestLaneLabel, isInSwimLaneRegion, zoomRange, TICK_AREA_HEIGHT, DEFAULT_LANE_HEIGHT, LANE_GAP, totalSwimLaneHeight, SWIM_LANE_SCROLL_SPEED, zoomAroundMs, defaultTheme, toJulianDate } from '@kteneyck/cesium-timeline-core';
5
5
  export * from '@kteneyck/cesium-timeline-core';
6
6
  export { TICK_AREA_HEIGHT } from '@kteneyck/cesium-timeline-core';
7
7
 
@@ -19,6 +19,10 @@ class TimelineControlsComponent {
19
19
  theme;
20
20
  swimLanesVisible;
21
21
  labels;
22
+ liveButtonSize = 'md';
23
+ liveButtonPosition = 'left';
24
+ /** @see TimelineBaseProps.live */
25
+ live = false;
22
26
  dateTimeClick = new EventEmitter();
23
27
  playPause = new EventEmitter();
24
28
  jumpToStart = new EventEmitter();
@@ -36,6 +40,12 @@ class TimelineControlsComponent {
36
40
  get isNormalSpeed() { return this.multiplier === 1; }
37
41
  get absMultiplier() { return Math.abs(this.multiplier); }
38
42
  get hasSwimLaneToggle() { return this.swimLanesVisible != null; }
43
+ static LIVE_SIZE_MAP = {
44
+ sm: { width: 44, height: 18, fontSize: '10px', dot: 5, borderRadius: '3px' },
45
+ md: { width: 56, height: 22, fontSize: '11px', dot: 6, borderRadius: '3px' },
46
+ lg: { width: 72, height: 30, fontSize: '13px', dot: 8, borderRadius: '4px' },
47
+ };
48
+ get liveSize() { return TimelineControlsComponent.LIVE_SIZE_MAP[this.liveButtonSize]; }
39
49
  get timeFormat() { return splitForDisplay(this.dateTimeFormat).timeFormat; }
40
50
  get dateFormat() { return splitForDisplay(this.dateTimeFormat).dateFormat; }
41
51
  get formattedTime() { return formatDateTime(this.currentTime, this.timeFormat, this.timezone); }
@@ -58,7 +68,7 @@ class TimelineControlsComponent {
58
68
  this.ro?.disconnect();
59
69
  }
60
70
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.10", ngImport: i0, type: TimelineControlsComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
61
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.10", type: TimelineControlsComponent, isStandalone: true, selector: "ct-timeline-controls", inputs: { currentTime: "currentTime", isPlaying: "isPlaying", multiplier: "multiplier", dateTimeFormat: "dateTimeFormat", timezone: "timezone", isLive: "isLive", hasStartTime: "hasStartTime", hasEndTime: "hasEndTime", showJumpToStart: "showJumpToStart", showJumpToEnd: "showJumpToEnd", theme: "theme", swimLanesVisible: "swimLanesVisible", labels: "labels" }, outputs: { dateTimeClick: "dateTimeClick", playPause: "playPause", jumpToStart: "jumpToStart", rewind: "rewind", fastForward: "fastForward", jumpToEnd: "jumpToEnd", jumpToLive: "jumpToLive", resetSpeed: "resetSpeed", toggleSwimLanes: "toggleSwimLanes" }, viewQueries: [{ propertyName: "containerRef", first: true, predicate: ["container"], descendants: true }], ngImport: i0, template: `
71
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.10", type: TimelineControlsComponent, isStandalone: true, selector: "ct-timeline-controls", inputs: { currentTime: "currentTime", isPlaying: "isPlaying", multiplier: "multiplier", dateTimeFormat: "dateTimeFormat", timezone: "timezone", isLive: "isLive", hasStartTime: "hasStartTime", hasEndTime: "hasEndTime", showJumpToStart: "showJumpToStart", showJumpToEnd: "showJumpToEnd", theme: "theme", swimLanesVisible: "swimLanesVisible", labels: "labels", liveButtonSize: "liveButtonSize", liveButtonPosition: "liveButtonPosition", live: "live" }, outputs: { dateTimeClick: "dateTimeClick", playPause: "playPause", jumpToStart: "jumpToStart", rewind: "rewind", fastForward: "fastForward", jumpToEnd: "jumpToEnd", jumpToLive: "jumpToLive", resetSpeed: "resetSpeed", toggleSwimLanes: "toggleSwimLanes" }, viewQueries: [{ propertyName: "containerRef", first: true, predicate: ["container"], descendants: true }], ngImport: i0, template: `
62
72
  <div
63
73
  #container
64
74
  [style.display]="isNarrow ? 'flex' : 'grid'"
@@ -69,14 +79,14 @@ class TimelineControlsComponent {
69
79
  [style.border-bottom]="'1px solid ' + theme.controlBarBorder"
70
80
  [style.font-family]="'system-ui, -apple-system, sans-serif'"
71
81
  >
72
- <!-- Left: Datetime + LIVE + speed badge -->
82
+ <!-- Left: Datetime + LIVE (if position=left) -->
73
83
  <div style="display:flex;align-items:center;gap:8px;flex-shrink:0">
74
84
  <div
75
- (click)="dateTimeClick.emit()"
76
- [title]="dateTimeClick.observed ? l.dateTimeClickTooltip : ''"
85
+ (click)="!live && dateTimeClick.emit()"
86
+ [title]="(!live && dateTimeClick.observed) ? l.dateTimeClickTooltip : ''"
77
87
  [style.color]="theme.labelColor"
78
88
  style="font-family:monospace;line-height:1.15;border-radius:4px;padding:2px 4px;transition:background 0.15s"
79
- [style.cursor]="dateTimeClick.observed ? 'pointer' : 'default'"
89
+ [style.cursor]="(!live && dateTimeClick.observed) ? 'pointer' : 'default'"
80
90
  >
81
91
  @if (timeFormat) {
82
92
  <div style="font-size:2em;font-weight:bold;letter-spacing:0.02em">
@@ -98,35 +108,47 @@ class TimelineControlsComponent {
98
108
  }
99
109
  </div>
100
110
 
101
- <div style="display:flex;flex-direction:column;gap:2px;justify-content:center">
102
- <!-- LIVE button -->
103
- <button
104
- (click)="jumpToLive.emit()"
105
- [style.color]="isLive ? theme.controlBarBackground : theme.buttonActiveColor"
106
- [style.background-color]="isLive ? theme.buttonActiveColor : 'transparent'"
107
- [style.border-color]="theme.buttonActiveColor"
108
- [style.opacity]="isLive ? 1 : 0.55"
109
- style="background:none;border:1px solid;cursor:pointer;font-size:11px;font-weight:bold;letter-spacing:0.05em;width:52px;min-width:52px;height:20px;border-radius:3px;display:flex;align-items:center;justify-content:center;padding:0;font-family:system-ui,-apple-system,sans-serif;transition:opacity 0.15s"
110
- [title]="isLive ? l.liveActiveTooltip : l.liveTooltip"
111
- >
112
- {{ isLive ? l.liveActiveLabel : l.liveLabel }}
113
- </button>
114
-
115
- <!-- Speed badge -->
116
- <div style="height:20px;display:flex;align-items:center">
117
- @if (!isNormalSpeed) {
111
+ @if (liveButtonPosition === 'left') {
112
+ <div style="display:flex;align-items:center;gap:4px">
113
+ <button
114
+ (click)="!live && jumpToLive.emit()"
115
+ [style.color]="(live || isLive) ? theme.controlBarBackground : theme.buttonActiveColor"
116
+ [style.background-color]="(live || isLive) ? theme.buttonActiveColor : 'transparent'"
117
+ [style.border-color]="theme.buttonActiveColor"
118
+ [style.opacity]="1"
119
+ [style.width.px]="liveSize.width"
120
+ [style.min-width.px]="liveSize.width"
121
+ [style.height.px]="liveSize.height"
122
+ [style.font-size]="liveSize.fontSize"
123
+ [style.border-radius]="liveSize.borderRadius"
124
+ [style.cursor]="live ? 'default' : 'pointer'"
125
+ style="background:none;border:1px solid;font-weight:bold;letter-spacing:0.05em;display:flex;align-items:center;justify-content:center;padding:0;gap:4px;font-family:system-ui,-apple-system,sans-serif;transition:opacity 0.15s"
126
+ [title]="(live || isLive) ? l.liveActiveTooltip : l.liveTooltip"
127
+ >
128
+ @if (live || isLive) {
129
+ <span
130
+ [style.width.px]="liveSize.dot"
131
+ [style.height.px]="liveSize.dot"
132
+ [style.background-color]="theme.liveDotColor"
133
+ style="border-radius:50%;display:inline-block;flex-shrink:0"
134
+ ></span>
135
+ }
136
+ {{ (live || isLive) ? l.liveActiveLabel : l.liveLabel }}
137
+ </button>
138
+ @if (!isNormalSpeed && !live) {
118
139
  <button
119
140
  (click)="resetSpeed.emit()"
120
141
  [style.color]="theme.buttonActiveColor"
121
142
  [style.border-color]="theme.buttonActiveColor + '44'"
122
- style="background:none;border:1px solid;cursor:pointer;font-size:11px;width:52px;min-width:52px;height:20px;border-radius:4px;display:flex;align-items:center;justify-content:center;padding:0;font-family:system-ui,-apple-system,sans-serif;transition:background-color 0.15s"
143
+ [style.width.px]="liveSize.width"
144
+ [style.min-width.px]="liveSize.width"
145
+ [style.height.px]="liveSize.height"
146
+ style="background:none;border:1px solid;cursor:pointer;font-size:11px;border-radius:4px;display:flex;align-items:center;justify-content:center;padding:0;font-family:system-ui,-apple-system,sans-serif;transition:background-color 0.15s"
123
147
  [title]="l.resetSpeedTooltip"
124
- >
125
- {{ isRewinding ? '◀ ' + absMultiplier + '×' : absMultiplier + '× ▶' }}
126
- </button>
148
+ >{{ isRewinding ? '◀ ' + absMultiplier + '×' : absMultiplier + '× ▶' }}</button>
127
149
  }
128
150
  </div>
129
- </div>
151
+ }
130
152
  </div>
131
153
 
132
154
  <!-- Center: Transport buttons -->
@@ -135,7 +157,7 @@ class TimelineControlsComponent {
135
157
  [style.flex]="isNarrow ? '1' : undefined"
136
158
  [style.justify-content]="isNarrow ? 'center' : undefined"
137
159
  >
138
- @if (showJumpToStart !== false) {
160
+ @if (!live && showJumpToStart !== false) {
139
161
  <button
140
162
  (click)="hasStartTime && jumpToStart.emit()"
141
163
  [disabled]="!hasStartTime"
@@ -147,53 +169,59 @@ class TimelineControlsComponent {
147
169
  >⏮</button>
148
170
  }
149
171
 
150
- <button
151
- (click)="rewind.emit()"
152
- [style.color]="isRewinding ? theme.buttonActiveColor : theme.buttonColor"
153
- [style.border-color]="isRewinding ? theme.buttonActiveColor + '33' : 'transparent'"
154
- class="ct-btn ct-btn-wide"
155
- [title]="isRewinding ? resolveRewindActive(absMultiplier) : l.rewindTooltip"
156
- >
157
- @if (isRewinding) {
158
- <span style="font-size:11px;font-weight:bold">{{ absMultiplier }}×</span>◀◀
159
- } @else {
160
- ◀◀
161
- }
162
- </button>
172
+ @if (!live) {
173
+ <button
174
+ (click)="rewind.emit()"
175
+ [style.color]="isRewinding ? theme.buttonActiveColor : theme.buttonColor"
176
+ [style.border-color]="isRewinding ? theme.buttonActiveColor + '33' : 'transparent'"
177
+ class="ct-btn ct-btn-wide"
178
+ [title]="isRewinding ? resolveRewindActive(absMultiplier) : l.rewindTooltip"
179
+ >
180
+ @if (isRewinding) {
181
+ <span style="font-size:11px;font-weight:bold">{{ absMultiplier }}×</span>◀◀
182
+ } @else {
183
+ ◀◀
184
+ }
185
+ </button>
186
+ }
163
187
 
164
- <button
165
- (click)="playPause.emit(!isPlaying)"
166
- [style.color]="theme.buttonActiveColor"
167
- [style.border-color]="theme.buttonActiveColor + '55'"
168
- [style.padding-left]="isPlaying ? '0' : '2px'"
169
- class="ct-btn ct-btn-play"
170
- [title]="isPlaying ? l.pauseTooltip : (isRewinding ? l.playFromRewindTooltip : l.playTooltip)"
171
- >
172
- @if (isPlaying) {
173
- <svg width="14" height="16" viewBox="0 0 14 16" fill="currentColor">
174
- <rect x="1" y="0" width="4" height="16" rx="1"/>
175
- <rect x="9" y="0" width="4" height="16" rx="1"/>
176
- </svg>
177
- } @else {
178
-
179
- }
180
- </button>
188
+ @if (!live) {
189
+ <button
190
+ (click)="playPause.emit(!isPlaying)"
191
+ [style.color]="theme.buttonActiveColor"
192
+ [style.border-color]="theme.buttonActiveColor + '55'"
193
+ [style.padding-left]="isPlaying ? '0' : '2px'"
194
+ class="ct-btn ct-btn-play"
195
+ [title]="isPlaying ? l.pauseTooltip : (isRewinding ? l.playFromRewindTooltip : l.playTooltip)"
196
+ >
197
+ @if (isPlaying) {
198
+ <svg width="14" height="16" viewBox="0 0 14 16" fill="currentColor">
199
+ <rect x="1" y="0" width="4" height="16" rx="1"/>
200
+ <rect x="9" y="0" width="4" height="16" rx="1"/>
201
+ </svg>
202
+ } @else {
203
+
204
+ }
205
+ </button>
206
+ }
181
207
 
182
- <button
183
- (click)="fastForward.emit()"
184
- [style.color]="isFastForward ? theme.buttonActiveColor : theme.buttonColor"
185
- [style.border-color]="isFastForward ? theme.buttonActiveColor + '33' : 'transparent'"
186
- class="ct-btn ct-btn-wide"
187
- [title]="isFastForward ? resolveFastForwardActive(absMultiplier) : l.fastForwardTooltip"
188
- >
189
- @if (isFastForward) {
190
- ▶▶<span style="font-size:11px;font-weight:bold">{{ absMultiplier }}×</span>
191
- } @else {
192
- ▶▶
193
- }
194
- </button>
208
+ @if (!live) {
209
+ <button
210
+ (click)="fastForward.emit()"
211
+ [style.color]="isFastForward ? theme.buttonActiveColor : theme.buttonColor"
212
+ [style.border-color]="isFastForward ? theme.buttonActiveColor + '33' : 'transparent'"
213
+ class="ct-btn ct-btn-wide"
214
+ [title]="isFastForward ? resolveFastForwardActive(absMultiplier) : l.fastForwardTooltip"
215
+ >
216
+ @if (isFastForward) {
217
+ ▶▶<span style="font-size:11px;font-weight:bold">{{ absMultiplier }}×</span>
218
+ } @else {
219
+ ▶▶
220
+ }
221
+ </button>
222
+ }
195
223
 
196
- @if (showJumpToEnd !== false) {
224
+ @if (!live && showJumpToEnd !== false) {
197
225
  <button
198
226
  (click)="hasEndTime && jumpToEnd.emit()"
199
227
  [disabled]="!hasEndTime"
@@ -206,9 +234,50 @@ class TimelineControlsComponent {
206
234
  }
207
235
  </div>
208
236
 
209
- <!-- Right: swim-lane toggle -->
237
+ <!-- Right: LIVE (if position=right) + swim-lane toggle -->
210
238
  @if (!isNarrow) {
211
- <div style="display:flex;justify-content:flex-end;align-items:center">
239
+ <div style="display:flex;justify-content:flex-end;align-items:center;gap:8px">
240
+ @if (liveButtonPosition === 'right') {
241
+ <div style="display:flex;align-items:center;gap:4px">
242
+ <button
243
+ (click)="!live && jumpToLive.emit()"
244
+ [style.color]="(live || isLive) ? theme.controlBarBackground : theme.buttonActiveColor"
245
+ [style.background-color]="(live || isLive) ? theme.buttonActiveColor : 'transparent'"
246
+ [style.border-color]="theme.buttonActiveColor"
247
+ [style.opacity]="1"
248
+ [style.width.px]="liveSize.width"
249
+ [style.min-width.px]="liveSize.width"
250
+ [style.height.px]="liveSize.height"
251
+ [style.font-size]="liveSize.fontSize"
252
+ [style.border-radius]="liveSize.borderRadius"
253
+ [style.cursor]="live ? 'default' : 'pointer'"
254
+ style="background:none;border:1px solid;font-weight:bold;letter-spacing:0.05em;display:flex;align-items:center;justify-content:center;padding:0;gap:4px;font-family:system-ui,-apple-system,sans-serif;transition:opacity 0.15s"
255
+ [title]="(live || isLive) ? l.liveActiveTooltip : l.liveTooltip"
256
+ >
257
+ @if (live || isLive) {
258
+ <span
259
+ [style.width.px]="liveSize.dot"
260
+ [style.height.px]="liveSize.dot"
261
+ [style.background-color]="theme.liveDotColor"
262
+ style="border-radius:50%;display:inline-block;flex-shrink:0"
263
+ ></span>
264
+ }
265
+ {{ (live || isLive) ? l.liveActiveLabel : l.liveLabel }}
266
+ </button>
267
+ @if (!isNormalSpeed && !live) {
268
+ <button
269
+ (click)="resetSpeed.emit()"
270
+ [style.color]="theme.buttonActiveColor"
271
+ [style.border-color]="theme.buttonActiveColor + '44'"
272
+ [style.width.px]="liveSize.width"
273
+ [style.min-width.px]="liveSize.width"
274
+ [style.height.px]="liveSize.height"
275
+ style="background:none;border:1px solid;cursor:pointer;font-size:11px;border-radius:4px;display:flex;align-items:center;justify-content:center;padding:0;font-family:system-ui,-apple-system,sans-serif;transition:background-color 0.15s"
276
+ [title]="l.resetSpeedTooltip"
277
+ >{{ isRewinding ? '◀ ' + absMultiplier + '×' : absMultiplier + '× ▶' }}</button>
278
+ }
279
+ </div>
280
+ }
212
281
  @if (hasSwimLaneToggle) {
213
282
  <button
214
283
  (click)="toggleSwimLanes.emit()"
@@ -263,14 +332,14 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.10", ngImpo
263
332
  [style.border-bottom]="'1px solid ' + theme.controlBarBorder"
264
333
  [style.font-family]="'system-ui, -apple-system, sans-serif'"
265
334
  >
266
- <!-- Left: Datetime + LIVE + speed badge -->
335
+ <!-- Left: Datetime + LIVE (if position=left) -->
267
336
  <div style="display:flex;align-items:center;gap:8px;flex-shrink:0">
268
337
  <div
269
- (click)="dateTimeClick.emit()"
270
- [title]="dateTimeClick.observed ? l.dateTimeClickTooltip : ''"
338
+ (click)="!live && dateTimeClick.emit()"
339
+ [title]="(!live && dateTimeClick.observed) ? l.dateTimeClickTooltip : ''"
271
340
  [style.color]="theme.labelColor"
272
341
  style="font-family:monospace;line-height:1.15;border-radius:4px;padding:2px 4px;transition:background 0.15s"
273
- [style.cursor]="dateTimeClick.observed ? 'pointer' : 'default'"
342
+ [style.cursor]="(!live && dateTimeClick.observed) ? 'pointer' : 'default'"
274
343
  >
275
344
  @if (timeFormat) {
276
345
  <div style="font-size:2em;font-weight:bold;letter-spacing:0.02em">
@@ -292,35 +361,47 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.10", ngImpo
292
361
  }
293
362
  </div>
294
363
 
295
- <div style="display:flex;flex-direction:column;gap:2px;justify-content:center">
296
- <!-- LIVE button -->
297
- <button
298
- (click)="jumpToLive.emit()"
299
- [style.color]="isLive ? theme.controlBarBackground : theme.buttonActiveColor"
300
- [style.background-color]="isLive ? theme.buttonActiveColor : 'transparent'"
301
- [style.border-color]="theme.buttonActiveColor"
302
- [style.opacity]="isLive ? 1 : 0.55"
303
- style="background:none;border:1px solid;cursor:pointer;font-size:11px;font-weight:bold;letter-spacing:0.05em;width:52px;min-width:52px;height:20px;border-radius:3px;display:flex;align-items:center;justify-content:center;padding:0;font-family:system-ui,-apple-system,sans-serif;transition:opacity 0.15s"
304
- [title]="isLive ? l.liveActiveTooltip : l.liveTooltip"
305
- >
306
- {{ isLive ? l.liveActiveLabel : l.liveLabel }}
307
- </button>
308
-
309
- <!-- Speed badge -->
310
- <div style="height:20px;display:flex;align-items:center">
311
- @if (!isNormalSpeed) {
364
+ @if (liveButtonPosition === 'left') {
365
+ <div style="display:flex;align-items:center;gap:4px">
366
+ <button
367
+ (click)="!live && jumpToLive.emit()"
368
+ [style.color]="(live || isLive) ? theme.controlBarBackground : theme.buttonActiveColor"
369
+ [style.background-color]="(live || isLive) ? theme.buttonActiveColor : 'transparent'"
370
+ [style.border-color]="theme.buttonActiveColor"
371
+ [style.opacity]="1"
372
+ [style.width.px]="liveSize.width"
373
+ [style.min-width.px]="liveSize.width"
374
+ [style.height.px]="liveSize.height"
375
+ [style.font-size]="liveSize.fontSize"
376
+ [style.border-radius]="liveSize.borderRadius"
377
+ [style.cursor]="live ? 'default' : 'pointer'"
378
+ style="background:none;border:1px solid;font-weight:bold;letter-spacing:0.05em;display:flex;align-items:center;justify-content:center;padding:0;gap:4px;font-family:system-ui,-apple-system,sans-serif;transition:opacity 0.15s"
379
+ [title]="(live || isLive) ? l.liveActiveTooltip : l.liveTooltip"
380
+ >
381
+ @if (live || isLive) {
382
+ <span
383
+ [style.width.px]="liveSize.dot"
384
+ [style.height.px]="liveSize.dot"
385
+ [style.background-color]="theme.liveDotColor"
386
+ style="border-radius:50%;display:inline-block;flex-shrink:0"
387
+ ></span>
388
+ }
389
+ {{ (live || isLive) ? l.liveActiveLabel : l.liveLabel }}
390
+ </button>
391
+ @if (!isNormalSpeed && !live) {
312
392
  <button
313
393
  (click)="resetSpeed.emit()"
314
394
  [style.color]="theme.buttonActiveColor"
315
395
  [style.border-color]="theme.buttonActiveColor + '44'"
316
- style="background:none;border:1px solid;cursor:pointer;font-size:11px;width:52px;min-width:52px;height:20px;border-radius:4px;display:flex;align-items:center;justify-content:center;padding:0;font-family:system-ui,-apple-system,sans-serif;transition:background-color 0.15s"
396
+ [style.width.px]="liveSize.width"
397
+ [style.min-width.px]="liveSize.width"
398
+ [style.height.px]="liveSize.height"
399
+ style="background:none;border:1px solid;cursor:pointer;font-size:11px;border-radius:4px;display:flex;align-items:center;justify-content:center;padding:0;font-family:system-ui,-apple-system,sans-serif;transition:background-color 0.15s"
317
400
  [title]="l.resetSpeedTooltip"
318
- >
319
- {{ isRewinding ? '◀ ' + absMultiplier + '×' : absMultiplier + '× ▶' }}
320
- </button>
401
+ >{{ isRewinding ? '◀ ' + absMultiplier + '×' : absMultiplier + '× ▶' }}</button>
321
402
  }
322
403
  </div>
323
- </div>
404
+ }
324
405
  </div>
325
406
 
326
407
  <!-- Center: Transport buttons -->
@@ -329,7 +410,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.10", ngImpo
329
410
  [style.flex]="isNarrow ? '1' : undefined"
330
411
  [style.justify-content]="isNarrow ? 'center' : undefined"
331
412
  >
332
- @if (showJumpToStart !== false) {
413
+ @if (!live && showJumpToStart !== false) {
333
414
  <button
334
415
  (click)="hasStartTime && jumpToStart.emit()"
335
416
  [disabled]="!hasStartTime"
@@ -341,53 +422,59 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.10", ngImpo
341
422
  >⏮</button>
342
423
  }
343
424
 
344
- <button
345
- (click)="rewind.emit()"
346
- [style.color]="isRewinding ? theme.buttonActiveColor : theme.buttonColor"
347
- [style.border-color]="isRewinding ? theme.buttonActiveColor + '33' : 'transparent'"
348
- class="ct-btn ct-btn-wide"
349
- [title]="isRewinding ? resolveRewindActive(absMultiplier) : l.rewindTooltip"
350
- >
351
- @if (isRewinding) {
352
- <span style="font-size:11px;font-weight:bold">{{ absMultiplier }}×</span>◀◀
353
- } @else {
354
- ◀◀
355
- }
356
- </button>
425
+ @if (!live) {
426
+ <button
427
+ (click)="rewind.emit()"
428
+ [style.color]="isRewinding ? theme.buttonActiveColor : theme.buttonColor"
429
+ [style.border-color]="isRewinding ? theme.buttonActiveColor + '33' : 'transparent'"
430
+ class="ct-btn ct-btn-wide"
431
+ [title]="isRewinding ? resolveRewindActive(absMultiplier) : l.rewindTooltip"
432
+ >
433
+ @if (isRewinding) {
434
+ <span style="font-size:11px;font-weight:bold">{{ absMultiplier }}×</span>◀◀
435
+ } @else {
436
+ ◀◀
437
+ }
438
+ </button>
439
+ }
357
440
 
358
- <button
359
- (click)="playPause.emit(!isPlaying)"
360
- [style.color]="theme.buttonActiveColor"
361
- [style.border-color]="theme.buttonActiveColor + '55'"
362
- [style.padding-left]="isPlaying ? '0' : '2px'"
363
- class="ct-btn ct-btn-play"
364
- [title]="isPlaying ? l.pauseTooltip : (isRewinding ? l.playFromRewindTooltip : l.playTooltip)"
365
- >
366
- @if (isPlaying) {
367
- <svg width="14" height="16" viewBox="0 0 14 16" fill="currentColor">
368
- <rect x="1" y="0" width="4" height="16" rx="1"/>
369
- <rect x="9" y="0" width="4" height="16" rx="1"/>
370
- </svg>
371
- } @else {
372
-
373
- }
374
- </button>
441
+ @if (!live) {
442
+ <button
443
+ (click)="playPause.emit(!isPlaying)"
444
+ [style.color]="theme.buttonActiveColor"
445
+ [style.border-color]="theme.buttonActiveColor + '55'"
446
+ [style.padding-left]="isPlaying ? '0' : '2px'"
447
+ class="ct-btn ct-btn-play"
448
+ [title]="isPlaying ? l.pauseTooltip : (isRewinding ? l.playFromRewindTooltip : l.playTooltip)"
449
+ >
450
+ @if (isPlaying) {
451
+ <svg width="14" height="16" viewBox="0 0 14 16" fill="currentColor">
452
+ <rect x="1" y="0" width="4" height="16" rx="1"/>
453
+ <rect x="9" y="0" width="4" height="16" rx="1"/>
454
+ </svg>
455
+ } @else {
456
+
457
+ }
458
+ </button>
459
+ }
375
460
 
376
- <button
377
- (click)="fastForward.emit()"
378
- [style.color]="isFastForward ? theme.buttonActiveColor : theme.buttonColor"
379
- [style.border-color]="isFastForward ? theme.buttonActiveColor + '33' : 'transparent'"
380
- class="ct-btn ct-btn-wide"
381
- [title]="isFastForward ? resolveFastForwardActive(absMultiplier) : l.fastForwardTooltip"
382
- >
383
- @if (isFastForward) {
384
- ▶▶<span style="font-size:11px;font-weight:bold">{{ absMultiplier }}×</span>
385
- } @else {
386
- ▶▶
387
- }
388
- </button>
461
+ @if (!live) {
462
+ <button
463
+ (click)="fastForward.emit()"
464
+ [style.color]="isFastForward ? theme.buttonActiveColor : theme.buttonColor"
465
+ [style.border-color]="isFastForward ? theme.buttonActiveColor + '33' : 'transparent'"
466
+ class="ct-btn ct-btn-wide"
467
+ [title]="isFastForward ? resolveFastForwardActive(absMultiplier) : l.fastForwardTooltip"
468
+ >
469
+ @if (isFastForward) {
470
+ ▶▶<span style="font-size:11px;font-weight:bold">{{ absMultiplier }}×</span>
471
+ } @else {
472
+ ▶▶
473
+ }
474
+ </button>
475
+ }
389
476
 
390
- @if (showJumpToEnd !== false) {
477
+ @if (!live && showJumpToEnd !== false) {
391
478
  <button
392
479
  (click)="hasEndTime && jumpToEnd.emit()"
393
480
  [disabled]="!hasEndTime"
@@ -400,9 +487,50 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.10", ngImpo
400
487
  }
401
488
  </div>
402
489
 
403
- <!-- Right: swim-lane toggle -->
490
+ <!-- Right: LIVE (if position=right) + swim-lane toggle -->
404
491
  @if (!isNarrow) {
405
- <div style="display:flex;justify-content:flex-end;align-items:center">
492
+ <div style="display:flex;justify-content:flex-end;align-items:center;gap:8px">
493
+ @if (liveButtonPosition === 'right') {
494
+ <div style="display:flex;align-items:center;gap:4px">
495
+ <button
496
+ (click)="!live && jumpToLive.emit()"
497
+ [style.color]="(live || isLive) ? theme.controlBarBackground : theme.buttonActiveColor"
498
+ [style.background-color]="(live || isLive) ? theme.buttonActiveColor : 'transparent'"
499
+ [style.border-color]="theme.buttonActiveColor"
500
+ [style.opacity]="1"
501
+ [style.width.px]="liveSize.width"
502
+ [style.min-width.px]="liveSize.width"
503
+ [style.height.px]="liveSize.height"
504
+ [style.font-size]="liveSize.fontSize"
505
+ [style.border-radius]="liveSize.borderRadius"
506
+ [style.cursor]="live ? 'default' : 'pointer'"
507
+ style="background:none;border:1px solid;font-weight:bold;letter-spacing:0.05em;display:flex;align-items:center;justify-content:center;padding:0;gap:4px;font-family:system-ui,-apple-system,sans-serif;transition:opacity 0.15s"
508
+ [title]="(live || isLive) ? l.liveActiveTooltip : l.liveTooltip"
509
+ >
510
+ @if (live || isLive) {
511
+ <span
512
+ [style.width.px]="liveSize.dot"
513
+ [style.height.px]="liveSize.dot"
514
+ [style.background-color]="theme.liveDotColor"
515
+ style="border-radius:50%;display:inline-block;flex-shrink:0"
516
+ ></span>
517
+ }
518
+ {{ (live || isLive) ? l.liveActiveLabel : l.liveLabel }}
519
+ </button>
520
+ @if (!isNormalSpeed && !live) {
521
+ <button
522
+ (click)="resetSpeed.emit()"
523
+ [style.color]="theme.buttonActiveColor"
524
+ [style.border-color]="theme.buttonActiveColor + '44'"
525
+ [style.width.px]="liveSize.width"
526
+ [style.min-width.px]="liveSize.width"
527
+ [style.height.px]="liveSize.height"
528
+ style="background:none;border:1px solid;cursor:pointer;font-size:11px;border-radius:4px;display:flex;align-items:center;justify-content:center;padding:0;font-family:system-ui,-apple-system,sans-serif;transition:background-color 0.15s"
529
+ [title]="l.resetSpeedTooltip"
530
+ >{{ isRewinding ? '◀ ' + absMultiplier + '×' : absMultiplier + '× ▶' }}</button>
531
+ }
532
+ </div>
533
+ }
406
534
  @if (hasSwimLaneToggle) {
407
535
  <button
408
536
  (click)="toggleSwimLanes.emit()"
@@ -469,6 +597,12 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.10", ngImpo
469
597
  type: Input
470
598
  }], labels: [{
471
599
  type: Input
600
+ }], liveButtonSize: [{
601
+ type: Input
602
+ }], liveButtonPosition: [{
603
+ type: Input
604
+ }], live: [{
605
+ type: Input
472
606
  }], dateTimeClick: [{
473
607
  type: Output
474
608
  }], playPause: [{
@@ -511,6 +645,10 @@ class TimelineCanvasComponent {
511
645
  months;
512
646
  swimLanes;
513
647
  showSwimLanes;
648
+ /** When true, needle scrub is disabled (left-click becomes pan). Zoom and pan remain active. */
649
+ disableNeedleDrag = false;
650
+ /** @see TimelineBaseProps.invertScrollZoom */
651
+ invertScrollZoom = false;
514
652
  timeChange = new EventEmitter();
515
653
  dragStart = new EventEmitter();
516
654
  dragEnd = new EventEmitter();
@@ -519,6 +657,7 @@ class TimelineCanvasComponent {
519
657
  swimLaneItemDoubleClick = new EventEmitter();
520
658
  swimLaneItemContextMenu = new EventEmitter();
521
659
  swimLaneReorder = new EventEmitter();
660
+ rangeSelect = new EventEmitter();
522
661
  canvasRef;
523
662
  // ── Mutable render state (equivalent to React refs) ────────────────────
524
663
  startMs = 0;
@@ -534,6 +673,12 @@ class TimelineCanvasComponent {
534
673
  mouseX = 0;
535
674
  scrubClientX = 0;
536
675
  swimLaneDownTime = 0;
676
+ // Range-selection state
677
+ rangeAnchorMs = 0;
678
+ rangeAnchorX = 0;
679
+ rangeSelection = null;
680
+ // Ghost needle (hover preview)
681
+ hoverMs = null;
537
682
  // Touch state
538
683
  touchMode = 'none';
539
684
  touchX = 0;
@@ -722,6 +867,8 @@ class TimelineCanvasComponent {
722
867
  showSwimLanes: this.showSwimLanesState,
723
868
  scrollTop: this.scrollTop,
724
869
  reorderState: this.reorderState,
870
+ rangeSelection: this.rangeSelection,
871
+ hoverMs: this.hoverMs,
725
872
  });
726
873
  if (clampedScrollTop !== this.scrollTop) {
727
874
  this.scrollTop = clampedScrollTop;
@@ -823,14 +970,32 @@ class TimelineCanvasComponent {
823
970
  }
824
971
  }
825
972
  if (e.button === 0) {
826
- this.mouseMode = 'scrub';
827
- this.scrubClientX = e.clientX;
828
- canvas.style.cursor = 'grabbing';
829
- this.ngZone.run(() => this.dragStart.emit());
830
- const ms = this.startMs + (x / rect.width) * (this.endMs - this.startMs);
831
- this.curMs = ms;
832
- this.draw();
833
- this.ngZone.run(() => this.timeChange.emit(Cesium.JulianDate.fromDate(new Date(ms))));
973
+ if (this.disableNeedleDrag) {
974
+ // In live mode left-click becomes a pan; needle scrub is disabled.
975
+ this.mouseMode = 'slide';
976
+ this.mouseX = e.clientX;
977
+ return;
978
+ }
979
+ const needleX = ((this.curMs - this.startMs) / (this.endMs - this.startMs)) * rect.width;
980
+ const nearNeedle = Math.abs(x - needleX) <= 10;
981
+ const inTickArea = y >= rect.height - TICK_AREA_HEIGHT;
982
+ if (!nearNeedle && inTickArea) {
983
+ this.mouseMode = 'rangeSelectPending';
984
+ this.rangeAnchorX = x;
985
+ this.rangeAnchorMs = this.startMs + (x / rect.width) * (this.endMs - this.startMs);
986
+ canvas.style.cursor = 'crosshair';
987
+ this.ngZone.run(() => this.dragStart.emit());
988
+ }
989
+ else {
990
+ this.mouseMode = 'scrub';
991
+ this.scrubClientX = e.clientX;
992
+ canvas.style.cursor = 'grabbing';
993
+ this.ngZone.run(() => this.dragStart.emit());
994
+ const ms = this.startMs + (x / rect.width) * (this.endMs - this.startMs);
995
+ this.curMs = ms;
996
+ this.draw();
997
+ this.ngZone.run(() => this.timeChange.emit(Cesium.JulianDate.fromDate(new Date(ms))));
998
+ }
834
999
  }
835
1000
  else if (e.button === 1) {
836
1001
  this.mouseMode = 'slide';
@@ -892,6 +1057,19 @@ class TimelineCanvasComponent {
892
1057
  this.draw();
893
1058
  this.ngZone.run(() => this.timeChange.emit(Cesium.JulianDate.fromDate(new Date(ms))));
894
1059
  }
1060
+ else if (this.mouseMode === 'rangeSelectPending' || this.mouseMode === 'rangeSelect') {
1061
+ const x = e.clientX - rect.left;
1062
+ const dx = Math.abs(x - this.rangeAnchorX);
1063
+ if (this.mouseMode === 'rangeSelectPending' && dx >= 3) {
1064
+ this.mouseMode = 'rangeSelect';
1065
+ }
1066
+ if (this.mouseMode === 'rangeSelect') {
1067
+ const cx = Math.max(0, Math.min(w, x));
1068
+ const curMs = this.startMs + (cx / w) * (this.endMs - this.startMs);
1069
+ this.rangeSelection = { startMs: this.rangeAnchorMs, endMs: curMs };
1070
+ this.draw();
1071
+ }
1072
+ }
895
1073
  else if (this.mouseMode === 'slide') {
896
1074
  const dx = this.mouseX - e.clientX;
897
1075
  this.mouseX = e.clientX;
@@ -932,6 +1110,53 @@ class TimelineCanvasComponent {
932
1110
  return;
933
1111
  }
934
1112
  this.stopEdgeScroll();
1113
+ if (this.mouseMode === 'rangeSelectPending') {
1114
+ // Short click — commit needle to anchor position
1115
+ this.curMs = this.rangeAnchorMs;
1116
+ this.rangeSelection = null;
1117
+ this.mouseMode = 'none';
1118
+ const canvas = this.canvasRef?.nativeElement;
1119
+ if (canvas)
1120
+ canvas.style.cursor = 'default';
1121
+ this.draw();
1122
+ this.ngZone.run(() => {
1123
+ this.timeChange.emit(Cesium.JulianDate.fromDate(new Date(this.rangeAnchorMs)));
1124
+ this.dragEnd.emit();
1125
+ });
1126
+ return;
1127
+ }
1128
+ if (this.mouseMode === 'rangeSelect') {
1129
+ const sel = this.rangeSelection;
1130
+ this.rangeSelection = null;
1131
+ if (sel) {
1132
+ const selStart = Math.min(sel.startMs, sel.endMs);
1133
+ const selEnd = Math.max(sel.startMs, sel.endMs);
1134
+ this.startMs = selStart;
1135
+ this.endMs = selEnd;
1136
+ // If the needle is outside the selected range, clamp it to the nearest
1137
+ // edge so the clock-tick auto-scroll doesn't immediately override the zoom.
1138
+ const clampedMs = Math.max(selStart, Math.min(selEnd, this.curMs));
1139
+ const needleMoved = clampedMs !== this.curMs;
1140
+ if (needleMoved) {
1141
+ this.curMs = clampedMs;
1142
+ }
1143
+ const startJd = Cesium.JulianDate.fromDate(new Date(selStart));
1144
+ const endJd = Cesium.JulianDate.fromDate(new Date(selEnd));
1145
+ this.ngZone.run(() => {
1146
+ this.rangeSelect.emit({ start: startJd, end: endJd });
1147
+ if (needleMoved) {
1148
+ this.timeChange.emit(Cesium.JulianDate.fromDate(new Date(clampedMs)));
1149
+ }
1150
+ });
1151
+ }
1152
+ this.mouseMode = 'none';
1153
+ const canvas = this.canvasRef?.nativeElement;
1154
+ if (canvas)
1155
+ canvas.style.cursor = 'default';
1156
+ this.draw();
1157
+ this.ngZone.run(() => this.dragEnd.emit());
1158
+ return;
1159
+ }
935
1160
  this.mouseMode = 'none';
936
1161
  const canvas = this.canvasRef?.nativeElement;
937
1162
  if (canvas)
@@ -939,14 +1164,25 @@ class TimelineCanvasComponent {
939
1164
  this.ngZone.run(() => this.dragEnd.emit());
940
1165
  }
941
1166
  onCanvasMouseMove(e) {
942
- if (this.mouseMode !== 'none')
943
- return;
944
1167
  const canvas = this.canvasRef.nativeElement;
945
1168
  const rect = canvas.getBoundingClientRect();
946
1169
  const x = e.clientX - rect.left;
947
1170
  const y = e.clientY - rect.top;
1171
+ // Update ghost needle while idle
1172
+ if (this.mouseMode === 'none') {
1173
+ if (!this.disableNeedleDrag) {
1174
+ this.hoverMs = this.startMs + (Math.max(0, Math.min(rect.width, x)) / rect.width) * (this.endMs - this.startMs);
1175
+ }
1176
+ else if (this.hoverMs !== null) {
1177
+ this.hoverMs = null;
1178
+ }
1179
+ }
1180
+ else {
1181
+ this.hoverMs = null;
1182
+ return;
1183
+ }
948
1184
  const needleX = ((this.curMs - this.startMs) / (this.endMs - this.startMs)) * rect.width;
949
- const nearNeedle = Math.abs(x - needleX) <= 10;
1185
+ const nearNeedle = !this.disableNeedleDrag && Math.abs(x - needleX) <= 10;
950
1186
  if (this.isInSwimLaneRegion(y, rect.height)) {
951
1187
  const hit = this.hitTestSwimLane(x, y, rect.width, rect.height);
952
1188
  const prev = this.hoveredItem;
@@ -955,14 +1191,12 @@ class TimelineCanvasComponent {
955
1191
  if (!prev || prev.item.id !== hit.item.id || prev.lane.id !== hit.lane.id) {
956
1192
  this.hoveredItem = hit;
957
1193
  this.ngZone.run(() => this.swimLaneItemHover.emit({ laneId: hit.lane.id, item: hit.item, originalEvent: e }));
958
- this.draw();
959
1194
  }
960
1195
  }
961
1196
  else {
962
1197
  if (prev) {
963
1198
  this.hoveredItem = null;
964
1199
  this.ngZone.run(() => this.swimLaneItemHover.emit(null));
965
- this.draw();
966
1200
  }
967
1201
  if (nearNeedle) {
968
1202
  canvas.style.cursor = 'grab';
@@ -972,14 +1206,23 @@ class TimelineCanvasComponent {
972
1206
  canvas.style.cursor = labelLane && this.swimLaneReorder.observed ? 'grab' : 'default';
973
1207
  }
974
1208
  }
1209
+ this.draw();
975
1210
  return;
976
1211
  }
977
1212
  if (this.hoveredItem) {
978
1213
  this.hoveredItem = null;
979
1214
  this.ngZone.run(() => this.swimLaneItemHover.emit(null));
980
- this.draw();
981
1215
  }
982
- canvas.style.cursor = nearNeedle ? 'grab' : 'default';
1216
+ if (nearNeedle) {
1217
+ canvas.style.cursor = 'grab';
1218
+ }
1219
+ else if (!this.disableNeedleDrag && y >= rect.height - TICK_AREA_HEIGHT) {
1220
+ canvas.style.cursor = 'crosshair';
1221
+ }
1222
+ else {
1223
+ canvas.style.cursor = 'default';
1224
+ }
1225
+ this.draw();
983
1226
  }
984
1227
  onCanvasClick(e) {
985
1228
  const elapsed = performance.now() - this.swimLaneDownTime;
@@ -1019,11 +1262,12 @@ class TimelineCanvasComponent {
1019
1262
  if (this.hoveredItem) {
1020
1263
  this.hoveredItem = null;
1021
1264
  this.ngZone.run(() => this.swimLaneItemHover.emit(null));
1022
- this.draw();
1023
1265
  }
1266
+ this.hoverMs = null;
1024
1267
  const canvas = this.canvasRef?.nativeElement;
1025
1268
  if (this.mouseMode === 'none' && canvas)
1026
1269
  canvas.style.cursor = 'default';
1270
+ this.draw();
1027
1271
  }
1028
1272
  // ── Wheel handler ──────────────────────────────────────────────────────
1029
1273
  onWheel(e) {
@@ -1044,7 +1288,7 @@ class TimelineCanvasComponent {
1044
1288
  return;
1045
1289
  }
1046
1290
  }
1047
- this.zoomFrom(Math.pow(1.05, e.deltaY > 0 ? -1 : 1));
1291
+ this.zoomFrom(Math.pow(1.05, e.deltaY > 0 === this.invertScrollZoom ? -1 : 1));
1048
1292
  }
1049
1293
  // ── Touch handlers ─────────────────────────────────────────────────────
1050
1294
  onTouchStart(e) {
@@ -1056,15 +1300,22 @@ class TimelineCanvasComponent {
1056
1300
  const cx = Math.max(0, Math.min(rect.width, x));
1057
1301
  const ms = this.startMs + (cx / rect.width) * (this.endMs - this.startMs);
1058
1302
  this.prePinchCurMs = this.curMs;
1059
- this.touchMode = 'scrub';
1060
- this.touchX = e.touches[0].clientX;
1061
- this.scrubClientX = e.touches[0].clientX;
1062
- this.curMs = ms;
1063
- this.draw();
1064
- this.ngZone.run(() => {
1065
- this.dragStart.emit();
1066
- this.timeChange.emit(Cesium.JulianDate.fromDate(new Date(ms)));
1067
- });
1303
+ if (this.disableNeedleDrag) {
1304
+ // In live mode single-finger becomes a pan, not a scrub.
1305
+ this.touchMode = 'slide';
1306
+ this.touchX = e.touches[0].clientX;
1307
+ }
1308
+ else {
1309
+ this.touchMode = 'scrub';
1310
+ this.touchX = e.touches[0].clientX;
1311
+ this.scrubClientX = e.touches[0].clientX;
1312
+ this.curMs = ms;
1313
+ this.draw();
1314
+ this.ngZone.run(() => {
1315
+ this.dragStart.emit();
1316
+ this.timeChange.emit(Cesium.JulianDate.fromDate(new Date(ms)));
1317
+ });
1318
+ }
1068
1319
  }
1069
1320
  else if (e.touches.length >= 2) {
1070
1321
  // If we were scrubbing, undo the needle move — pinch-zoom should not
@@ -1140,7 +1391,7 @@ class TimelineCanvasComponent {
1140
1391
  }
1141
1392
  }
1142
1393
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.10", ngImport: i0, type: TimelineCanvasComponent, deps: [{ token: i0.NgZone }], target: i0.ɵɵFactoryTarget.Component });
1143
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "21.2.10", type: TimelineCanvasComponent, isStandalone: true, selector: "ct-timeline-canvas", inputs: { currentTime: "currentTime", defaultStartMs: "defaultStartMs", defaultEndMs: "defaultEndMs", theme: "theme", maxTicks: "maxTicks", timezone: "timezone", dateTimeFormat: "dateTimeFormat", months: "months", swimLanes: "swimLanes", showSwimLanes: "showSwimLanes" }, outputs: { timeChange: "timeChange", dragStart: "dragStart", dragEnd: "dragEnd", swimLaneItemClick: "swimLaneItemClick", swimLaneItemHover: "swimLaneItemHover", swimLaneItemDoubleClick: "swimLaneItemDoubleClick", swimLaneItemContextMenu: "swimLaneItemContextMenu", swimLaneReorder: "swimLaneReorder" }, viewQueries: [{ propertyName: "canvasRef", first: true, predicate: ["canvas"], descendants: true }], usesOnChanges: true, ngImport: i0, template: `<canvas #canvas
1394
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "21.2.10", type: TimelineCanvasComponent, isStandalone: true, selector: "ct-timeline-canvas", inputs: { currentTime: "currentTime", defaultStartMs: "defaultStartMs", defaultEndMs: "defaultEndMs", theme: "theme", maxTicks: "maxTicks", timezone: "timezone", dateTimeFormat: "dateTimeFormat", months: "months", swimLanes: "swimLanes", showSwimLanes: "showSwimLanes", disableNeedleDrag: "disableNeedleDrag", invertScrollZoom: "invertScrollZoom" }, outputs: { timeChange: "timeChange", dragStart: "dragStart", dragEnd: "dragEnd", swimLaneItemClick: "swimLaneItemClick", swimLaneItemHover: "swimLaneItemHover", swimLaneItemDoubleClick: "swimLaneItemDoubleClick", swimLaneItemContextMenu: "swimLaneItemContextMenu", swimLaneReorder: "swimLaneReorder", rangeSelect: "rangeSelect" }, viewQueries: [{ propertyName: "canvasRef", first: true, predicate: ["canvas"], descendants: true }], usesOnChanges: true, ngImport: i0, template: `<canvas #canvas
1144
1395
  style="width:100%;flex:1;min-height:0;display:block;cursor:default"
1145
1396
  (mousedown)="onCanvasMouseDown($event)"
1146
1397
  (mousemove)="onCanvasMouseMove($event)"
@@ -1181,6 +1432,10 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.10", ngImpo
1181
1432
  type: Input
1182
1433
  }], showSwimLanes: [{
1183
1434
  type: Input
1435
+ }], disableNeedleDrag: [{
1436
+ type: Input
1437
+ }], invertScrollZoom: [{
1438
+ type: Input
1184
1439
  }], timeChange: [{
1185
1440
  type: Output
1186
1441
  }], dragStart: [{
@@ -1197,6 +1452,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.10", ngImpo
1197
1452
  type: Output
1198
1453
  }], swimLaneReorder: [{
1199
1454
  type: Output
1455
+ }], rangeSelect: [{
1456
+ type: Output
1200
1457
  }], canvasRef: [{
1201
1458
  type: ViewChild,
1202
1459
  args: ['canvas']
@@ -1230,6 +1487,13 @@ class TimelineComponent {
1230
1487
  swimLaneTransition = 'animated';
1231
1488
  /** Overrides for control-bar labels and tooltips (i18n / custom verbiage). */
1232
1489
  labels;
1490
+ /** @see TimelineBaseProps.liveButtonSize */
1491
+ liveButtonSize;
1492
+ /** @see TimelineBaseProps.liveButtonPosition */
1493
+ liveButtonPosition;
1494
+ /** @see TimelineBaseProps.live */
1495
+ live = false;
1496
+ invertScrollZoom = false;
1233
1497
  // ── Outputs ────────────────────────────────────────────────────────────
1234
1498
  timeChange = new EventEmitter();
1235
1499
  playPause = new EventEmitter();
@@ -1241,6 +1505,7 @@ class TimelineComponent {
1241
1505
  swimLaneItemDoubleClick = new EventEmitter();
1242
1506
  swimLaneItemContextMenu = new EventEmitter();
1243
1507
  swimLaneReorder = new EventEmitter();
1508
+ rangeSelect = new EventEmitter();
1244
1509
  // ── ViewChild refs ─────────────────────────────────────────────────────
1245
1510
  canvasComp;
1246
1511
  controlsRef;
@@ -1265,7 +1530,7 @@ class TimelineComponent {
1265
1530
  return this.swimLanes != null && this.swimLanes.length > 0;
1266
1531
  }
1267
1532
  get isLive() {
1268
- return Math.abs(Cesium.JulianDate.toDate(this.currentTimeState).getTime() - Date.now()) < 10_000;
1533
+ return Math.abs(Cesium.JulianDate.toDate(this.currentTimeState).getTime() - Date.now()) < 2_000;
1269
1534
  }
1270
1535
  get isCollapsed() {
1271
1536
  return this.hasSwimLanes && !this.swimLanesExpanded;
@@ -1316,7 +1581,7 @@ class TimelineComponent {
1316
1581
  this.cleanupClockSync();
1317
1582
  this.setupClockSync();
1318
1583
  }
1319
- if (changes['jumpToTime'] && this.jumpToTime) {
1584
+ if (changes['jumpToTime'] && this.jumpToTime && !this.live) {
1320
1585
  const t = toJulianDate(this.jumpToTime);
1321
1586
  this.handleTimeChange(t);
1322
1587
  if (this.canvasComp) {
@@ -1470,7 +1735,7 @@ class TimelineComponent {
1470
1735
  this.cdr.markForCheck();
1471
1736
  }
1472
1737
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.10", ngImport: i0, type: TimelineComponent, deps: [{ token: i0.ChangeDetectorRef }, { token: i0.NgZone }], target: i0.ɵɵFactoryTarget.Component });
1473
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.10", type: TimelineComponent, isStandalone: true, selector: "ct-timeline", inputs: { startTime: "startTime", endTime: "endTime", currentTime: "currentTime", clock: "clock", height: "height", showControls: "showControls", showJumpToStart: "showJumpToStart", showJumpToEnd: "showJumpToEnd", enableDrag: "enableDrag", dateTimeFormat: "dateTimeFormat", jumpToTime: "jumpToTime", maxTicks: "maxTicks", ffSpeeds: "ffSpeeds", rwSpeeds: "rwSpeeds", theme: "theme", cssClass: "cssClass", timezone: "timezone", swimLanes: "swimLanes", showSwimLanes: "showSwimLanes", swimLaneTransition: "swimLaneTransition", labels: "labels" }, outputs: { timeChange: "timeChange", playPause: "playPause", multiplierChange: "multiplierChange", dateTimeClick: "dateTimeClick", showSwimLanesChange: "showSwimLanesChange", swimLaneItemClick: "swimLaneItemClick", swimLaneItemHover: "swimLaneItemHover", swimLaneItemDoubleClick: "swimLaneItemDoubleClick", swimLaneItemContextMenu: "swimLaneItemContextMenu", swimLaneReorder: "swimLaneReorder" }, viewQueries: [{ propertyName: "canvasComp", first: true, predicate: TimelineCanvasComponent, descendants: true }, { propertyName: "controlsRef", first: true, predicate: ["controlsEl"], descendants: true }], usesOnChanges: true, ngImport: i0, template: `
1738
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.10", type: TimelineComponent, isStandalone: true, selector: "ct-timeline", inputs: { startTime: "startTime", endTime: "endTime", currentTime: "currentTime", clock: "clock", height: "height", showControls: "showControls", showJumpToStart: "showJumpToStart", showJumpToEnd: "showJumpToEnd", enableDrag: "enableDrag", dateTimeFormat: "dateTimeFormat", jumpToTime: "jumpToTime", maxTicks: "maxTicks", ffSpeeds: "ffSpeeds", rwSpeeds: "rwSpeeds", theme: "theme", cssClass: "cssClass", timezone: "timezone", swimLanes: "swimLanes", showSwimLanes: "showSwimLanes", swimLaneTransition: "swimLaneTransition", labels: "labels", liveButtonSize: "liveButtonSize", liveButtonPosition: "liveButtonPosition", live: "live", invertScrollZoom: "invertScrollZoom" }, outputs: { timeChange: "timeChange", playPause: "playPause", multiplierChange: "multiplierChange", dateTimeClick: "dateTimeClick", showSwimLanesChange: "showSwimLanesChange", swimLaneItemClick: "swimLaneItemClick", swimLaneItemHover: "swimLaneItemHover", swimLaneItemDoubleClick: "swimLaneItemDoubleClick", swimLaneItemContextMenu: "swimLaneItemContextMenu", swimLaneReorder: "swimLaneReorder", rangeSelect: "rangeSelect" }, viewQueries: [{ propertyName: "canvasComp", first: true, predicate: TimelineCanvasComponent, descendants: true }, { propertyName: "controlsRef", first: true, predicate: ["controlsEl"], descendants: true }], usesOnChanges: true, ngImport: i0, template: `
1474
1739
  <div
1475
1740
  [class]="cssClass"
1476
1741
  [style.width]="'100%'"
@@ -1506,11 +1771,14 @@ class TimelineComponent {
1506
1771
  (dateTimeClick)="dateTimeClick.emit()"
1507
1772
  (toggleSwimLanes)="handleToggleSwimLanes()"
1508
1773
  [labels]="labels"
1774
+ [liveButtonSize]="liveButtonSize"
1775
+ [liveButtonPosition]="liveButtonPosition"
1776
+ [live]="live"
1509
1777
  />
1510
1778
  </div>
1511
1779
  }
1512
1780
 
1513
- @if (enableDrag !== false) {
1781
+ @if (enableDrag !== false || live) {
1514
1782
  <ct-timeline-canvas
1515
1783
  [currentTime]="currentTimeState"
1516
1784
  [defaultStartMs]="defaultStartMs"
@@ -1522,6 +1790,8 @@ class TimelineComponent {
1522
1790
  [months]="labels?.months"
1523
1791
  [swimLanes]="swimLanes"
1524
1792
  [showSwimLanes]="swimLanesExpanded"
1793
+ [disableNeedleDrag]="live"
1794
+ [invertScrollZoom]="invertScrollZoom"
1525
1795
  (timeChange)="handleTimeChange($event)"
1526
1796
  (dragStart)="isDragging = true"
1527
1797
  (dragEnd)="isDragging = false"
@@ -1530,10 +1800,11 @@ class TimelineComponent {
1530
1800
  (swimLaneItemDoubleClick)="swimLaneItemDoubleClick.emit($event)"
1531
1801
  (swimLaneItemContextMenu)="swimLaneItemContextMenu.emit($event)"
1532
1802
  (swimLaneReorder)="swimLaneReorder.emit($event)"
1803
+ (rangeSelect)="rangeSelect.emit($event)"
1533
1804
  />
1534
1805
  }
1535
1806
  </div>
1536
- `, isInline: true, styles: [":host{display:block}\n"], dependencies: [{ kind: "component", type: TimelineControlsComponent, selector: "ct-timeline-controls", inputs: ["currentTime", "isPlaying", "multiplier", "dateTimeFormat", "timezone", "isLive", "hasStartTime", "hasEndTime", "showJumpToStart", "showJumpToEnd", "theme", "swimLanesVisible", "labels"], outputs: ["dateTimeClick", "playPause", "jumpToStart", "rewind", "fastForward", "jumpToEnd", "jumpToLive", "resetSpeed", "toggleSwimLanes"] }, { kind: "component", type: TimelineCanvasComponent, selector: "ct-timeline-canvas", inputs: ["currentTime", "defaultStartMs", "defaultEndMs", "theme", "maxTicks", "timezone", "dateTimeFormat", "months", "swimLanes", "showSwimLanes"], outputs: ["timeChange", "dragStart", "dragEnd", "swimLaneItemClick", "swimLaneItemHover", "swimLaneItemDoubleClick", "swimLaneItemContextMenu", "swimLaneReorder"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
1807
+ `, isInline: true, styles: [":host{display:block}\n"], dependencies: [{ kind: "component", type: TimelineControlsComponent, selector: "ct-timeline-controls", inputs: ["currentTime", "isPlaying", "multiplier", "dateTimeFormat", "timezone", "isLive", "hasStartTime", "hasEndTime", "showJumpToStart", "showJumpToEnd", "theme", "swimLanesVisible", "labels", "liveButtonSize", "liveButtonPosition", "live"], outputs: ["dateTimeClick", "playPause", "jumpToStart", "rewind", "fastForward", "jumpToEnd", "jumpToLive", "resetSpeed", "toggleSwimLanes"] }, { kind: "component", type: TimelineCanvasComponent, selector: "ct-timeline-canvas", inputs: ["currentTime", "defaultStartMs", "defaultEndMs", "theme", "maxTicks", "timezone", "dateTimeFormat", "months", "swimLanes", "showSwimLanes", "disableNeedleDrag", "invertScrollZoom"], outputs: ["timeChange", "dragStart", "dragEnd", "swimLaneItemClick", "swimLaneItemHover", "swimLaneItemDoubleClick", "swimLaneItemContextMenu", "swimLaneReorder", "rangeSelect"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
1537
1808
  }
1538
1809
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.10", ngImport: i0, type: TimelineComponent, decorators: [{
1539
1810
  type: Component,
@@ -1573,11 +1844,14 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.10", ngImpo
1573
1844
  (dateTimeClick)="dateTimeClick.emit()"
1574
1845
  (toggleSwimLanes)="handleToggleSwimLanes()"
1575
1846
  [labels]="labels"
1847
+ [liveButtonSize]="liveButtonSize"
1848
+ [liveButtonPosition]="liveButtonPosition"
1849
+ [live]="live"
1576
1850
  />
1577
1851
  </div>
1578
1852
  }
1579
1853
 
1580
- @if (enableDrag !== false) {
1854
+ @if (enableDrag !== false || live) {
1581
1855
  <ct-timeline-canvas
1582
1856
  [currentTime]="currentTimeState"
1583
1857
  [defaultStartMs]="defaultStartMs"
@@ -1589,6 +1863,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.10", ngImpo
1589
1863
  [months]="labels?.months"
1590
1864
  [swimLanes]="swimLanes"
1591
1865
  [showSwimLanes]="swimLanesExpanded"
1866
+ [disableNeedleDrag]="live"
1867
+ [invertScrollZoom]="invertScrollZoom"
1592
1868
  (timeChange)="handleTimeChange($event)"
1593
1869
  (dragStart)="isDragging = true"
1594
1870
  (dragEnd)="isDragging = false"
@@ -1597,6 +1873,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.10", ngImpo
1597
1873
  (swimLaneItemDoubleClick)="swimLaneItemDoubleClick.emit($event)"
1598
1874
  (swimLaneItemContextMenu)="swimLaneItemContextMenu.emit($event)"
1599
1875
  (swimLaneReorder)="swimLaneReorder.emit($event)"
1876
+ (rangeSelect)="rangeSelect.emit($event)"
1600
1877
  />
1601
1878
  }
1602
1879
  </div>
@@ -1643,6 +1920,14 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.10", ngImpo
1643
1920
  type: Input
1644
1921
  }], labels: [{
1645
1922
  type: Input
1923
+ }], liveButtonSize: [{
1924
+ type: Input
1925
+ }], liveButtonPosition: [{
1926
+ type: Input
1927
+ }], live: [{
1928
+ type: Input
1929
+ }], invertScrollZoom: [{
1930
+ type: Input
1646
1931
  }], timeChange: [{
1647
1932
  type: Output
1648
1933
  }], playPause: [{
@@ -1663,6 +1948,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.10", ngImpo
1663
1948
  type: Output
1664
1949
  }], swimLaneReorder: [{
1665
1950
  type: Output
1951
+ }], rangeSelect: [{
1952
+ type: Output
1666
1953
  }], canvasComp: [{
1667
1954
  type: ViewChild,
1668
1955
  args: [TimelineCanvasComponent]