@design.estate/dees-catalog 12.4.0 → 13.0.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 (75) hide show
  1. package/dist_bundle/bundle.js +2603 -1201
  2. package/dist_bundle/bundle.js.map +1 -1
  3. package/dist_ts_web/00_commitinfo_data.js +3 -3
  4. package/dist_ts_web/elements/00group-chart/dees-chart-area/component.d.ts +26 -16
  5. package/dist_ts_web/elements/00group-chart/dees-chart-area/component.js +161 -72
  6. package/dist_ts_web/elements/00group-chart/dees-chart-area/realtime-updates.d.ts +2 -1
  7. package/dist_ts_web/elements/00group-chart/dees-chart-area/realtime-updates.js +3 -2
  8. package/dist_ts_web/elements/00group-chart/dees-chart-area/template.js +4 -3
  9. package/dist_ts_web/elements/00group-input/dees-input-dropdown/dees-input-dropdown-popup.d.ts +13 -0
  10. package/dist_ts_web/elements/00group-input/dees-input-dropdown/dees-input-dropdown-popup.js +76 -7
  11. package/dist_ts_web/elements/00group-input/dees-input-reference/dees-input-reference.d.ts +42 -0
  12. package/dist_ts_web/elements/00group-input/dees-input-reference/dees-input-reference.demo.d.ts +1 -0
  13. package/dist_ts_web/elements/00group-input/dees-input-reference/dees-input-reference.demo.js +8 -0
  14. package/dist_ts_web/elements/00group-input/dees-input-reference/dees-input-reference.js +347 -0
  15. package/dist_ts_web/elements/00group-input/dees-input-reference/document.d.ts +5 -0
  16. package/dist_ts_web/elements/00group-input/dees-input-reference/document.js +45 -0
  17. package/dist_ts_web/elements/00group-input/dees-input-reference/index.d.ts +2 -0
  18. package/dist_ts_web/elements/00group-input/dees-input-reference/index.js +3 -0
  19. package/dist_ts_web/elements/00group-input/dees-input-reference/interfaces.d.ts +16 -0
  20. package/dist_ts_web/elements/00group-input/dees-input-reference/interfaces.js +2 -0
  21. package/dist_ts_web/elements/00group-input/index.d.ts +1 -0
  22. package/dist_ts_web/elements/00group-input/index.js +2 -1
  23. package/dist_ts_web/elements/00group-kanban/dees-kanban/dees-kanban.d.ts +58 -0
  24. package/dist_ts_web/elements/00group-kanban/dees-kanban/dees-kanban.demo.d.ts +1 -0
  25. package/dist_ts_web/elements/00group-kanban/dees-kanban/dees-kanban.demo.js +9 -0
  26. package/dist_ts_web/elements/00group-kanban/dees-kanban/dees-kanban.js +536 -0
  27. package/dist_ts_web/elements/00group-kanban/dees-kanban-card/dees-kanban-card.d.ts +20 -0
  28. package/dist_ts_web/elements/00group-kanban/dees-kanban-card/dees-kanban-card.js +157 -0
  29. package/dist_ts_web/elements/00group-kanban/dees-kanban-column/dees-kanban-column.d.ts +23 -0
  30. package/dist_ts_web/elements/00group-kanban/dees-kanban-column/dees-kanban-column.js +192 -0
  31. package/dist_ts_web/elements/00group-kanban/dees-kanban-discussion/dees-kanban-discussion.d.ts +24 -0
  32. package/dist_ts_web/elements/00group-kanban/dees-kanban-discussion/dees-kanban-discussion.js +211 -0
  33. package/dist_ts_web/elements/00group-kanban/dees-kanban-ticket-detail/dees-kanban-ticket-detail.d.ts +35 -0
  34. package/dist_ts_web/elements/00group-kanban/dees-kanban-ticket-detail/dees-kanban-ticket-detail.js +269 -0
  35. package/dist_ts_web/elements/00group-kanban/index.d.ts +6 -0
  36. package/dist_ts_web/elements/00group-kanban/index.js +7 -0
  37. package/dist_ts_web/elements/00group-kanban/interfaces.d.ts +84 -0
  38. package/dist_ts_web/elements/00group-kanban/interfaces.js +2 -0
  39. package/dist_ts_web/elements/00group-kanban/kanban.demodata.d.ts +7 -0
  40. package/dist_ts_web/elements/00group-kanban/kanban.demodata.js +60 -0
  41. package/dist_ts_web/elements/00group-kanban/kanban.helpers.d.ts +6 -0
  42. package/dist_ts_web/elements/00group-kanban/kanban.helpers.js +11 -0
  43. package/dist_ts_web/elements/00group-kanban/kanban.styles.d.ts +1 -0
  44. package/dist_ts_web/elements/00group-kanban/kanban.styles.js +18 -0
  45. package/dist_ts_web/elements/index.d.ts +1 -0
  46. package/dist_ts_web/elements/index.js +2 -1
  47. package/dist_ts_web/services/DeesServiceLibLoader.d.ts +1 -0
  48. package/dist_ts_web/services/DeesServiceLibLoader.js +2 -1
  49. package/package.json +1 -1
  50. package/readme.md +165 -1
  51. package/scripts/check-packed-consumer.mjs +1 -1
  52. package/ts_web/00_commitinfo_data.ts +3 -3
  53. package/ts_web/elements/00group-chart/dees-chart-area/component.ts +195 -79
  54. package/ts_web/elements/00group-chart/dees-chart-area/realtime-updates.ts +4 -2
  55. package/ts_web/elements/00group-chart/dees-chart-area/template.ts +3 -2
  56. package/ts_web/elements/00group-input/dees-input-dropdown/dees-input-dropdown-popup.ts +40 -5
  57. package/ts_web/elements/00group-input/dees-input-reference/dees-input-reference.demo.ts +9 -0
  58. package/ts_web/elements/00group-input/dees-input-reference/dees-input-reference.ts +221 -0
  59. package/ts_web/elements/00group-input/dees-input-reference/document.ts +38 -0
  60. package/ts_web/elements/00group-input/dees-input-reference/index.ts +2 -0
  61. package/ts_web/elements/00group-input/dees-input-reference/interfaces.ts +8 -0
  62. package/ts_web/elements/00group-input/index.ts +1 -0
  63. package/ts_web/elements/00group-kanban/dees-kanban/dees-kanban.demo.ts +9 -0
  64. package/ts_web/elements/00group-kanban/dees-kanban/dees-kanban.ts +315 -0
  65. package/ts_web/elements/00group-kanban/dees-kanban-card/dees-kanban-card.ts +58 -0
  66. package/ts_web/elements/00group-kanban/dees-kanban-column/dees-kanban-column.ts +60 -0
  67. package/ts_web/elements/00group-kanban/dees-kanban-discussion/dees-kanban-discussion.ts +81 -0
  68. package/ts_web/elements/00group-kanban/dees-kanban-ticket-detail/dees-kanban-ticket-detail.ts +114 -0
  69. package/ts_web/elements/00group-kanban/index.ts +6 -0
  70. package/ts_web/elements/00group-kanban/interfaces.ts +67 -0
  71. package/ts_web/elements/00group-kanban/kanban.demodata.ts +56 -0
  72. package/ts_web/elements/00group-kanban/kanban.helpers.ts +13 -0
  73. package/ts_web/elements/00group-kanban/kanban.styles.ts +18 -0
  74. package/ts_web/elements/index.ts +1 -0
  75. package/ts_web/services/DeesServiceLibLoader.ts +2 -0
