@design.estate/dees-catalog 12.5.0 → 13.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (32) hide show
  1. package/dist_bundle/bundle.js +340 -177
  2. package/dist_bundle/bundle.js.map +1 -1
  3. package/dist_ts_web/00_commitinfo_data.js +1 -1
  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-reference/dees-input-reference.js +4 -4
  10. package/dist_ts_web/elements/00group-kanban/dees-kanban/dees-kanban.js +32 -18
  11. package/dist_ts_web/elements/00group-kanban/dees-kanban-card/dees-kanban-card.js +37 -21
  12. package/dist_ts_web/elements/00group-kanban/dees-kanban-column/dees-kanban-column.js +16 -12
  13. package/dist_ts_web/elements/00group-kanban/dees-kanban-discussion/dees-kanban-discussion.js +26 -15
  14. package/dist_ts_web/elements/00group-kanban/dees-kanban-ticket-detail/dees-kanban-ticket-detail.d.ts +2 -0
  15. package/dist_ts_web/elements/00group-kanban/dees-kanban-ticket-detail/dees-kanban-ticket-detail.js +73 -40
  16. package/dist_ts_web/elements/00group-kanban/kanban.helpers.d.ts +3 -1
  17. package/dist_ts_web/elements/00group-kanban/kanban.helpers.js +3 -1
  18. package/dist_ts_web/elements/00group-kanban/kanban.styles.js +11 -3
  19. package/package.json +1 -1
  20. package/readme.md +19 -1
  21. package/ts_web/00_commitinfo_data.ts +1 -1
  22. package/ts_web/elements/00group-chart/dees-chart-area/component.ts +195 -79
  23. package/ts_web/elements/00group-chart/dees-chart-area/realtime-updates.ts +4 -2
  24. package/ts_web/elements/00group-chart/dees-chart-area/template.ts +3 -2
  25. package/ts_web/elements/00group-input/dees-input-reference/dees-input-reference.ts +3 -3
  26. package/ts_web/elements/00group-kanban/dees-kanban/dees-kanban.ts +31 -17
  27. package/ts_web/elements/00group-kanban/dees-kanban-card/dees-kanban-card.ts +36 -20
  28. package/ts_web/elements/00group-kanban/dees-kanban-column/dees-kanban-column.ts +15 -11
  29. package/ts_web/elements/00group-kanban/dees-kanban-discussion/dees-kanban-discussion.ts +25 -14
  30. package/ts_web/elements/00group-kanban/dees-kanban-ticket-detail/dees-kanban-ticket-detail.ts +64 -38
  31. package/ts_web/elements/00group-kanban/kanban.helpers.ts +5 -1
  32. package/ts_web/elements/00group-kanban/kanban.styles.ts +10 -2
@@ -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
  `)}
@@ -35,18 +35,18 @@ export class DeesInputReference extends DeesElement {
35
35
  :host { display: block; min-width: 0; color: var(--dees-color-text-primary); font-family: var(--dees-font-family); }
36
36
  .editor { border: 1px solid var(--dees-color-border-default); border-radius: var(--dees-radius-lg); background: var(--dees-color-bg-primary); }
37
37
  .editor:focus-within { outline: 2px solid var(--dees-color-focus-ring); outline-offset: 1px; }
38
- .ProseMirror { padding: 12px; min-height: 84px; max-height: 260px; overflow: auto; outline: none; white-space: pre-wrap; overflow-wrap: anywhere; font-size: var(--dees-font-body-size); line-height: 1.6; }
38
+ .ProseMirror { padding: 12px; min-height: var(--dees-reference-min-height, 84px); max-height: 260px; overflow: auto; outline: none; white-space: pre-wrap; overflow-wrap: anywhere; font-size: var(--dees-reference-font-size, var(--dees-font-body-size)); line-height: 1.6; }
39
39
  .ProseMirror p { margin: 0; }
40
40
  .ProseMirror p + p { margin-top: 0; }
41
41
  .ProseMirror:has(> p:only-child > br:only-child)::before { content: attr(data-placeholder); float: left; color: var(--dees-color-text-muted); pointer-events: none; height: 0; }
42
- .ProseMirror [data-reference-id] { display: inline; padding: 1px 5px; border-radius: var(--dees-radius-sm); color: var(--dees-color-link); background: var(--dees-color-badge-default-bg); cursor: pointer; font-weight: 500; }
42
+ .ProseMirror [data-reference-id] { display: inline; padding: 1px 5px; border-radius: var(--dees-radius-sm); color: var(--dees-color-link); background: var(--dees-color-fill-tertiary); cursor: pointer; font-weight: 500; }
43
43
  .ProseMirror [data-reference-id]:focus-visible, .ProseMirror .ProseMirror-selectednode { outline: 2px solid var(--dees-color-focus-ring); }
44
44
  .ProseMirror[contenteditable=false] { opacity: .65; }
45
45
  .error { color: var(--dees-color-text-error); font-size: var(--dees-font-footnote-size); }
46
46
  `];
