@sentropic/design-system-svelte 0.34.27 → 0.34.32

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.
@@ -25,6 +25,15 @@
25
25
 
26
26
  <script lang="ts">
27
27
  import ChartDataList from "./ChartDataList.svelte";
28
+ import {
29
+ resolveAnnotations,
30
+ annotationDataListItems,
31
+ polygonPoints,
32
+ type ChartAnnotation
33
+ } from "./chartAnnotations.js";
34
+ import { formatDataLabel, normalizeDataLabels, type DataLabelsProp } from "./chartDataLabels.js";
35
+ import { keyForX, resolveActiveIndex } from "./chartCrosshair.js";
36
+ import { datapointAriaLabel, datapointNavAction, rovingTabIndex } from "./chartKeyboardNav.js";
28
37
 
29
38
  type ComboChartProps = {
30
39
  categories: string[];
@@ -42,6 +51,38 @@
42
51
  hiddenSeries?: string[];
43
52
  /** Emitted on click / Enter / Space on a legend item. */
44
53
  onToggleSeries?: (seriesId: string) => void;
54
+ /**
55
+ * Annotation overlay in DATA space. The x coordinate is CATEGORICAL — it
56
+ * matches a category by equality (band centre); the y coordinate (and
57
+ * `value`/`from`/`to`) are LEFT (bar) value-axis numbers. Regions render
58
+ * behind the bars, every other kind above. Additive: absent ⇒ unchanged.
59
+ */
60
+ annotations?: ChartAnnotation[];
61
+ /**
62
+ * Per-datum value labels on BOTH the bars and the line points. `false`/absent
63
+ * (default) → none. `true` → each value with the chart's numeric formatter.
64
+ * Object → `format(value)` and/or a `position` override. Labels are
65
+ * `aria-hidden` — the values already live in the accessible ChartDataList.
66
+ */
67
+ dataLabels?: DataLabelsProp;
68
+ /**
69
+ * CONTROLLED synchronised hover key (FR-3). The key is the CATEGORY string.
70
+ * When provided (string or null), the crosshair tracks this key instead of
71
+ * the chart's internal pointer hover (null ⇒ nothing shown). Absent keeps
72
+ * the legacy uncontrolled behaviour.
73
+ */
74
+ hoverKey?: string | null;
75
+ /** Emitted when the user hovers a bar/point (its CATEGORY) or leaves (`null`). */
76
+ onHoverKeyChange?: (key: string | null) => void;
77
+ /**
78
+ * FR-5 — keyboard navigation of the categories (roving tabindex). When `true`
79
+ * (or implied by wiring `onSelectKey`), a focusable overlay of one column per
80
+ * category is rendered: one tab stop, arrows move, Home/End jump, Enter/Space
81
+ * select, Escape leaves. Absent ⇒ no overlay, rendering unchanged.
82
+ */
83
+ keyboardNav?: boolean;
84
+ /** Emitted on Enter/Space (category) or `null` on Escape. */
85
+ onSelectKey?: (key: string | null) => void;
45
86
  width?: number;
46
87
  height?: number;
47
88
  label: string;
@@ -57,12 +98,21 @@
57
98
  legend = true,
58
99
  hiddenSeries,
59
100
  onToggleSeries,
101
+ annotations,
102
+ dataLabels,
103
+ hoverKey,
104
+ onHoverKeyChange,
105
+ keyboardNav,
106
+ onSelectKey,
60
107
  width = 480,
61
108
  height = 240,
62
109
  label,
63
110
  class: className
64
111
  }: ComboChartProps = $props();
65
112
 
113
+ let focusedIndex: number = $state(-1);
114
+ let datapointRefs: Array<SVGRectElement | null> = [];
115
+
66
116
  // Interactive legend is active as soon as the parent wires either prop.
67
117
  const legendInteractive = $derived(onToggleSeries !== undefined || hiddenSeries !== undefined);
68
118
  const hiddenSet = $derived(new Set(hiddenSeries ?? []));
@@ -263,22 +313,103 @@
263
313
  .flatMap((s) => categories.map((c, ci) => `${s.label}, ${c}: ${s.data[ci] ?? 0}`)),
264
314
  ...lines
265
315
  .filter((s) => !hiddenSet.has(s.label))
266
- .flatMap((s) => categories.map((c, ci) => `${s.label}, ${c}: ${s.data[ci] ?? 0}`))
316
+ .flatMap((s) => categories.map((c, ci) => `${s.label}, ${c}: ${s.data[ci] ?? 0}`)),
317
+ ...annotationDataListItems(annotations)
267
318
  ]);
268
319
 