@@ -19,7 +19,14 @@ import { getEchartsSeriesColors, hexToRgba, getThemeColors } from '../dees-chart
19
19
 
20
20
  import { systemFontFamily } from '../../00fonts.js';
21
21
 
22
- import type { IChartApi, ISeriesApi, UTCTimestamp, MouseEventParams } from 'lightweight-charts';
22
+ import type {
23
+ AreaData,
24
+ IChartApi,
25
+ ISeriesApi,
26
+ MouseEventParams,
27
+ UTCTimestamp,
28
+ WhitespaceData,
29
+ } from 'lightweight-charts';
23
30
  import { DeesServiceLibLoader, type ILightweightChartsBundle } from '../../../services/index.js';
24
31
  import '../../00group-layout/dees-tile/dees-tile.js';
25
32
 
@@ -27,9 +34,28 @@ export type ChartSeriesConfig = {
27
34
  name?: string;
28
35
  /** Explicit series color (hex or hsl). Falls back to the shared catalog palette by index. */
29
36
  color?: string;
30
- data: Array<{ x: any; y: number }>;
37
+ /** A null reading keeps its timestamp and breaks the drawn area line. */
38
+ data: Array<{ x: any; y: number | null }>;
31
39
  }[];
32
40
 
41
+ export interface IChartSeriesStats {
42
+ name: string;
43
+ /** Null when the newest sample is unavailable, even if older samples exist. */
44
+ latest: number | null;
45
+ min: number | null;
46
+ max: number | null;
47
+ avg: number | null;
48
+ color: string;
49
+ }
50
+
51
+ type TChartAreaData = AreaData<UTCTimestamp> | WhitespaceData<UTCTimestamp>;
52
+
53
+ interface IChartRenderedSegment {
54
+ key: string;
55
+ logicalIndex: number;
56
+ data: TChartAreaData[];
57
+ }
58
+
33
59
  export type TChartLegendStat = 'latest' | 'min' | 'max' | 'avg';
34
60
  export type TChartPriceLine = 'avg' | 'max';
35
61
 
@@ -55,7 +81,7 @@ export class DeesChartArea extends DeesElement {
55
81
  accessor chart: IChartApi | null = null;
56
82
 
57
83
  @state()
58
- accessor seriesStats: Array<{ name: string; latest: number; min: number; max: number; avg: number; color: string }> = [];
84
+ accessor seriesStats: IChartSeriesStats[] = [];
59
85
 
60
86
  @state()
61
87
  accessor isFullPage: boolean = false;
@@ -117,6 +143,9 @@ export class DeesChartArea extends DeesElement {
117
143
  private autoScrollTimer: number | null = null;
118
144
  private lcBundle: ILightweightChartsBundle | null = null;
119
145
  private seriesApis: Map<string, ISeriesApi<any>> = new Map();
146
+ /** LC's data() omits whitespace; this mirrors the exact points sent to each series API. */
147
+ private renderedSeriesData: Map<string, IRealtimeChartPoint[]> = new Map();
148
+ private renderedSegments: IChartRenderedSegment[] = [];
120
149
  private priceLines: Map<string, any[]> = new Map();
121
150
  private tooltipEl: HTMLDivElement | null = null;
122
151
  private crosshairActive = false;
@@ -219,6 +248,8 @@ export class DeesChartArea extends DeesElement {
219
248
  console.error('Error destroying chart:', error);
220
249
  } finally {
221
250
  this.seriesApis.clear();
251
+ this.renderedSeriesData.clear();
252
+ this.renderedSegments = [];
222
253
  this.priceLines.clear();
223
254
  }
224
255
  });