47
47
 
48
48
  public render() {
49
- return html`<div class="editor"></div>${this.loadError ? html`<p class="error" role="alert">${this.loadError}</p>` : ''}`;
49
+ return html`<div class="editor" part="editor"></div>${this.loadError ? html`<p class="error" role="alert">${this.loadError}</p>` : ''}`;
50
50
  }
51
51
 
52
52
  public async connectedCallback(): Promise<void> {
@@ -63,14 +63,27 @@ export class DeesKanban extends DeesElement {
63
63
  private resolvedTickets = new Map<string, IKanbanTicket>();
64
64
 
65
65
  public static styles = [...kanbanStyles, css`
66
- :host { padding: 24px; background: var(--dees-color-bg-canvas); min-height: 100%; }
67
- .heading { display: flex; gap: 12px; align-items: center; flex-wrap: wrap; margin-bottom: 20px; }
68
- h2 { font-size: var(--dees-font-title1-size); font-weight: 650; letter-spacing: -.02em; }
69
- .filters { display: flex; align-items: end; gap: 16px; flex-wrap: wrap; margin-bottom: 24px; }
70
- .filters dees-input-text { flex: 1; min-width: 200px; max-width: 360px; }
71
- .filters dees-input-dropdown { min-width: 200px; max-width: 320px; flex: 1; }
72
- .columns { display: flex; gap: 16px; overflow-x: auto; padding: 3px 3px 16px; align-items: stretch; }
73
- @media (max-width: 600px) { :host { padding: 16px; } .filters { gap: 10px; } .columns { scroll-snap-type: x proximity; } dees-kanban-column { scroll-snap-align: start; flex-basis: 85vw; } }
66
+ :host { padding: 20px 24px; background: var(--dees-color-bg-canvas); min-height: 100%; }
67
+ .heading { display: flex; gap: 12px; align-items: center; flex-wrap: wrap; margin-bottom: 16px; }
68
+ .identity { display: flex; gap: 12px; align-items: baseline; flex: 1; min-width: 220px; flex-wrap: wrap; }
69
+ h2 { font-size: var(--dees-font-title3-size); font-weight: 600; letter-spacing: -.02em; line-height: 1.3; overflow-wrap: anywhere; }
70
+ .totals { font-size: var(--dees-font-caption1-size); color: var(--dees-color-text-secondary); white-space: nowrap; }
71
+ .filters { display: flex; align-items: center; gap: 8px; }
72
+ .filters dees-input-text { width: 230px; min-width: 140px; }
73
+ .filters dees-input-dropdown { width: 180px; min-width: 140px; }
74
+ .actions { display: flex; gap: 8px; margin-left: auto; }
75
+ .columns { display: flex; gap: 20px; overflow-x: auto; padding: 3px 2px 16px; align-items: stretch; }
76
+ @media (max-width: 1100px) { .filters { order: 3; flex-basis: 100%; } .filters dees-input-text { flex: 1; max-width: 320px; } }
77
+ @media (max-width: 600px) {
78
+ :host { padding: 12px 16px; }
79
+ .heading { gap: 12px 8px; }
80
+ .identity { min-width: 100%; gap: 6px 10px; }
81
+ .actions { margin-left: 0; }
82
+ .filters { flex-wrap: wrap; }
83
+ .filters dees-input-text, .filters dees-input-dropdown { flex: 1; width: auto; min-width: 130px; }
84
+ .columns { margin: 0 -16px; padding: 3px 16px 16px; scroll-snap-type: x proximity; scroll-padding-inline: 16px; gap: 14px; }
85
+ dees-kanban-column { scroll-snap-align: start; flex: 0 0 86%; min-width: 0; max-width: none; }
86
+ }
74
87
  `];
75
88
 
76
89
  public async connectedCallback(): Promise<void> {
@@ -86,18 +99,18 @@ export class DeesKanban extends DeesElement {
86
99
  (!query || [ticket.key, ticket.title, ticket.assignee?.name, ...ticket.labels ?? [],
87
100
  ...this.tickets.filter(child => child.parentId === ticket.id).map(child => `${child.key} ${child.title}`)].join(' ').toLocaleLowerCase().includes(query)));
88
101
  const epic = epics.find(ticket => ticket.id === this.epicId);
89
- return html`<header class="heading"><h2>${this.heading}</h2><span class="caption">${tasks.length} tasks · ${epics.length} epics</span><span class="spacer"></span>
90
- ${this.provider.createTicket ? html`<dees-button type="ghost" size="sm" text="New epic" .disabled=${!this.columns.length}
91
- @clicked=${() => { void this.createTicket({ kind: 'epic', columnId: this.columns[0].id }); }}></dees-button>
92
- <dees-button type="accent" size="sm" icon="lucide:Plus" text="New task" .disabled=${!this.columns.length}
93
- @clicked=${() => { void this.createTicket({ kind: 'task', columnId: this.columns[0].id, parentId: this.epicId || undefined }); }}></dees-button>` : ''}</header>
94
- <div class="filters"><dees-input-text label="Search tickets" placeholder="Title, ticket number, assignee…" .value=${this.query}
102
+ return html`<header class="heading"><div class="identity"><h2>${this.heading}</h2><span class="totals">${tasks.length} tasks · ${epics.length} epics</span></div>
103
+ <div class="filters"><dees-input-text label="Search tickets" .labelPosition=${'none'} placeholder="Search tickets…" .value=${this.query}
95
104
  @input=${(event: Event) => { this.query = String((event.currentTarget as DeesInputText).value ?? ''); }}></dees-input-text>
96
- <dees-input-dropdown label="Epic" .value=${{ key: this.epicId, option: epic?.title ?? 'All epics' }}
105
+ <dees-input-dropdown label="Epic" .labelPosition=${'none'} .value=${{ key: this.epicId, option: epic?.title ?? 'All epics' }}
97
106
  .options=${[{ key: '', option: 'All epics' }, ...epics.map(ticket => ({ key: ticket.id, option: ticket.title }))]}
98
107
  @selectedOption=${(event: CustomEvent<{ key: string }>) => { this.epicId = event.detail.key; }}></dees-input-dropdown>
99
- ${epic ? html`<dees-button type="ghost" size="sm" text="Open epic" @clicked=${() => { void this.openTicket(epic.id); }}></dees-button>` : ''}
108
+ ${epic ? html`<dees-button type="ghost" text="Open epic" @clicked=${() => { void this.openTicket(epic.id); }}></dees-button>` : ''}
100
109
  </div>
110
+ ${this.provider.createTicket ? html`<div class="actions"><dees-button type="secondary" text="New epic" .disabled=${!this.columns.length}
111
+ @clicked=${() => { void this.createTicket({ kind: 'epic', columnId: this.columns[0].id }); }}></dees-button>
112
+ <dees-button type="accent" icon="lucide:Plus" text="New task" .disabled=${!this.columns.length}
113
+ @clicked=${() => { void this.createTicket({ kind: 'task', columnId: this.columns[0].id, parentId: this.epicId || undefined }); }}></dees-button></div>` : ''}</header>
101
114
  ${this.loading || this.openingTicket ? html`<p class="caption" role="status">${this.loading ? 'Loading board…' : 'Opening ticket…'}</p>` : ''}
102
115
  ${this.error || this.operationError ? html`<p class="error" role="alert">${this.error || this.operationError}</p>` : ''}
103
116
  ${!this.columns.length ? html`<p class="empty">No columns configured.</p>` : html`<div class="columns" aria-label=${this.heading}>
@@ -131,6 +144,7 @@ export class DeesKanban extends DeesElement {
131
144
  this.selectedId = id;
132
145
  if (!this.detail) {
133
146
  this.detail = new DeesKanbanTicketDetail();
147
+ this.detail.showTicketKey = false;
134
148
  for (const name of interactionEvents) this.detail.addEventListener(name, this.handleInteraction);
135
149
  }
136
150
  this.detail.ticket = ticket;
@@ -138,7 +152,7 @@ export class DeesKanban extends DeesElement {
138
152
  this.syncDetail();
139
153
  if (!this.modal && !this.modalOpening) {
140
154
  this.modalOpening = DeesModal.createAndShow({ heading: ticket.key, width: 1000, maxWidth: 1040,
141
- mobileFullscreen: true, content: html`${this.detail}`, menuOptions: [] });
155
+ mobileFullscreen: true, contentPadding: 0, content: html`${this.detail}`, menuOptions: [] });
142
156
  const modal = await this.modalOpening;
143
157
  this.modalOpening = undefined;
144
158
  if (!this.isConnected) { await modal.destroy(); return; }