320
+ // --- Annotation overlay ---------------------------------------------------
321
+ // `xScale` matches a category by equality → its band centre (relative to the
322
+ // plot); `yScale` maps a LEFT (bar) value-axis number. Out-of-domain coords
323
+ // yield null → dropped, so an annotation never escapes the plot.
324
+ const resolvedAnnotations = $derived(
325
+ resolveAnnotations(annotations, {
326
+ xScale: (v: number | string) => {
327
+ const i = categories.indexOf(String(v));
328
+ return i < 0 ? null : bandCenter(i) - MARGIN.left;
329
+ },
330
+ yScale: (v: number) =>
331
+ Number.isFinite(v) ? scaleLinear(v, leftScale.domainMin, leftScale.domainMax, plotHeight, 0) : null,
332
+ plotLeft: MARGIN.left,
333
+ plotTop: MARGIN.top,
334
+ plotWidth,
335
+ plotHeight
336
+ })
337
+ );
338
+ const annotationRegions = $derived(resolvedAnnotations.filter((a) => a.kind === "region"));
339
+ const annotationAbove = $derived(resolvedAnnotations.filter((a) => a.kind !== "region"));
340
+
341
+ // --- Data labels ----------------------------------------------------------
342
+ // One value label per visible bar (outside) + per visible line point (top).
343
+ const dataLabelOpts = $derived(normalizeDataLabels(dataLabels));
344
+ const barDataLabelItems = $derived(
345
+ dataLabelOpts.enabled
346
+ ? barGroups.flatMap((group, gi) =>
347
+ group.map((seg, si) => {
348
+ const inside = dataLabelOpts.position === "inside" || dataLabelOpts.position === "center";
349
+ return {
350
+ key: `bar-${gi}-${si}`,
351
+ x: seg.cx,
352
+ y: inside ? seg.y + seg.height / 2 : seg.cy - 6,
353
+ text: formatDataLabel(seg.value, dataLabelOpts, formatTick),
354
+ baseline: (inside ? "middle" : "auto") as "middle" | "auto"
355
+ };
356
+ })
357
+ )
358
+ : []
359
+ );
360
+ const lineDataLabelItems = $derived(
361
+ dataLabelOpts.enabled
362
+ ? lineSeries.flatMap((series, li) =>
363
+ series.hidden
364
+ ? []
365
+ : series.points.map((p, pi) => {
366
+ const center =
367
+ dataLabelOpts.position === "center" || dataLabelOpts.position === "inside";
368
+ return {
369
+ key: `line-${li}-${pi}`,
370
+ x: p.x,
371
+ y: center ? p.y : p.y - 8,
372
+ text: formatDataLabel(p.value, dataLabelOpts, formatTick),
373
+ baseline: (center ? "middle" : "auto") as "middle" | "auto"
374
+ };
375
+ })
376
+ )
377
+ : []
378
+ );
379
+
380
+ // --- Crosshair + keyboard nav keys (FR-3 / FR-5) --------------------------
381
+ // The shared datum is the CATEGORY: its key is the category string.
382
+ const hoverKeys = $derived(categories.map((c) => keyForX(c)));
383
+ const categorySummary = (ci: number): string =>
384
+ [
385
+ ...bars.filter((s) => !hiddenSet.has(s.label)),
386
+ ...lines.filter((s) => !hiddenSet.has(s.label))
387
+ ]
388
+ .map((s) => {
389
+ const raw = s.data[ci];
390
+ return raw == null || !Number.isFinite(raw) ? null : `${s.label}: ${raw}`;
391
+ })
392
+ .filter((v): v is string => v !== null)
393
+ .join(", ");
394
+
269
395
  type Hover =
270
396
  | { kind: "bar"; gi: number; si: number }
271
397
  | { kind: "line"; li: number; pi: number }
272
398
  | null;
273
- let hovered: Hover = $state(null);
399
+ let hovered = $state<Hover>(null);
274
400
 
401
+ function emitHoverKey(index: number | null) {
402
+ onHoverKeyChange?.(index == null ? null : hoverKeys[index] ?? null);
403
+ }
275
404
  function handleLeave() {
276
405
  hovered = null;
406
+ emitHoverKey(null);
277
407
  }
278
408
  function handleVisualPointerMove(event: PointerEvent) {
279
409
  const target = event.target;
280
410
  if (!(target instanceof Element)) {
281
411
  hovered = null;
412
+ emitHoverKey(null);
282
413
  return;
283
414
  }
284
415
  const kind = target.getAttribute("data-chart-kind");
@@ -286,10 +417,50 @@
286
417
  const b = Number(target.getAttribute("data-chart-b"));
287
418
  if (kind === "bar" && Number.isInteger(a) && Number.isInteger(b)) {
288
419
  hovered = { kind: "bar", gi: a, si: b };
420
+ emitHoverKey(a); // gi === category index
289
421
  } else if (kind === "line" && Number.isInteger(a) && Number.isInteger(b)) {
290
422
  hovered = { kind: "line", li: a, pi: b };
423
+ emitHoverKey(b); // pi === category index
291
424
  } else {
292
425
  hovered = null;
426
+ emitHoverKey(null);
427
+ }
428
+ }
429
+
430
+ // Category index whose crosshair is DISPLAYED: the controlled `hoverKey` when
431
+ // provided (resolved against the category keys), else the internal pointer
432
+ // category (derived from the hovered bar/line datum).
433
+ const internalCategoryIndex = $derived(
434
+ hovered == null ? null : hovered.kind === "bar" ? hovered.gi : hovered.pi
435
+ );
436
+ const activeCategoryIndex = $derived(
437
+ resolveActiveIndex(hoverKey, internalCategoryIndex, hoverKeys)
438
+ );
439
+ const crosshairX = $derived(activeCategoryIndex >= 0 ? bandCenter(activeCategoryIndex) : null);
440
+
441
+ // --- Keyboard navigation (FR-5) ------------------------------------------
442
+ // One focusable transparent column per category carries the roving tab stop.
443
+ const navEnabled = $derived(
444
+ (keyboardNav === true || onSelectKey !== undefined) && categories.length > 0
445
+ );
446
+ function focusDatum(index: number) {
447
+ focusedIndex = index;
448
+ datapointRefs[index]?.focus();
449
+ emitHoverKey(index);
450
+ }
451
+ function handleDatapointKeyDown(event: KeyboardEvent, index: number) {
452
+ const action = datapointNavAction(event.key, index, categories.length);
453
+ if (!action) return;
454
+ event.preventDefault();
455
+ if (action.kind === "move") {
456
+ focusDatum(action.index);
457
+ } else if (action.kind === "select") {
458
+ onSelectKey?.(hoverKeys[index] ?? null);
459
+ } else {
460
+ focusedIndex = -1;
461
+ emitHoverKey(null);
462
+ onSelectKey?.(null);
463
+ (event.currentTarget as SVGElement).blur();
293
464
  }
294
465
  }