@@ -316,15 +347,17 @@ export class DeesChartArea extends DeesElement {
316
347
  this.paintRangeOverlay(fromCoordinate, toCoordinate);
317
348
  }
318
349
 
319
- private convertDataToLC(data: Array<{ x: any; y: number }>): Array<{ time: UTCTimestamp; value: number }> {
320
- const pointsByTime = new Map<number, { time: UTCTimestamp; value: number }>();
350
+ private convertDataToLC(data: ChartSeriesConfig[number]['data']): TChartAreaData[] {
351
+ const pointsByTime = new Map<number, TChartAreaData>();
321
352
  for (const point of data) {
322
353
  const ms = typeof point.x === 'number' ? point.x : new Date(point.x).getTime();
323
354
  const time = Math.floor(ms / 1000);
324
- if (!Number.isFinite(time) || !Number.isFinite(point.y)) continue;
355
+ if (!Number.isFinite(time) || (point.y !== null && !Number.isFinite(point.y))) continue;
325
356
  // Lightweight Charts requires unique ordered timestamps. The latest
326
- // sample wins when multiple updates land in the same second.
327
- pointsByTime.set(time, { time: time as UTCTimestamp, value: point.y });
357
+ // sample, including an unavailable one, wins within the same second.
358
+ pointsByTime.set(time, point.y === null
359
+ ? { time: time as UTCTimestamp }
360
+ : { time: time as UTCTimestamp, value: point.y });
328
361
  }
329
362
  return [...pointsByTime.values()]
330
363
  .sort((a, b) => (a.time as number) - (b.time as number));
@@ -338,6 +371,45 @@ export class DeesChartArea extends DeesElement {
338
371
  return `${index}:${this.getSeriesName(series, index)}`;
339
372
  }
340
373
 
374
+ /**
375
+ * Lightweight Charts joins Area points across whitespace. Render each
376
+ * uninterrupted numeric run as its own Area series instead. All missing
377
+ * timestamps stay on the first real series so the time scale keeps them;
378
+ * there is no synthetic numeric value or extra public legend series.
379
+ */
380
+ private getRenderedSegments(chartSeries: ChartSeriesConfig): IChartRenderedSegment[] {
381
+ const renderedSegments: IChartRenderedSegment[] = [];
382
+ chartSeries.forEach((series, logicalIndex) => {
383
+ const points = this.convertDataToLC(series.data);
384
+ const missingPoints = points.filter((point) => !('value' in point));
385
+ const numericRuns: TChartAreaData[][] = [];
386
+ let currentRun: TChartAreaData[] = [];
387
+ for (const point of points) {
388
+ if ('value' in point) {
389
+ currentRun.push(point);
390
+ } else if (currentRun.length > 0) {
391
+ numericRuns.push(currentRun);
392
+ currentRun = [];
393
+ }
394
+ }
395
+ if (currentRun.length > 0) numericRuns.push(currentRun);
396
+ if (numericRuns.length === 0) numericRuns.push([]);
397
+
398
+ const baseKey = this.getSeriesKey(series, logicalIndex);
399
+ numericRuns.forEach((run, segmentIndex) => {
400
+ const data = segmentIndex === 0
401
+ ? [...run, ...missingPoints].sort((left, right) => left.time - right.time)
402
+ : run;
403
+ renderedSegments.push({
404
+ key: segmentIndex === 0 ? baseKey : `${baseKey}#${segmentIndex}`,
405
+ logicalIndex,
406
+ data,
407
+ });
408
+ });
409
+ });
410
+ return renderedSegments;
411
+ }
412
+
341
413
  private getCanonicalChartSeries(chartSeries: ChartSeriesConfig): ChartSeriesConfig {
342
414
  const cutoffTime = Date.now() - this.rollingWindow;
343
415
  return chartSeries.map((series) => ({
@@ -348,15 +420,34 @@ export class DeesChartArea extends DeesElement {
348
420
  return ms > cutoffTime;
349
421
  })).map((point) => ({
350
422
  x: (point.time as number) * 1000,
351
- y: point.value,
423
+ y: 'value' in point ? point.value : null,
352
424
  })),
353
425
  }));
354
426
  }
355
427
 
356
- private getRenderedSeriesData(api: ISeriesApi<any>): IRealtimeChartPoint[] {
357
- return api.data()
358
- .filter((point): point is { time: UTCTimestamp; value: number } => 'value' in point)
359
- .map((point) => ({ time: point.time as number, value: point.value }));
428
+ private getRenderedSeriesData(segmentKey: string): IRealtimeChartPoint[] {
429
+ const renderedData = this.renderedSeriesData.get(segmentKey);
430
+ if (!renderedData) throw new Error(`Missing rendered chart data for ${segmentKey}`);
431
+ return renderedData;
432
+ }
433
+
434
+ private rememberRenderedSeriesData(segmentKey: string, data: readonly TChartAreaData[]): void {
435
+ this.renderedSeriesData.set(segmentKey, data
436
+ .map((point) => 'value' in point
437
+ ? { time: point.time as number, value: point.value }
438
+ : { time: point.time as number }));
439
+ }
440
+
441
+ private rememberIncrementalUpdate(segmentKey: string, point: IRealtimeChartPoint): void {
442
+ const renderedData = this.getRenderedSeriesData(segmentKey);
443
+ const existingIndex = renderedData.findIndex((entry) => entry.time === point.time);
444
+ if (existingIndex >= 0) renderedData[existingIndex] = point;
445
+ else renderedData.push(point);
446
+ }
447
+
448
+ private toLcPoint(point: IRealtimeChartPoint): TChartAreaData {
449
+ const time = point.time as UTCTimestamp;
450
+ return point.value === undefined ? { time } : { time, value: point.value };
360
451
  }
361
452
 
362
453
  private colorToRgba(color: string, alpha: number): string {
@@ -423,19 +514,21 @@ export class DeesChartArea extends DeesElement {
423
514
  },
424
515
  } as any);