295
466
 
@@ -403,6 +574,20 @@
403
574
  </text>
404
575
  {/each}
405
576
 
577
+ <!-- Annotation regions sit BEHIND the bars (filled bands). -->
578
+ {#if annotationRegions.length > 0}
579
+ <g class="st-comboChart__annotations st-comboChart__annotations--behind">
580
+ {#each annotationRegions as a (a.key)}
581
+ {#if a.kind === "region"}
582
+ <rect class="st-comboChart__annotationRegion" x={a.x} y={a.y} width={a.width} height={a.height} />
583
+ {#if a.label}
584
+ <text class="st-comboChart__annotationLabel" x={a.x + 4} y={a.y + 11}>{a.label}</text>
585
+ {/if}
586
+ {/if}
587
+ {/each}
588
+ </g>
589
+ {/if}
590
+
406
591
  <!-- bars -->
407
592
  {#each barGroups as group, gi (gi)}
408
593
  {#each group as seg, si (si)}
@@ -444,7 +629,93 @@
444
629
  {/each}
445
630
  {/if}
446
631
  {/each}
632
+
633
+ <!-- Annotations ABOVE the bars/lines: lines, shapes, points, labels. -->
634
+ {#if annotationAbove.length > 0}
635
+ <g class="st-comboChart__annotations st-comboChart__annotations--above">
636
+ {#each annotationAbove as a (a.key)}
637
+ {#if a.kind === "line"}
638
+ <line class="st-comboChart__annotationLine" x1={a.x1} y1={a.y1} x2={a.x2} y2={a.y2} />
639
+ {#if a.label}
640
+ <text
641
+ class="st-comboChart__annotationLabel"
642
+ x={a.axis === "x" ? a.x1 + 4 : MARGIN.left + plotWidth - 4}
643
+ y={a.axis === "x" ? MARGIN.top + 11 : a.y1 - 4}
644
+ text-anchor={a.axis === "x" ? "start" : "end"}
645
+ >
646
+ {a.label}
647
+ </text>
648
+ {/if}
649
+ {:else if a.kind === "shape"}
650
+ <polygon class="st-comboChart__annotationShape" points={polygonPoints(a.points)} />
651
+ {#if a.label}
652
+ <text class="st-comboChart__annotationLabel" x={a.labelX} y={a.labelY} text-anchor="middle">{a.label}</text>
653
+ {/if}
654
+ {:else if a.kind === "point"}
655
+ <circle class="st-comboChart__annotationPoint" cx={a.x} cy={a.y} r="4.5" />
656
+ {#if a.label}
657
+ <text class="st-comboChart__annotationLabel" x={a.x} y={a.y - 8} text-anchor="middle">{a.label}</text>
658
+ {/if}
659
+ {:else if a.kind === "label"}
660
+ <text class="st-comboChart__annotationText" x={a.x} y={a.y} text-anchor={a.anchor}>{a.text}</text>
661
+ {/if}
662
+ {/each}
663
+ </g>
664
+ {/if}
665
+
666
+ <!-- Data labels — one value per bar + per line point, on top. aria-hidden. -->
667
+ {#if barDataLabelItems.length + lineDataLabelItems.length > 0}
668
+ <g class="st-comboChart__dataLabels" aria-hidden="true">
669
+ {#each barDataLabelItems as d (d.key)}
670
+ <text class="st-comboChart__dataLabel" x={d.x} y={d.y} text-anchor="middle" dominant-baseline={d.baseline}>{d.text}</text>
671
+ {/each}
672
+ {#each lineDataLabelItems as d (d.key)}
673
+ <text class="st-comboChart__dataLabel" x={d.x} y={d.y} text-anchor="middle" dominant-baseline={d.baseline}>{d.text}</text>
674
+ {/each}
675
+ </g>
676
+ {/if}
677
+
678
+ <!-- Crosshair (FR-3) — a tokenised dashed line on the CATEGORY axis at the
679
+ active category. Decorative (aria-hidden). -->
680
+ {#if crosshairX !== null}
681
+ <g class="st-comboChart__crosshair" aria-hidden="true">
682
+ <line class="st-comboChart__crosshairLine" x1={crosshairX} x2={crosshairX} y1={MARGIN.top} y2={MARGIN.top + plotHeight} />
683
+ </g>
684
+ {/if}
447
685
  </svg>
686
+
687
+ <!-- Keyboard navigation overlay (FR-5) — a focusable, transparent column per
688
+ category. NOT aria-hidden: the accessible roving cursor. -->
689
+ {#if navEnabled}
690
+ <svg
691
+ class="st-comboChart__navLayer"
692
+ viewBox="0 0 {width} {height}"
693
+ preserveAspectRatio="xMidYMid meet"
694
+ width="100%"
695
+ height="100%"
696
+ role="group"
697
+ aria-label="{label} — points de données"
698
+ >
699
+ {#each categories as category, ci (ci)}
700
+ <rect
701
+ bind:this={datapointRefs[ci]}
702
+ class="st-comboChart__navDatum"
703
+ x={MARGIN.left + (plotWidth / Math.max(categories.length, 1)) * ci}
704
+ y={MARGIN.top}
705
+ width={plotWidth / Math.max(categories.length, 1)}
706
+ height={plotHeight}
707
+ role="img"
708
+ tabindex={rovingTabIndex(ci, focusedIndex, categories.length)}
709
+ aria-label={datapointAriaLabel(category, categorySummary(ci))}
710
+ onkeydown={(event) => handleDatapointKeyDown(event, ci)}
711
+ onfocus={() => {
712
+ focusedIndex = ci;
713
+ emitHoverKey(ci);
714
+ }}
715
+ />
716
+ {/each}
717
+ </svg>
718
+ {/if}
448
719
  </div>
449
720
 
450
721
  <ChartDataList {label} items={dataValueItems} />
@@ -578,6 +849,66 @@
578
849
  .st-comboChart__dot--category7 { fill: var(--st-semantic-data-category7); }
579
850
  .st-comboChart__dot--category8 { fill: var(--st-semantic-data-category8); }
580
851
 
852
+ /* --- Annotation layer ----------------------------------------------------
853
+ Regions render BEHIND the bars; lines/shapes/points/labels render ABOVE. */
854
+ .st-comboChart__annotationRegion {
855
+ fill: color-mix(in srgb, var(--st-semantic-feedback-info) 12%, transparent);
856
+ stroke: none;
857
+ }
858
+ .st-comboChart__annotationLine {
859
+ stroke: var(--st-semantic-feedback-info);
860
+ stroke-width: 1.5;
861
+ stroke-dasharray: 4 3;
862
+ }
863
+ .st-comboChart__annotationShape {
864
+ fill: color-mix(in srgb, var(--st-semantic-feedback-info) 14%, transparent);
865
+ stroke: var(--st-semantic-feedback-info);
866
+ stroke-width: 1.5;
867
+ }
868
+ .st-comboChart__annotationPoint {
869
+ fill: var(--st-semantic-feedback-info);
870
+ stroke: var(--st-semantic-surface-default);
871
+ stroke-width: 1.5;
872
+ }
873
+ .st-comboChart__annotationLabel,
874
+ .st-comboChart__annotationText {
875
+ fill: var(--st-semantic-text-primary);
876
+ font-size: 0.625rem;
877
+ font-weight: 600;
878
+ }
879
+
880
+ /* Data labels — per-bar + per-point value, drawn on top. Token-only colour. */
881
+ .st-comboChart__dataLabel {
882
+ fill: var(--st-semantic-text-primary);
883
+ font-size: 0.6875rem;
884
+ font-weight: 600;
885
+ }
886
+
887
+ /* --- Crosshair layer (FR-3) ----------------------------------------------
888
+ A tokenised dashed line on the CATEGORY axis at the active category. */
889
+ .st-comboChart__crosshairLine {
890
+ stroke: var(--st-semantic-border-strong);
891
+ stroke-width: 1;
892
+ stroke-dasharray: 3 3;
893
+ opacity: 0.7;
894
+ }
895
+
896
+ /* --- Keyboard navigation layer (FR-5) ------------------------------------
897
+ A focusable, transparent overlay of one column per category. */
898
+ .st-comboChart__navLayer {
899
+ inset: 0;
900
+ position: absolute;
901
+ }
902
+ .st-comboChart__navDatum {
903
+ fill: transparent;
904
+ outline: none;
905
+ }
906
+ .st-comboChart__navDatum:focus-visible {
907
+ fill: color-mix(in srgb, var(--st-semantic-border-interactive) 12%, transparent);
908
+ outline: 2px solid var(--st-semantic-border-interactive);
909
+ outline-offset: 1px;
910
+ }
911
+
581
912
  @media (prefers-reduced-motion: reduce) {
582
913
  .st-comboChart__bar,
583
914
  .st-comboChart__dot {
@@ -10,6 +10,8 @@ export type ComboChartLineSeries = {
10
10
  tone?: ComboChartTone;
11
11
  smooth?: boolean;
12
12
  };
13
+ import { type ChartAnnotation } from "./chartAnnotations.js";
14
+ import { type DataLabelsProp } from "./chartDataLabels.js";
13
15
  type ComboChartProps = {
14
16
  categories: string[];
15
17
  bars?: ComboChartBarSeries[];
@@ -26,6 +28,38 @@ type ComboChartProps = {
26
28
  hiddenSeries?: string[];
27
29
  /** Emitted on click / Enter / Space on a legend item. */
28
30
  onToggleSeries?: (seriesId: string) => void;
31
+ /**
32
+ * Annotation overlay in DATA space. The x coordinate is CATEGORICAL — it
33
+ * matches a category by equality (band centre); the y coordinate (and
34
+ * `value`/`from`/`to`) are LEFT (bar) value-axis numbers. Regions render
35
+ * behind the bars, every other kind above. Additive: absent ⇒ unchanged.
36
+ */
37
+ annotations?: ChartAnnotation[];
38
+ /**
39
+ * Per-datum value labels on BOTH the bars and the line points. `false`/absent
40
+ * (default) → none. `true` → each value with the chart's numeric formatter.
41
+ * Object → `format(value)` and/or a `position` override. Labels are
42
+ * `aria-hidden` — the values already live in the accessible ChartDataList.
43
+ */
44
+ dataLabels?: DataLabelsProp;
45
+ /**
46
+ * CONTROLLED synchronised hover key (FR-3). The key is the CATEGORY string.
47
+ * When provided (string or null), the crosshair tracks this key instead of
48
+ * the chart's internal pointer hover (null ⇒ nothing shown). Absent keeps
49
+ * the legacy uncontrolled behaviour.
50
+ */
51
+ hoverKey?: string | null;
52
+ /** Emitted when the user hovers a bar/point (its CATEGORY) or leaves (`null`). */
53
+ onHoverKeyChange?: (key: string | null) => void;
54
+ /**
55
+ * FR-5 — keyboard navigation of the categories (roving tabindex). When `true`
56
+ * (or implied by wiring `onSelectKey`), a focusable overlay of one column per
57
+ * category is rendered: one tab stop, arrows move, Home/End jump, Enter/Space
58
+ * select, Escape leaves. Absent ⇒ no overlay, rendering unchanged.
59
+ */
60
+ keyboardNav?: boolean;
61
+ /** Emitted on Enter/Space (category) or `null` on Escape. */
62
+ onSelectKey?: (key: string | null) => void;
29
63
  width?: number;
30
64
  height?: number;
31
65
  label: string;
@@ -1 +1 @@
1
- {"version":3,"file":"ComboChart.svelte.d.ts","sourceRoot":"","sources":["../src/lib/ComboChart.svelte.ts"],"names":[],"mappings":"AAGE,MAAM,MAAM,cAAc,GACtB,WAAW,GACX,WAAW,GACX,WAAW,GACX,WAAW,GACX,WAAW,GACX,WAAW,GACX,WAAW,GACX,WAAW,CAAC;AAEhB,MAAM,MAAM,mBAAmB,GAAG;IAChC,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,EAAE,CAAC;IACf,IAAI,CAAC,EAAE,cAAc,CAAC;CACvB,CAAC;AAEF,MAAM,MAAM,oBAAoB,GAAG;IACjC,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,EAAE,CAAC;IACf,IAAI,CAAC,EAAE,cAAc,CAAC;IACtB,MAAM,CAAC,EAAE,OAAO,CAAC;CAClB,CAAC;AAMF,KAAK,eAAe,GAAG;IACrB,UAAU,EAAE,MAAM,EAAE,CAAC;IACrB,IAAI,CAAC,EAAE,mBAAmB,EAAE,CAAC;IAC7B,KAAK,CAAC,EAAE,oBAAoB,EAAE,CAAC;IAC/B,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB;;;;;OAKG;IACH,YAAY,CAAC,EAAE,MAAM,EAAE,CAAC;IACxB,yDAAyD;IACzD,cAAc,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,IAAI,CAAC;IAC5C,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB,CAAC;AA6WJ,QAAA,MAAM,UAAU,qDAAwC,CAAC;AACzD,KAAK,UAAU,GAAG,UAAU,CAAC,OAAO,UAAU,CAAC,CAAC;AAChD,eAAe,UAAU,CAAC"}
1
+ {"version":3,"file":"ComboChart.svelte.d.ts","sourceRoot":"","sources":["../src/lib/ComboChart.svelte.ts"],"names":[],"mappings":"AAGE,MAAM,MAAM,cAAc,GACtB,WAAW,GACX,WAAW,GACX,WAAW,GACX,WAAW,GACX,WAAW,GACX,WAAW,GACX,WAAW,GACX,WAAW,CAAC;AAEhB,MAAM,MAAM,mBAAmB,GAAG;IAChC,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,EAAE,CAAC;IACf,IAAI,CAAC,EAAE,cAAc,CAAC;CACvB,CAAC;AAEF,MAAM,MAAM,oBAAoB,GAAG;IACjC,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,EAAE,CAAC;IACf,IAAI,CAAC,EAAE,cAAc,CAAC;IACtB,MAAM,CAAC,EAAE,OAAO,CAAC;CAClB,CAAC;AAIJ,OAAO,EAIH,KAAK,eAAe,EACrB,MAAM,uBAAuB,CAAC;AACjC,OAAO,EAAwC,KAAK,cAAc,EAAE,MAAM,sBAAsB,CAAC;AAK/F,KAAK,eAAe,GAAG;IACrB,UAAU,EAAE,MAAM,EAAE,CAAC;IACrB,IAAI,CAAC,EAAE,mBAAmB,EAAE,CAAC;IAC7B,KAAK,CAAC,EAAE,oBAAoB,EAAE,CAAC;IAC/B,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB;;;;;OAKG;IACH,YAAY,CAAC,EAAE,MAAM,EAAE,CAAC;IACxB,yDAAyD;IACzD,cAAc,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,IAAI,CAAC;IAC5C;;;;;OAKG;IACH,WAAW,CAAC,EAAE,eAAe,EAAE,CAAC;IAChC;;;;;OAKG;IACH,UAAU,CAAC,EAAE,cAAc,CAAC;IAC5B;;;;;OAKG;IACH,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,kFAAkF;IAClF,gBAAgB,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,KAAK,IAAI,CAAC;IAChD;;;;;OAKG;IACH,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,6DAA6D;IAC7D,WAAW,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,KAAK,IAAI,CAAC;IAC3C,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB,CAAC;AA4jBJ,QAAA,MAAM,UAAU,qDAAwC,CAAC;AACzD,KAAK,UAAU,GAAG,UAAU,CAAC,OAAO,UAAU,CAAC,CAAC;AAChD,eAAe,UAAU,CAAC"}
@@ -1,5 +1,8 @@
1
1
  <script lang="ts" module>
2
2
  import type { Snippet } from "svelte";
3
+ import type { CellDecoration } from "./cellDecoration.js";
4
+
5
+ export type { CellDecoration, CellDecorationIntent } from "./cellDecoration.js";
3
6
 
4
7
  export interface DataTableColumn<R = DataTableRow> {
5
8
  key: string;
@@ -8,6 +11,11 @@
8
11
  align?: "start" | "center" | "end";
9
12
  width?: string;
10
13
  cell?: Snippet<[R, DataTableColumn<R>]>;
14
+ /**
15
+ * Conditional formatting (confort) : décoration sémantique calculée par
16
+ * cellule. Si une `decorations` map est aussi fournie, la map gagne.
17
+ */
18
+ cellDecoration?: (row: R, value: unknown, colId: string) => CellDecoration | null;
11
19
  }
12
20
 
13
21
  export interface DataTableRow {
@@ -25,10 +33,17 @@
25
33
 
26
34
  <script lang="ts">
27
35
  import type { HTMLTableAttributes } from "svelte/elements";
36
+ import { cellDecorationClass, cellDecorationLabel } from "./cellDecoration.js";
37
+ import CellDecorationIcon from "./CellDecorationIcon.svelte";
28
38
 
29
39
  type DataTableProps = Omit<HTMLTableAttributes, "class"> & {
30
40
  columns: DataTableColumn[];
31
41
  rows: DataTableRow[];
42
+ /**
43
+ * Conditional formatting : décorations sémantiques par cellule, indexées
44
+ * `rowId` → `colId` → décoration. Prioritaire sur `column.cellDecoration`.
45
+ */
46
+ decorations?: Record<string, Record<string, CellDecoration>>;
32
47
  caption?: string;
33
48
  size?: "sm" | "md" | "lg";
34
49
  selectable?: DataTableSelectMode;
@@ -53,6 +68,7 @@
53
68
  let {
54
69
  columns,
55
70
  rows,
71
+ decorations,
56
72
  caption,
57
73
  size = "md",
58
74
  selectable = "none",
@@ -191,6 +207,28 @@
191
207
  return String(row[key] ?? "");
192
208
  }
193
209
 
210
+ function resolveDecoration(row: DataTableRow, column: DataTableColumn): CellDecoration | null {
211
+ // La map `decorations` gagne sur le callback `column.cellDecoration`.
212
+ const fromMap = decorations?.[row.id]?.[column.key];
213
+ if (fromMap) return fromMap;
214
+ if (column.cellDecoration) {
215
+ return column.cellDecoration(row, row[column.key], column.key) ?? null;
216
+ }
217
+ return null;
218
+ }
219
+
220
+ function cellClass(column: DataTableColumn, decoration: CellDecoration | null) {
221
+ return (
222
+ [
223
+ alignClass(column.align),
224
+ decoration && "st-cell",
225
+ decoration && cellDecorationClass(decoration.intent),
226
+ ]
227
+ .filter(Boolean)
228
+ .join(" ") || undefined
229
+ );
230
+ }
231
+
194
232
  function goToPage(target: number) {
195
233
  if (target >= 1 && target <= pageCount && target !== safePage) {
196
234
  page = target;
@@ -302,8 +340,24 @@
302
340
  </td>
303
341
  {/if}
304
342
  {#each columns as column (column.key)}
305
- <td class={[alignClass(column.align)].filter(Boolean).join(" ") || undefined}>
306
- {#if column.cell}
343
+ {@const decoration = resolveDecoration(row, column)}
344
+ <td
345
+ class={cellClass(column, decoration)}
346
+ title={decoration ? cellDecorationLabel[decoration.intent] : undefined}
347
+ >
348
+ {#if decoration}
349
+ <span class="st-cell__content">
350
+ <CellDecorationIcon icon={decoration.icon} />
351
+ <span>
352
+ {#if column.cell}
353
+ {@render column.cell(row, column)}
354
+ {:else}
355
+ {cellValue(row, column.key)}
356
+ {/if}
357
+ </span>
358
+ <span class="st-visually-hidden">{cellDecorationLabel[decoration.intent]}</span>
359
+ </span>
360
+ {:else if column.cell}
307
361
  {@render column.cell(row, column)}
308
362
  {:else}
309
363
  {cellValue(row, column.key)}
@@ -426,6 +480,41 @@
426
480
  text-align: end;
427
481
  }
428
482
 
483
+ /* Conditional formatting (« classe Power-BI ») — décoration sémantique de
484
+ cellule. Le fond teinté réutilise le pattern accessible de Badge/Tag
485
+ (color-mix 14% sur token feedback) ; le texte garde le token plein. Le fond
486
+ n'est jamais la seule indication : icône + texte SR accompagnent l'intent. */
487
+ .st-cell__content {
488
+ align-items: center;
489
+ display: inline-flex;
490
+ gap: 0.375rem;
491
+ }
492
+
493
+ .st-cell--intent-positive {
494
+ background: color-mix(in srgb, var(--st-semantic-feedback-success) 14%, white);
495
+ color: var(--st-semantic-feedback-success);
496
+ }
497
+
498
+ .st-cell--intent-negative {
499
+ background: color-mix(in srgb, var(--st-semantic-feedback-error) 14%, white);
500
+ color: var(--st-semantic-feedback-error);
501
+ }
502
+
503
+ .st-cell--intent-warning {
504
+ background: color-mix(in srgb, var(--st-semantic-feedback-warning) 14%, white);
505
+ color: var(--st-semantic-feedback-warning);
506
+ }
507
+
508
+ .st-cell--intent-info {
509
+ background: color-mix(in srgb, var(--st-semantic-feedback-info) 14%, white);
510
+ color: var(--st-semantic-feedback-info);
511
+ }
512
+
513
+ .st-cell--intent-neutral {
514
+ background: var(--st-semantic-surface-subtle);
515
+ color: var(--st-semantic-text-secondary);
516
+ }
517
+
429
518
  .st-dataTable__select {
430
519
  width: 2.5rem;
431
520
  }
@@ -1,4 +1,6 @@
1
1
  import type { Snippet } from "svelte";
2
+ import type { CellDecoration } from "./cellDecoration.js";
3
+ export type { CellDecoration, CellDecorationIntent } from "./cellDecoration.js";
2
4
  export interface DataTableColumn<R = DataTableRow> {
3
5
  key: string;
4
6
  label: string;
@@ -6,6 +8,11 @@ export interface DataTableColumn<R = DataTableRow> {
6
8
  align?: "start" | "center" | "end";
7
9
  width?: string;
8
10
  cell?: Snippet<[R, DataTableColumn<R>]>;
11
+ /**
12
+ * Conditional formatting (confort) : décoration sémantique calculée par
13
+ * cellule. Si une `decorations` map est aussi fournie, la map gagne.
14
+ */
15
+ cellDecoration?: (row: R, value: unknown, colId: string) => CellDecoration | null;
9
16
  }
10
17
  export interface DataTableRow {
11
18
  id: string;
@@ -20,6 +27,11 @@ import type { HTMLTableAttributes } from "svelte/elements";
20
27
  type DataTableProps = Omit<HTMLTableAttributes, "class"> & {
21
28
  columns: DataTableColumn[];
22
29
  rows: DataTableRow[];
30
+ /**
31
+ * Conditional formatting : décorations sémantiques par cellule, indexées
32
+ * `rowId` → `colId` → décoration. Prioritaire sur `column.cellDecoration`.
33
+ */
34
+ decorations?: Record<string, Record<string, CellDecoration>>;
23
35
  caption?: string;
24
36
  size?: "sm" | "md" | "lg";
25
37
  selectable?: DataTableSelectMode;
@@ -1 +1 @@
1
- {"version":3,"file":"DataTable.svelte.d.ts","sourceRoot":"","sources":["../src/lib/DataTable.svelte.ts"],"names":[],"mappings":"AAGE,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,QAAQ,CAAC;AAEtC,MAAM,WAAW,eAAe,CAAC,CAAC,GAAG,YAAY;IAC/C,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,KAAK,CAAC,EAAE,OAAO,GAAG,QAAQ,GAAG,KAAK,CAAC;IACnC,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,IAAI,CAAC,EAAE,OAAO,CAAC,CAAC,CAAC,EAAE,eAAe,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;CACzC;AAED,MAAM,WAAW,YAAY;IAC3B,EAAE,EAAE,MAAM,CAAC;IACX,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;CACxB;AAED,MAAM,MAAM,mBAAmB,GAAG,MAAM,GAAG,QAAQ,GAAG,UAAU,CAAC;AAEjE,MAAM,WAAW,aAAa;IAC5B,GAAG,EAAE,MAAM,CAAC;IACZ,SAAS,EAAE,KAAK,GAAG,MAAM,CAAC;CAC3B;AAGH,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,iBAAiB,CAAC;AAGzD,KAAK,cAAc,GAAG,IAAI,CAAC,mBAAmB,EAAE,OAAO,CAAC,GAAG;IACzD,OAAO,EAAE,eAAe,EAAE,CAAC;IAC3B,IAAI,EAAE,YAAY,EAAE,CAAC;IACrB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,IAAI,CAAC,EAAE,IAAI,GAAG,IAAI,GAAG,IAAI,CAAC;IAC1B,UAAU,CAAC,EAAE,mBAAmB,CAAC;IACjC,WAAW,CAAC,EAAE,MAAM,EAAE,CAAC;IACvB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,MAAM,CAAC,EAAE,aAAa,GAAG,IAAI,CAAC;IAC9B,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,UAAU,CAAC,EAAE,CAAC,KAAK,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,GAAG,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,KAAK,MAAM,CAAC;IAC9E,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,UAAU,CAAC,EAAE,CAAC,GAAG,EAAE,YAAY,KAAK,IAAI,CAAC;IACzC,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB,CAAC;AAiQJ,QAAA,MAAM,SAAS,mFAAwC,CAAC;AACxD,KAAK,SAAS,GAAG,UAAU,CAAC,OAAO,SAAS,CAAC,CAAC;AAC9C,eAAe,SAAS,CAAC"}
1
+ {"version":3,"file":"DataTable.svelte.d.ts","sourceRoot":"","sources":["../src/lib/DataTable.svelte.ts"],"names":[],"mappings":"AAGE,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,QAAQ,CAAC;AACtC,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AAE1D,YAAY,EAAE,cAAc,EAAE,oBAAoB,EAAE,MAAM,qBAAqB,CAAC;AAEhF,MAAM,WAAW,eAAe,CAAC,CAAC,GAAG,YAAY;IAC/C,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,KAAK,CAAC,EAAE,OAAO,GAAG,QAAQ,GAAG,KAAK,CAAC;IACnC,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,IAAI,CAAC,EAAE,OAAO,CAAC,CAAC,CAAC,EAAE,eAAe,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;IACxC;;;OAGG;IACH,cAAc,CAAC,EAAE,CAAC,GAAG,EAAE,CAAC,EAAE,KAAK,EAAE,OAAO,EAAE,KAAK,EAAE,MAAM,KAAK,cAAc,GAAG,IAAI,CAAC;CACnF;AAED,MAAM,WAAW,YAAY;IAC3B,EAAE,EAAE,MAAM,CAAC;IACX,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;CACxB;AAED,MAAM,MAAM,mBAAmB,GAAG,MAAM,GAAG,QAAQ,GAAG,UAAU,CAAC;AAEjE,MAAM,WAAW,aAAa;IAC5B,GAAG,EAAE,MAAM,CAAC;IACZ,SAAS,EAAE,KAAK,GAAG,MAAM,CAAC;CAC3B;AAGH,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,iBAAiB,CAAC;AAKzD,KAAK,cAAc,GAAG,IAAI,CAAC,mBAAmB,EAAE,OAAO,CAAC,GAAG;IACzD,OAAO,EAAE,eAAe,EAAE,CAAC;IAC3B,IAAI,EAAE,YAAY,EAAE,CAAC;IACrB;;;OAGG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,cAAc,CAAC,CAAC,CAAC;IAC7D,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,IAAI,CAAC,EAAE,IAAI,GAAG,IAAI,GAAG,IAAI,CAAC;IAC1B,UAAU,CAAC,EAAE,mBAAmB,CAAC;IACjC,WAAW,CAAC,EAAE,MAAM,EAAE,CAAC;IACvB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,MAAM,CAAC,EAAE,aAAa,GAAG,IAAI,CAAC;IAC9B,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,UAAU,CAAC,EAAE,CAAC,KAAK,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,GAAG,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,KAAK,MAAM,CAAC;IAC9E,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,UAAU,CAAC,EAAE,CAAC,GAAG,EAAE,YAAY,KAAK,IAAI,CAAC;IACzC,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB,CAAC;AAuSJ,QAAA,MAAM,SAAS,mFAAwC,CAAC;AACxD,KAAK,SAAS,GAAG,UAAU,CAAC,OAAO,SAAS,CAAC,CAAC;AAC9C,eAAe,SAAS,CAAC"}