425
516
 
426
- let idx = 0;
427
- for (const [, api] of this.seriesApis) {
428
- const color = this.resolveSeriesColor(this.chartSeries[idx], idx, isDark);
517
+ for (const segment of this.renderedSegments) {
518
+ const api = this.seriesApis.get(segment.key);
519
+ if (!api) continue;
520
+ const color = this.resolveSeriesColor(
521
+ this.chartSeries[segment.logicalIndex], segment.logicalIndex, isDark,
522
+ );
429
523
  api.applyOptions({
430
524
  topColor: this.colorToRgba(color, isDark ? 0.4 : 0.5),
431
525
  bottomColor: this.colorToRgba(color, 0),
432
526
  lineColor: color,
433
527
  });
434
- idx++;
435
528
  }
436
529
  }
437
530
 
438
- private recreateSeries(chartSeries: ChartSeriesConfig) {
531
+ private recreateSeries(chartSeries: ChartSeriesConfig, renderedSegments = this.getRenderedSegments(chartSeries)) {
439
532
  if (!this.chart || !this.lcBundle) return;
440
533
  const isDark = !this.goBright;
441
534
 
@@ -443,10 +536,13 @@ export class DeesChartArea extends DeesElement {
443
536
  this.chart.removeSeries(api);
444
537
  }
445
538
  this.seriesApis.clear();
539
+ this.renderedSeriesData.clear();
446
540
  this.priceLines.clear();
541
+ this.renderedSegments = renderedSegments;
447
542
 
448
- chartSeries.forEach((s, index) => {
449
- const color = this.resolveSeriesColor(s, index, isDark);
543
+ renderedSegments.forEach((segment) => {
544
+ const series = chartSeries[segment.logicalIndex];
545
+ const color = this.resolveSeriesColor(series, segment.logicalIndex, isDark);
450
546
  const api = this.chart!.addSeries(this.lcBundle!.AreaSeries, {
451
547
  topColor: this.colorToRgba(color, isDark ? 0.4 : 0.5),
452
548
  bottomColor: this.colorToRgba(color, 0),
@@ -459,10 +555,12 @@ export class DeesChartArea extends DeesElement {
459
555
  crosshairMarkerRadius: 4,
460
556
  });
461
557
 
462
- api.setData(this.convertDataToLC(s.data));
463
- this.updatePriceLines(this.getSeriesKey(s, index), api, s.data, color);
464
-
465
- this.seriesApis.set(this.getSeriesKey(s, index), api);
558
+ api.setData(segment.data);
559
+ this.seriesApis.set(segment.key, api);
560
+ this.rememberRenderedSeriesData(segment.key, segment.data);
561
+ if (segment.key === this.getSeriesKey(series, segment.logicalIndex)) {
562
+ this.updatePriceLines(segment.key, api, series.data, color);
563
+ }
466
564
  });
467
565
  this.applyYAxisScale(chartSeries);
468
566
  this.computeStats(chartSeries);
@@ -477,17 +575,19 @@ export class DeesChartArea extends DeesElement {
477
575
 
478
576
  private updateSeriesPresentation(chartSeries: ChartSeriesConfig) {
479
577
  const isDark = !this.goBright;
480
- chartSeries.forEach((series, index) => {
481
- const seriesKey = this.getSeriesKey(series, index);
482
- const api = this.seriesApis.get(seriesKey);
578
+ this.renderedSegments.forEach((segment) => {
579
+ const series = chartSeries[segment.logicalIndex];
580
+ const api = this.seriesApis.get(segment.key);
483
581
  if (!api) return;
484
- const color = this.resolveSeriesColor(series, index, isDark);
582
+ const color = this.resolveSeriesColor(series, segment.logicalIndex, isDark);
485
583
  api.applyOptions({
486
584
  topColor: this.colorToRgba(color, isDark ? 0.4 : 0.5),
487
585
  bottomColor: this.colorToRgba(color, 0),
488
586
  lineColor: color,
489
587
  });
490
- this.updatePriceLines(seriesKey, api, series.data, color);
588
+ if (segment.key === this.getSeriesKey(series, segment.logicalIndex)) {
589
+ this.updatePriceLines(segment.key, api, series.data, color);
590
+ }
491
591
  });
492
592
  this.computeStats(chartSeries);
493
593
 
@@ -500,7 +600,7 @@ export class DeesChartArea extends DeesElement {
500
600
  if (this.yAxisScaling === 'dynamic') {
501
601
  maxValue = 1;
502
602
  for (const entry of series) for (const point of entry.data) {
503
- if (!Number.isFinite(point.y)) continue;
603
+ if (point.y === null || !Number.isFinite(point.y)) continue;
504
604
  minValue = Math.min(minValue, Math.floor(point.y * 1.1));
505
605
  maxValue = Math.max(maxValue, Math.ceil(point.y * 1.1));
506
606
  }
@@ -533,22 +633,39 @@ export class DeesChartArea extends DeesElement {
533
633
  ) return;
534
634
 
535
635
  const canonicalSeries = this.getCanonicalChartSeries(this.internalChartData);
536
- canonicalSeries.forEach((series, index) => {
537
- const api = this.seriesApis.get(this.getSeriesKey(series, index));
538
- if (api) api.setData(this.convertDataToLC(series.data));
636
+ const canonicalSegments = this.getRenderedSegments(canonicalSeries);
637
+ if (!haveSameSeriesLayout(
638
+ [...this.seriesApis.keys()], canonicalSegments.map((segment) => segment.key),
639
+ )) {
640
+ this.clearCrosshairForDataReplacement();
641
+ this.internalChartData = canonicalSeries;
642
+ this.recreateSeries(canonicalSeries, canonicalSegments);
643
+ return;
644
+ }
645
+ canonicalSegments.forEach((segment) => {
646
+ const api = this.seriesApis.get(segment.key);
647
+ if (api) {
648
+ api.setData(segment.data);
649
+ this.rememberRenderedSeriesData(segment.key, segment.data);
650
+ }
539
651
  });
652
+ this.internalChartData = canonicalSeries;
653
+ this.renderedSegments = canonicalSegments;
540
654
  this.pendingRealtimePrune = false;
655
+ this.updateSeriesPresentation(canonicalSeries);
541
656
  }
542
657
 
543
658
  private computeStats(chartSeries: ChartSeriesConfig) {
544
659
  const isDark = !this.goBright;
545
660
  this.seriesStats = chartSeries.map((s, index) => {
546
661
  const color = this.resolveSeriesColor(s, index, isDark);
547
- const values = s.data.map(d => d.y);
662
+ const values = s.data
663
+ .map((point) => point.y)
664
+ .filter((value): value is number => value !== null && Number.isFinite(value));
665
+ const latest = s.data.length > 0 ? s.data[s.data.length - 1].y : null;
548
666
  if (values.length === 0) {
549
- return { name: s.name || `series-${index}`, latest: 0, min: 0, max: 0, avg: 0, color };
667
+ return { name: s.name || `series-${index}`, latest: null, min: null, max: null, avg: null, color };
550
668
  }
551
- const latest = values[values.length - 1];
552
669
  const min = Math.min(...values);
553
670
  const max = Math.max(...values);
554
671
  const avg = Math.round((values.reduce((sum, v) => sum + v, 0) / values.length) * 100) / 100;
@@ -556,7 +673,7 @@ export class DeesChartArea extends DeesElement {
556
673
  });
557
674
  }
558
675
 
559
- private updatePriceLines(name: string, api: ISeriesApi<any>, data: Array<{ x: any; y: number }>, color: string) {
676
+ private updatePriceLines(name: string, api: ISeriesApi<any>, data: ChartSeriesConfig[number]['data'], color: string) {
560
677
  // Remove existing price lines for this series
561
678
  const existing = this.priceLines.get(name);
562
679
  if (existing) {
@@ -565,12 +682,14 @@ export class DeesChartArea extends DeesElement {
565
682
  }
566
683
  }
567
684
 
568
- if (data.length === 0) {
685
+ const values = data
686
+ .map((point) => point.y)
687
+ .filter((value): value is number => value !== null && Number.isFinite(value));
688
+ if (values.length === 0) {
569
689
  this.priceLines.set(name, []);
570
690
  return;
571
691
  }
572
692
 
573
- const values = data.map(d => d.y);
574
693
  const avg = values.reduce((sum, v) => sum + v, 0) / values.length;
575
694
  const max = Math.max(...values);
576
695
 
@@ -627,23 +746,24 @@ export class DeesChartArea extends DeesElement {
627
746
  const borderColor = isDark ? 'hsl(0 0% 14.9%)' : 'hsl(0 0% 89.8%)';
628
747
 
629
748
  let html = '';
630
- let idx = 0;
631
749
  let hasData = false;
632
- for (const api of this.seriesApis.values()) {
750
+ for (const segment of this.renderedSegments) {
751
+ const api = this.seriesApis.get(segment.key);
752
+ if (!api) continue;
633
753
  const data = param.seriesData.get(api);
634
- if (data && 'value' in data && (data as any).value !== undefined) {
754
+ if (data && 'value' in data && typeof data.value === 'number') {
755
+ const marginTop = hasData ? '6px' : '0';
635
756
  hasData = true;
636
- const series = this.internalChartData[idx];
637
- const name = series ? this.getSeriesName(series, idx) : `series-${idx}`;
638
- const color = this.resolveSeriesColor(series, idx, isDark);
639
- const formatted = this.yAxisFormatter((data as any).value);
640
- html += `<div style="display:flex;align-items:center;gap:8px;margin:${idx > 0 ? '6px' : '0'} 0;">
757
+ const series = this.internalChartData[segment.logicalIndex];
758
+ const name = this.getSeriesName(series, segment.logicalIndex);
759
+ const color = this.resolveSeriesColor(series, segment.logicalIndex, isDark);
760
+ const formatted = this.yAxisFormatter(data.value);
761
+ html += `<div style="display:flex;align-items:center;gap:8px;margin:${marginTop} 0;">
641
762
  <span style="display:inline-block;width:10px;height:10px;background:${color};border-radius:2px;"></span>
642
763
  <span style="font-weight:500;">${name}:</span>
643
764
  <span style="margin-left:auto;font-weight:600;">${formatted}</span>
644
765
  </div>`;
645
766
  }
646
- idx++;
647
767
  }
648
768
 
649
769
  if (!hasData) {
@@ -786,55 +906,55 @@ export class DeesChartArea extends DeesElement {
786
906
  try {
787
907
  void animate;
788
908
  const canonicalSeries = this.getCanonicalChartSeries(newSeries);
909
+ const renderedSegments = this.getRenderedSegments(canonicalSeries);
789
910
  this.internalChartData = canonicalSeries;
790
911
  const renderedKeys = [...this.seriesApis.keys()];
791
- const canonicalKeys = canonicalSeries.map((series, index) => this.getSeriesKey(series, index));
912
+ const canonicalKeys = renderedSegments.map((segment) => segment.key);
792
913
 
793
914
  if (!haveSameSeriesLayout(renderedKeys, canonicalKeys)) {
794
915
  this.clearCrosshairForDataReplacement();
795
- this.recreateSeries(canonicalSeries);
916
+ this.recreateSeries(canonicalSeries, renderedSegments);
796
917
  this.updateSeriesPresentation(canonicalSeries);
797
918
  return;
798
919
  }
920
+ this.renderedSegments = renderedSegments;
799
921
 
800
922
  if (!this.realtimeMode || this.rollingWindow <= 0) {
801
923
  this.clearCrosshairForDataReplacement();
802
- canonicalSeries.forEach((series, index) => {
803
- this.seriesApis.get(this.getSeriesKey(series, index))!
804
- .setData(this.convertDataToLC(series.data));
924
+ renderedSegments.forEach((segment) => {
925
+ this.seriesApis.get(segment.key)!.setData(segment.data);
926
+ this.rememberRenderedSeriesData(segment.key, segment.data);
805
927
  });
806
928
  this.pendingRealtimePrune = false;
807
929
  this.updateSeriesPresentation(canonicalSeries);
808
930
  return;
809
931
  }
810
932
 
811
- const plannedUpdates = canonicalSeries.map((series, index) => {
812
- const api = this.seriesApis.get(this.getSeriesKey(series, index))!;
813
- const canonicalData = this.convertDataToLC(series.data)
814
- .map((point) => ({ time: point.time as number, value: point.value }));
933
+ const plannedUpdates = renderedSegments.map((segment) => {
934
+ const api = this.seriesApis.get(segment.key)!;
935
+ const canonicalData = segment.data.map((point) => 'value' in point
936
+ ? { time: point.time as number, value: point.value }
937
+ : { time: point.time as number });
815
938
  return {
939
+ key: segment.key,
816
940
  api,
817
941
  canonicalData,
818
- plan: planRealtimeChartUpdates(this.getRenderedSeriesData(api), canonicalData),
942
+ plan: planRealtimeChartUpdates(this.getRenderedSeriesData(segment.key), canonicalData),
819
943
  };
820
944
  });
821
945
 
822
946
  if (plannedUpdates.some(({ plan }) => plan.replacementRequired)) {
823
947
  this.clearCrosshairForDataReplacement();
824
- for (const { api, canonicalData } of plannedUpdates) {
825
- api.setData(canonicalData.map((point) => ({
826
- time: point.time as UTCTimestamp,
827
- value: point.value,
828
- })));
948
+ for (const { key, api, canonicalData } of plannedUpdates) {
949
+ api.setData(canonicalData.map((point) => this.toLcPoint(point)));
950
+ this.renderedSeriesData.set(key, [...canonicalData]);
829
951
  }
830
952
  this.pendingRealtimePrune = false;
831
953
  } else {
832
- for (const { api, plan } of plannedUpdates) {
954
+ for (const { key, api, plan } of plannedUpdates) {
833
955
  for (const update of plan.updates) {
834
- api.update({
835
- time: update.point.time as UTCTimestamp,
836
- value: update.point.value,
837
- }, update.historicalUpdate);
956
+ api.update(this.toLcPoint(update.point), update.historicalUpdate);
957
+ this.rememberIncrementalUpdate(key, update.point);
838
958
  }
839
959
  }
840
960
 
@@ -842,20 +962,16 @@ export class DeesChartArea extends DeesElement {
842
962
  const pruneRecommended = plannedUpdates.some(({ plan }) => plan.pruneRecommended);
843
963
  if (pruneRequired) {
844
964
  this.clearCrosshairForDataReplacement();
845
- for (const { api, canonicalData } of plannedUpdates) {
846
- api.setData(canonicalData.map((point) => ({
847
- time: point.time as UTCTimestamp,
848
- value: point.value,
849
- })));
965
+ for (const { key, api, canonicalData } of plannedUpdates) {
966
+ api.setData(canonicalData.map((point) => this.toLcPoint(point)));
967
+ this.renderedSeriesData.set(key, [...canonicalData]);
850
968
  }
851
969
  } else if (pruneRecommended && this.crosshairActive) {
852
970
  this.pendingRealtimePrune = true;
853
971
  } else if (pruneRecommended) {
854
- for (const { api, canonicalData } of plannedUpdates) {
855
- api.setData(canonicalData.map((point) => ({
856
- time: point.time as UTCTimestamp,
857
- value: point.value,
858
- })));
972
+ for (const { key, api, canonicalData } of plannedUpdates) {
973
+ api.setData(canonicalData.map((point) => this.toLcPoint(point)));
974
+ this.renderedSeriesData.set(key, [...canonicalData]);
859
975
  }
860
976
  this.pendingRealtimePrune = false;
861
977
  }
@@ -879,7 +995,7 @@ export class DeesChartArea extends DeesElement {
879
995
  } catch (e) { /* range may be invalid */ }
880
996
  }
881
997
 
882
- public async appendData(newData: { name?: string; data: Array<{ x: any; y: number }> }[]) {
998
+ public async appendData(newData: ChartSeriesConfig) {
883
999
  if (!this.chart) return;
884
1000
  const nextSeries = this.internalChartData.map((series) => ({
885
1001
  ...series,
@@ -1,6 +1,7 @@
1
1
  export interface IRealtimeChartPoint {
2
2
  time: number;
3
- value: number;
3
+ /** Omitted for a time point where this series has no reading. */
4
+ value?: number;
4
5
  }
5
6
 
6
7
  export interface IRealtimeChartUpdate {
@@ -78,7 +79,8 @@ export const planRealtimeChartUpdates = (
78
79
  continue;
79
80
  }
80
81
 
81
- if (renderedPoint.value !== canonicalPoint.value) {
82
+ if (renderedPoint.value !== canonicalPoint.value
83
+ || ('value' in renderedPoint) !== ('value' in canonicalPoint)) {
82
84
  updates.push({
83
85
  point: canonicalPoint,
84
86
  historicalUpdate: canonicalPoint.time < renderedLastTime,
@@ -14,7 +14,8 @@ export const renderChartArea = (component: DeesChartArea): TemplateResult => {
14
14
  <div class="chartContainer">
15
15
  <div class="rangeSelectionOverlay" aria-hidden="true"></div>
16
16
  </div>
17
- ${!component.chartSeries.some(series => series.data.length) ? html`<div class="chartState" role="status">No data for this period</div>` : ''}
17
+ ${!component.chartSeries.some(series => series.data.some(point => point.y !== null))
18
+ ? html`<div class="chartState" role="status">No data for this period</div>` : ''}
18
19
  ${component.seriesStats.length > 0 && !component.hideLegend ? html`
19
20
  <div slot="footer" class="statsBar">
20
21
  ${component.seriesStats.map(s => html`
@@ -24,7 +25,7 @@ export const renderChartArea = (component: DeesChartArea): TemplateResult => {
24
25
  ${component.legendStats
25
26
  .filter(key => key === 'latest' || key === 'min' || key === 'max' || key === 'avg')
26
27
  .map(key => html`
27
- <span class="statsItem">${key} <strong>${component.yAxisFormatter(s[key])}</strong></span>
28
+ <span class="statsItem">${key} <strong>${s[key] === null ? '—' : component.yAxisFormatter(s[key])}</strong></span>
28
29
  `)}
29
30
  </div>
30
31
  `)}
@@ -19,6 +19,9 @@ declare global {
19
19
  }
20
20
  }
21
21
 
22
+ export interface IDropdownSearchOption { key: string; option: string; payload?: unknown; }
23
+ export type TDropdownSearch = (query: string, signal: AbortSignal) => Promise<IDropdownSearchOption[]>;
24
+
22
25
  @customElement('dees-input-dropdown-popup')
23
26
  export class DeesInputDropdownPopup extends DeesElement {
24
27
  @property({ type: Array })
@@ -39,6 +42,13 @@ export class DeesInputDropdownPopup extends DeesElement {
39
42
  @property({ type: String })
40
43
  accessor selectedKey = '';
41
44
 
45
+ /** Optional remote search. Results are authoritative and are not filtered again locally. */
46
+ @property({ attribute: false }) accessor searchOptions: TDropdownSearch | undefined;
47
+ @property({ type: String }) accessor searchLabel = 'Search options';
48
+ @state() accessor loading = false;
49
+ @state() accessor searchError = '';
50
+ private searchAbort?: AbortController;
51
+
42
52
  @state()
43
53
  accessor filteredOptions: { option: string; key: string; payload?: any }[] = [];
44
54
 
@@ -212,9 +222,9 @@ export class DeesInputDropdownPopup extends DeesElement {
212
222
  <div class="search">
213
223
  <input
214
224
  type="text"
215
- placeholder="Search options..."
225
+ placeholder=${this.searchLabel}
216
226
  role="combobox"
217
- aria-label="Search options"
227
+ aria-label=${this.searchLabel}
218
228
  aria-expanded="true"
219
229
  aria-controls="dropdown-options"
220
230
  aria-activedescendant=${this.filteredOptions.length ? `dropdown-option-${this.highlightedIndex}` : ''}
@@ -227,8 +237,8 @@ export class DeesInputDropdownPopup extends DeesElement {
227
237
  : null}
228
238
  <div class="options-container" id="dropdown-options" role="listbox" aria-label=${this.ownerComponent?.getAttribute('label') || 'Options'}
229
239
  tabindex=${this.enableSearch ? '-1' : '0'} aria-activedescendant=${this.filteredOptions.length ? `dropdown-option-${this.highlightedIndex}` : ''}>
230
- ${this.filteredOptions.length === 0
231
- ? html`<div class="no-options">No options found</div>`
240
+ ${this.loading || this.searchError || this.filteredOptions.length === 0
241
+ ? html`<div class="no-options" role="status">${this.loading ? 'Searching…' : this.searchError || 'No options found'}</div>`
232
242
  : this.filteredOptions.map((option, index) => {
233
243
  const isHighlighted = this.highlightedIndex === index;
234
244
  return html`
@@ -263,6 +273,8 @@ export class DeesInputDropdownPopup extends DeesElement {
263
273
  }
264
274
 
265
275
  public async show(): Promise<void> {
276
+ this.loading = false;
277
+ this.searchError = '';
266
278
  this.filteredOptions = this.options;
267
279
  this.highlightedIndex = Math.max(0, this.options.findIndex((option) => option.key === this.selectedKey));
268
280
  this.searchValue = '';
@@ -287,6 +299,7 @@ export class DeesInputDropdownPopup extends DeesElement {
287
299
  // Add scroll/resize listeners for repositioning
288
300
  window.addEventListener('scroll', this.handleScrollOrResize, { capture: true, passive: true });
289
301
  window.addEventListener('resize', this.handleScrollOrResize, { passive: true });
302
+ if (this.searchOptions) void this.searchRemote();
290
303
  }
291
304
 
292
305
  public async hide(): Promise<void> {
@@ -295,6 +308,7 @@ export class DeesInputDropdownPopup extends DeesElement {
295
308
  return;
296
309
  }
297
310
  this.isDestroying = true;
311
+ this.searchAbort?.abort();
298
312
 
299
313
  // Remove scroll/resize listeners
300
314
  window.removeEventListener('scroll', this.handleScrollOrResize, { capture: true } as EventListenerOptions);
@@ -331,6 +345,7 @@ export class DeesInputDropdownPopup extends DeesElement {
331
345
 
332
346
  public updateOptions(options: { option: string; key: string; payload?: any }[]): void {
333
347
  this.options = options;
348
+ if (this.searchOptions) return;
334
349
  // Re-filter with current search value
335
350
  if (this.searchValue) {
336
351
  const searchLower = this.searchValue.toLowerCase();
@@ -354,6 +369,7 @@ export class DeesInputDropdownPopup extends DeesElement {
354
369
  private handleSearch = (event: Event): void => {
355
370
  const searchTerm = (event.target as HTMLInputElement).value;
356
371
  this.searchValue = searchTerm;
372
+ if (this.searchOptions) { void this.searchRemote(); return; }
357
373
  const searchLower = searchTerm.toLowerCase();
358
374
  this.filteredOptions = this.options.filter((option) =>
359
375
  option.option.toLowerCase().includes(searchLower)
@@ -361,7 +377,25 @@ export class DeesInputDropdownPopup extends DeesElement {
361
377
  this.highlightedIndex = 0;
362
378
  };
363
379
 
380
+ private async searchRemote(): Promise<void> {
381
+ this.searchAbort?.abort();
382
+ const controller = this.searchAbort = new AbortController();
383
+ this.loading = true;
384
+ this.searchError = '';
385
+ this.filteredOptions = [];
386
+ this.highlightedIndex = 0;
387
+ try {
388
+ const options = await this.searchOptions!(this.searchValue, controller.signal);
389
+ if (!controller.signal.aborted) this.filteredOptions = options;
390
+ } catch (error) {
391
+ if (!controller.signal.aborted) this.searchError = error instanceof Error ? error.message : 'Search failed. Try again.';
392
+ } finally {
393
+ if (!controller.signal.aborted) this.loading = false;
394
+ }
395
+ }
396
+
364
397
  private handleSearchKeydown = (event: KeyboardEvent): void => {
398
+ if (event.isComposing) return;
365
399
  const key = event.key;
366
400
  const maxIndex = this.filteredOptions.length - 1;
367
401
 
@@ -370,7 +404,7 @@ export class DeesInputDropdownPopup extends DeesElement {
370
404
  this.highlightedIndex = this.highlightedIndex + 1 > maxIndex ? 0 : this.highlightedIndex + 1;
371
405
  } else if (key === 'ArrowUp') {
372
406
  event.preventDefault();
373
- this.highlightedIndex = this.highlightedIndex - 1 < 0 ? maxIndex : this.highlightedIndex - 1;
407
+ this.highlightedIndex = Math.max(0, this.highlightedIndex - 1 < 0 ? maxIndex : this.highlightedIndex - 1);
374
408
  } else if (key === 'Enter') {
375
409
  event.preventDefault();
376
410
  if (this.filteredOptions[this.highlightedIndex]) {
@@ -391,6 +425,7 @@ export class DeesInputDropdownPopup extends DeesElement {
391
425
  };
392
426
 
393
427
  async disconnectedCallback() {
428
+ this.searchAbort?.abort();
394
429
  window.removeEventListener('scroll', this.handleScrollOrResize, { capture: true } as EventListenerOptions);
395
430
  window.removeEventListener('resize', this.handleScrollOrResize);
396
431
  zIndexRegistry.unregister(this);
@@ -0,0 +1,9 @@
1
+ import { html } from '@design.estate/dees-element';
2
+ import { demoPage, demoSection } from '../../../demos/templates.js';
3
+
4
+ export const demoFunc = () => demoPage('Reference composer', 'Type #_ to search at the cursor. Choose a ticket and continue writing.',
5
+ demoSection('Ticket references', 'References preserve IDs through editing, undo and copy/paste.', html`
6
+ <dees-input-reference .searchReferences=${async (query: string) => [
7
+ { id: 'ticket-1', label: '#CAT-101 Keyboard navigation' }, { id: 'ticket-2', label: '#CAT-102 Ticket detail' },
8
+ ].filter(item => item.label.toLowerCase().includes(query.toLowerCase()))}></dees-input-reference>
9
+ `));