@sentropic/design-system-svelte 0.17.0 → 0.19.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 (41) hide show
  1. package/dist/BoxPlotChart.svelte +302 -0
  2. package/dist/BoxPlotChart.svelte.d.ts +40 -0
  3. package/dist/BoxPlotChart.svelte.d.ts.map +1 -0
  4. package/dist/Calendar.svelte +237 -42
  5. package/dist/Calendar.svelte.d.ts.map +1 -1
  6. package/dist/HeatmapChart.svelte +337 -0
  7. package/dist/HeatmapChart.svelte.d.ts +35 -0
  8. package/dist/HeatmapChart.svelte.d.ts.map +1 -0
  9. package/dist/HistogramChart.svelte +294 -0
  10. package/dist/HistogramChart.svelte.d.ts +38 -0
  11. package/dist/HistogramChart.svelte.d.ts.map +1 -0
  12. package/dist/Popper.svelte +157 -0
  13. package/dist/Popper.svelte.d.ts +17 -0
  14. package/dist/Popper.svelte.d.ts.map +1 -1
  15. package/dist/RadarChart.svelte +340 -0
  16. package/dist/RadarChart.svelte.d.ts +43 -0
  17. package/dist/RadarChart.svelte.d.ts.map +1 -0
  18. package/dist/Rating.svelte +130 -35
  19. package/dist/Rating.svelte.d.ts.map +1 -1
  20. package/dist/SankeyChart.svelte +364 -0
  21. package/dist/SankeyChart.svelte.d.ts +45 -0
  22. package/dist/SankeyChart.svelte.d.ts.map +1 -0
  23. package/dist/SelectableList.svelte +60 -12
  24. package/dist/SelectableList.svelte.d.ts.map +1 -1
  25. package/dist/SelectableRow.svelte +23 -8
  26. package/dist/SelectableRow.svelte.d.ts +5 -4
  27. package/dist/SelectableRow.svelte.d.ts.map +1 -1
  28. package/dist/SlideIndicator.svelte +17 -3
  29. package/dist/SlideIndicator.svelte.d.ts.map +1 -1
  30. package/dist/SunburstChart.svelte +388 -0
  31. package/dist/SunburstChart.svelte.d.ts +39 -0
  32. package/dist/SunburstChart.svelte.d.ts.map +1 -0
  33. package/dist/TimePicker.svelte +176 -13
  34. package/dist/TimePicker.svelte.d.ts.map +1 -1
  35. package/dist/chartContrast.d.ts +0 -4
  36. package/dist/chartContrast.d.ts.map +1 -1
  37. package/dist/chartContrast.js +4 -56
  38. package/dist/index.d.ts +12 -0
  39. package/dist/index.d.ts.map +1 -1
  40. package/dist/index.js +6 -0
  41. package/package.json +1 -1
@@ -0,0 +1 @@
1
+ {"version":3,"file":"HistogramChart.svelte.d.ts","sourceRoot":"","sources":["../src/lib/HistogramChart.svelte.ts"],"names":[],"mappings":"AAGE;;;;;;;;;;;;;;;;;;;GAmBG;AACH,MAAM,MAAM,kBAAkB,GAC1B,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,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,kBAAkB,CAAC;CAC3B,CAAC;AAMF,KAAK,mBAAmB,GAAG;IACzB,IAAI,EAAE,mBAAmB,EAAE,GAAG,MAAM,EAAE,CAAC;IACvC,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB,CAAC;AA4JJ,QAAA,MAAM,cAAc,yDAAwC,CAAC;AAC7D,KAAK,cAAc,GAAG,UAAU,CAAC,OAAO,cAAc,CAAC,CAAC;AACxD,eAAe,cAAc,CAAC"}
@@ -44,6 +44,23 @@
44
44
  class?: string;
45
45
  /** Notified whenever the resolved placement changes (after flip). */
46
46
  onPlacementChange?: (placement: PopperPlacement) => void;
47
+ /**
48
+ * (a11y, opt-in) When true, traps keyboard focus inside the panel while it
49
+ * is open (Tab cycles through focusable children; Shift+Tab cycles backward).
50
+ * Intended for modal overlays built on top of Popper. Non-modal usage (menus,
51
+ * tooltips) should leave this false (default) to keep the natural tab order.
52
+ */
53
+ trapFocus?: boolean;
54
+ /**
55
+ * (a11y) When true (default when open), pressing Escape calls `onClose` so
56
+ * the consumer can set `open = false`. Set to false to suppress this behavior.
57
+ */
58
+ closeOnEscape?: boolean;
59
+ /**
60
+ * Called when the panel requests closing (Escape key, or future outside-click).
61
+ * The consumer is responsible for updating `open` in response.
62
+ */
63
+ onClose?: () => void;
47
64
  children?: Snippet;
48
65
  };
49
66
 
@@ -158,6 +175,8 @@
158
175
  </script>
159
176
 
160
177
  <script lang="ts">
178
+ import { untrack } from "svelte";
179
+
161
180
  let {
162
181
  anchor,
163
182
  open = false,
@@ -170,6 +189,9 @@
170
189
  portal = true,
171
190
  class: className,
172
191
  onPlacementChange,
192
+ trapFocus = false,
193
+ closeOnEscape = true,
194
+ onClose,
173
195
  children
174
196
  }: PopperProps = $props();
175
197
 
@@ -243,17 +265,152 @@
243
265
  };
244
266
  });
245
267
 
268
+ // ─── a11y: focus management ────────────────────────────────────────────────
269
+ // Capture the element that had focus before the panel opened so we can
270
+ // restore it when the panel closes (only when trapFocus is active).
271
+ // SSR-safe (guarded by typeof window).
272
+ // Use a plain variable (not $state) so reads inside the effect are not
273
+ // tracked and don't cause the effect to re-run on every preFocusEl write.
274
+ let preFocusEl: HTMLElement | null = null;
275
+
276
+ $effect(() => {
277
+ if (typeof window === "undefined") return;
278
+ if (open && anchor) {
279
+ untrack(() => {
280
+ // Snapshot the active element at open time only when trapFocus is active,
281
+ // so restoreFocus doesn't steal focus on non-modal usage.
282
+ if (trapFocus) {
283
+ preFocusEl = document.activeElement as HTMLElement | null;
284
+ }
285
+ });
286
+ } else {
287
+ // Panel just closed: restore focus only if trapFocus was active and the
288
+ // original element is still connected to the DOM.
289
+ untrack(() => {
290
+ if (
291
+ trapFocus &&
292
+ preFocusEl &&
293
+ typeof preFocusEl.focus === "function" &&
294
+ document.contains(preFocusEl)
295
+ ) {
296
+ preFocusEl.focus();
297
+ }
298
+ preFocusEl = null;
299
+ });
300
+ }
301
+ });
302
+
303
+ /** Returns all keyboard-focusable children of `container`. */
304
+ function getFocusable(container: HTMLElement): HTMLElement[] {
305
+ if (typeof window === "undefined") return [];
306
+ return Array.from(
307
+ container.querySelectorAll<HTMLElement>(
308
+ 'a[href],button:not([disabled]),input:not([disabled]),select:not([disabled]),' +
309
+ 'textarea:not([disabled]),[tabindex]:not([tabindex="-1"]),[contenteditable="true"]'
310
+ )
311
+ ).filter((el) => !el.closest("[disabled]"));
312
+ }
313
+
314
+ // ─── a11y: move focus into the panel on open (trapFocus only) ─────────────
315
+ $effect(() => {
316
+ if (typeof window === "undefined") return;
317
+ if (!open || !trapFocus || !panel) return;
318
+ // Wait for the panel to be mounted before moving focus.
319
+ untrack(() => {
320
+ const focusable = getFocusable(panel!);
321
+ if (focusable.length > 0) {
322
+ focusable[0].focus();
323
+ } else {
324
+ // If no focusable children, focus the panel itself so Escape works.
325
+ panel!.focus();
326
+ }
327
+ });
328
+ });
329
+
330
+ // ─── a11y: recapture focus that escapes the trap (focusin on document) ─────
331
+ $effect(() => {
332
+ if (typeof window === "undefined") return;
333
+ if (!open || !trapFocus) return;
334
+
335
+ function handleFocusIn(e: FocusEvent) {
336
+ if (!panel) return;
337
+ const target = e.target as Node | null;
338
+ // If focus landed outside the panel, bring it back to the first focusable.
339
+ if (target && !panel.contains(target)) {
340
+ const focusable = getFocusable(panel);
341
+ if (focusable.length > 0) {
342
+ focusable[0].focus();
343
+ } else {
344
+ panel.focus();
345
+ }
346
+ }
347
+ }
348
+
349
+ document.addEventListener("focusin", handleFocusIn);
350
+ return () => {
351
+ document.removeEventListener("focusin", handleFocusIn);
352
+ };
353
+ });
354
+
355
+ // ─── a11y: Escape on document (closeOnEscape) ─────────────────────────────
356
+ // Listen on document so the Escape shortcut works even when focus is outside
357
+ // the panel (e.g. before the first Tab, or in an edge case where focus escaped).
358
+ $effect(() => {
359
+ if (typeof window === "undefined") return;
360
+ if (!open || !closeOnEscape) return;
361
+
362
+ function handleDocKeydown(e: KeyboardEvent) {
363
+ if (e.key === "Escape") {
364
+ e.preventDefault();
365
+ onClose?.();
366
+ }
367
+ }
368
+
369
+ document.addEventListener("keydown", handleDocKeydown);
370
+ return () => {
371
+ document.removeEventListener("keydown", handleDocKeydown);
372
+ };
373
+ });
374
+
375
+ /** Keyboard handler attached to the panel for Tab-trap cycling. */
376
+ function handlePanelKeydown(e: KeyboardEvent) {
377
+ if (typeof window === "undefined") return;
378
+
379
+ // Tab trap: cycle focus within the panel.
380
+ if (e.key === "Tab" && trapFocus && panel) {
381
+ const focusable = getFocusable(panel);
382
+ if (focusable.length === 0) { e.preventDefault(); return; }
383
+ const first = focusable[0];
384
+ const last = focusable[focusable.length - 1];
385
+ if (e.shiftKey) {
386
+ if (document.activeElement === first) {
387
+ e.preventDefault();
388
+ last.focus();
389
+ }
390
+ } else {
391
+ if (document.activeElement === last) {
392
+ e.preventDefault();
393
+ first.focus();
394
+ }
395
+ }
396
+ }
397
+ }
398
+
246
399
  const panelStyle = () =>
247
400
  `position: ${strategy}; top: ${top}px; left: ${left}px;`;
248
401
  const panelSide = () => splitPlacement(resolvedPlacement).side;
249
402
  </script>
250
403
 
251
404
  {#snippet floating()}
405
+ <!-- svelte-ignore a11y_no_noninteractive_element_interactions -->
406
+ <!-- svelte-ignore a11y_no_static_element_interactions -->
252
407
  <div
253
408
  bind:this={panel}
254
409
  class={className ? `st-popper ${className}` : "st-popper"}
255
410
  data-popper-placement={resolvedPlacement}
256
411
  style={panelStyle()}
412
+ tabindex={trapFocus ? -1 : undefined}
413
+ onkeydown={trapFocus ? handlePanelKeydown : undefined}
257
414
  >
258
415
  {@render children?.()}
259
416
  {#if arrow}
@@ -26,6 +26,23 @@ export type PopperProps = {
26
26
  class?: string;
27
27
  /** Notified whenever the resolved placement changes (after flip). */
28
28
  onPlacementChange?: (placement: PopperPlacement) => void;
29
+ /**
30
+ * (a11y, opt-in) When true, traps keyboard focus inside the panel while it
31
+ * is open (Tab cycles through focusable children; Shift+Tab cycles backward).
32
+ * Intended for modal overlays built on top of Popper. Non-modal usage (menus,
33
+ * tooltips) should leave this false (default) to keep the natural tab order.
34
+ */
35
+ trapFocus?: boolean;
36
+ /**
37
+ * (a11y) When true (default when open), pressing Escape calls `onClose` so
38
+ * the consumer can set `open = false`. Set to false to suppress this behavior.
39
+ */
40
+ closeOnEscape?: boolean;
41
+ /**
42
+ * Called when the panel requests closing (Escape key, or future outside-click).
43
+ * The consumer is responsible for updating `open` in response.
44
+ */
45
+ onClose?: () => void;
29
46
  children?: Snippet;
30
47
  };
31
48
  /** Split a placement into its side and (optional) alignment. */
@@ -1 +1 @@
1
- {"version":3,"file":"Popper.svelte.d.ts","sourceRoot":"","sources":["../src/lib/Popper.svelte.ts"],"names":[],"mappings":"AAGE,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,QAAQ,CAAC;AAGtC,MAAM,MAAM,cAAc,GAAG,UAAU,GAAG,OAAO,CAAC;AAElD,MAAM,MAAM,eAAe,GACvB,KAAK,GACL,QAAQ,GACR,MAAM,GACN,OAAO,GACP,WAAW,GACX,SAAS,GACT,cAAc,GACd,YAAY,GACZ,YAAY,GACZ,UAAU,GACV,aAAa,GACb,WAAW,CAAC;AAEhB,MAAM,MAAM,UAAU,GAAG,KAAK,GAAG,QAAQ,GAAG,MAAM,GAAG,OAAO,CAAC;AAC7D,MAAM,MAAM,WAAW,GAAG,OAAO,GAAG,QAAQ,GAAG,KAAK,CAAC;AAErD,MAAM,MAAM,WAAW,GAAG;IACxB,yDAAyD;IACzD,MAAM,EAAE,WAAW,GAAG,IAAI,CAAC;IAC3B,wEAAwE;IACxE,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,4DAA4D;IAC5D,SAAS,CAAC,EAAE,eAAe,CAAC;IAC5B,gEAAgE;IAChE,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,4EAA4E;IAC5E,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,wEAAwE;IACxE,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,yCAAyC;IACzC,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,gCAAgC;IAChC,QAAQ,CAAC,EAAE,cAAc,CAAC;IAC1B,0DAA0D;IAC1D,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,oDAAoD;IACpD,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,qEAAqE;IACrE,iBAAiB,CAAC,EAAE,CAAC,SAAS,EAAE,eAAe,KAAK,IAAI,CAAC;IACzD,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB,CAAC;AAEF,gEAAgE;AAChE,wBAAgB,cAAc,CAAC,SAAS,EAAE,eAAe,GAAG;IAC1D,IAAI,EAAE,UAAU,CAAC;IACjB,KAAK,EAAE,WAAW,CAAC;CACpB,CAGA;AAED,4DAA4D;AAC5D,wBAAgB,aAAa,CAAC,IAAI,EAAE,UAAU,EAAE,KAAK,EAAE,WAAW,GAAG,eAAe,CAEnF;AASD,MAAM,MAAM,IAAI,GAAG;IACjB,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;CAChB,CAAC;AAEF;;;;;;;;GAQG;AACH,wBAAgB,eAAe,CAC7B,UAAU,EAAE,IAAI,EAChB,UAAU,EAAE,MAAM,EAClB,WAAW,EAAE,MAAM,EACnB,OAAO,EAAE;IACP,SAAS,EAAE,eAAe,CAAC;IAC3B,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,OAAO,CAAC;IACd,KAAK,EAAE,OAAO,CAAC;IACf,aAAa,EAAE,MAAM,CAAC;IACtB,cAAc,EAAE,MAAM,CAAC;CACxB,GACA;IAAE,SAAS,EAAE,eAAe,CAAC;IAAC,GAAG,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,CAwD3D;AAsHH,QAAA,MAAM,MAAM,iDAAwC,CAAC;AACrD,KAAK,MAAM,GAAG,UAAU,CAAC,OAAO,MAAM,CAAC,CAAC;AACxC,eAAe,MAAM,CAAC"}
1
+ {"version":3,"file":"Popper.svelte.d.ts","sourceRoot":"","sources":["../src/lib/Popper.svelte.ts"],"names":[],"mappings":"AAGE,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,QAAQ,CAAC;AAGtC,MAAM,MAAM,cAAc,GAAG,UAAU,GAAG,OAAO,CAAC;AAElD,MAAM,MAAM,eAAe,GACvB,KAAK,GACL,QAAQ,GACR,MAAM,GACN,OAAO,GACP,WAAW,GACX,SAAS,GACT,cAAc,GACd,YAAY,GACZ,YAAY,GACZ,UAAU,GACV,aAAa,GACb,WAAW,CAAC;AAEhB,MAAM,MAAM,UAAU,GAAG,KAAK,GAAG,QAAQ,GAAG,MAAM,GAAG,OAAO,CAAC;AAC7D,MAAM,MAAM,WAAW,GAAG,OAAO,GAAG,QAAQ,GAAG,KAAK,CAAC;AAErD,MAAM,MAAM,WAAW,GAAG;IACxB,yDAAyD;IACzD,MAAM,EAAE,WAAW,GAAG,IAAI,CAAC;IAC3B,wEAAwE;IACxE,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,4DAA4D;IAC5D,SAAS,CAAC,EAAE,eAAe,CAAC;IAC5B,gEAAgE;IAChE,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,4EAA4E;IAC5E,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,wEAAwE;IACxE,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,yCAAyC;IACzC,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,gCAAgC;IAChC,QAAQ,CAAC,EAAE,cAAc,CAAC;IAC1B,0DAA0D;IAC1D,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,oDAAoD;IACpD,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,qEAAqE;IACrE,iBAAiB,CAAC,EAAE,CAAC,SAAS,EAAE,eAAe,KAAK,IAAI,CAAC;IACzD;;;;;OAKG;IACH,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB;;;OAGG;IACH,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB;;;OAGG;IACH,OAAO,CAAC,EAAE,MAAM,IAAI,CAAC;IACrB,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB,CAAC;AAEF,gEAAgE;AAChE,wBAAgB,cAAc,CAAC,SAAS,EAAE,eAAe,GAAG;IAC1D,IAAI,EAAE,UAAU,CAAC;IACjB,KAAK,EAAE,WAAW,CAAC;CACpB,CAGA;AAED,4DAA4D;AAC5D,wBAAgB,aAAa,CAAC,IAAI,EAAE,UAAU,EAAE,KAAK,EAAE,WAAW,GAAG,eAAe,CAEnF;AASD,MAAM,MAAM,IAAI,GAAG;IACjB,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;CAChB,CAAC;AAEF;;;;;;;;GAQG;AACH,wBAAgB,eAAe,CAC7B,UAAU,EAAE,IAAI,EAChB,UAAU,EAAE,MAAM,EAClB,WAAW,EAAE,MAAM,EACnB,OAAO,EAAE;IACP,SAAS,EAAE,eAAe,CAAC;IAC3B,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,OAAO,CAAC;IACd,KAAK,EAAE,OAAO,CAAC;IACf,aAAa,EAAE,MAAM,CAAC;IACtB,cAAc,EAAE,MAAM,CAAC;CACxB,GACA;IAAE,SAAS,EAAE,eAAe,CAAC;IAAC,GAAG,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,CAwD3D;AAmQH,QAAA,MAAM,MAAM,iDAAwC,CAAC;AACrD,KAAK,MAAM,GAAG,UAAU,CAAC,OAAO,MAAM,CAAC,CAAC;AACxC,eAAe,MAAM,CAAC"}
@@ -0,0 +1,340 @@
1
+ <script lang="ts" module>
2
+ /**
3
+ * RadarChart - API canonique (référence Svelte, React/Vue doivent s'aligner)
4
+ *
5
+ * Props obligatoires :
6
+ * axes string[] - libellés des axes (N axes = polygone à N côtés)
7
+ * series RadarChartSeries[] - séries {label, values: number[], tone?}
8
+ * label string - aria-label du graphique
9
+ *
10
+ * Props optionnelles :
11
+ * maxValue number (défaut : max des valeurs, min 1) - valeur plafond du
12
+ * domaine. PAS de plancher arbitraire à 100 - l'échelle
13
+ * s'adapte aux données. React/Vue doivent supprimer leur
14
+ * `Math.max(100, …)` pour s'aligner sur ce comportement.
15
+ * levels number (défaut 4) - nombre de cercles / anneaux de grille
16
+ * legend boolean (défaut false) - affiche la légende des séries
17
+ * width number (défaut 360) - largeur du viewBox en px
18
+ * height number (défaut 320) - hauteur du viewBox en px
19
+ * class string - classe CSS supplémentaire
20
+ *
21
+ * NaN/vide : les valeurs non-finies sont exclues du calcul du domaine
22
+ * (filter Number.isFinite). Séries vides → polygone nul sans crash.
23
+ */
24
+ export type RadarChartTone =
25
+ | "category1"
26
+ | "category2"
27
+ | "category3"
28
+ | "category4"
29
+ | "category5"
30
+ | "category6"
31
+ | "category7"
32
+ | "category8";
33
+
34
+ export type RadarChartSeries = {
35
+ label: string;
36
+ values: number[];
37
+ tone?: RadarChartTone;
38
+ };
39
+ </script>
40
+
41
+ <script lang="ts">
42
+ import ChartDataList from "./ChartDataList.svelte";
43
+
44
+ type RadarChartProps = {
45
+ axes: string[];
46
+ series: RadarChartSeries[];
47
+ label: string;
48
+ legend?: boolean;
49
+ maxValue?: number;
50
+ levels?: number;
51
+ width?: number;
52
+ height?: number;
53
+ class?: string;
54
+ };
55
+
56
+ let {
57
+ axes,
58
+ series,
59
+ label,
60
+ legend = false,
61
+ maxValue,
62
+ levels = 4,
63
+ width = 360,
64
+ height = 320,
65
+ class: className
66
+ }: RadarChartProps = $props();
67
+
68
+ const TONES = [
69
+ "category1",
70
+ "category2",
71
+ "category3",
72
+ "category4",
73
+ "category5",
74
+ "category6",
75
+ "category7",
76
+ "category8"
77
+ ] as const;
78
+
79
+ function pointAt(cx: number, cy: number, radius: number, angle: number) {
80
+ return {
81
+ x: cx + radius * Math.cos(angle),
82
+ y: cy + radius * Math.sin(angle)
83
+ };
84
+ }
85
+
86
+ let hoveredIndex: number | null = $state(null);
87
+
88
+ const center = $derived({ x: width / 2, y: height / 2 });
89
+ const radius = $derived(Math.max(Math.min(width, height) / 2 - 42, 1));
90
+ const safeLevelCount = $derived(Math.max(1, Math.floor(levels)));
91
+ const domainMax = $derived.by(() => {
92
+ if (Number.isFinite(maxValue) && (maxValue ?? 0) > 0) return maxValue as number;
93
+ const values = series.flatMap((entry) => entry.values).filter(Number.isFinite);
94
+ return Math.max(1, ...values);
95
+ });
96
+
97
+ const axisEntries = $derived(
98
+ axes.map((axis, index) => {
99
+ const angle = -Math.PI / 2 + (Math.PI * 2 * index) / Math.max(axes.length, 1);
100
+ const end = pointAt(center.x, center.y, radius, angle);
101
+ const labelPoint = pointAt(center.x, center.y, radius + 22, angle);
102
+ return { axis, index, angle, end, labelPoint };
103
+ })
104
+ );
105
+
106
+ const rings = $derived(
107
+ Array.from({ length: safeLevelCount }, (_, index) => {
108
+ const ringRadius = (radius * (index + 1)) / safeLevelCount;
109
+ return axisEntries.map((axis) => pointAt(center.x, center.y, ringRadius, axis.angle)).map((point) => `${point.x},${point.y}`).join(" ");
110
+ })
111
+ );
112
+
113
+ const polygons = $derived(
114
+ series.map((entry, seriesIndex) => {
115
+ const tone = entry.tone ?? TONES[seriesIndex % TONES.length];
116
+ const points = axes.map((_, axisIndex) => {
117
+ const value = Math.max(0, entry.values[axisIndex] ?? 0);
118
+ const scaled = Math.min(value / domainMax, 1) * radius;
119
+ const angle = -Math.PI / 2 + (Math.PI * 2 * axisIndex) / Math.max(axes.length, 1);
120
+ return pointAt(center.x, center.y, scaled, angle);
121
+ });
122
+ return {
123
+ entry,
124
+ tone,
125
+ points,
126
+ pointString: points.map((point) => `${point.x},${point.y}`).join(" ")
127
+ };
128
+ })
129
+ );
130
+
131
+ const legendItems = $derived(series.map((entry, index) => ({ label: entry.label, tone: entry.tone ?? TONES[index % TONES.length] })));
132
+
133
+ const dataValueItems = $derived(
134
+ series.flatMap((entry) => axes.map((axis, axisIndex) => `${entry.label}, ${axis}: ${entry.values[axisIndex] ?? 0}`))
135
+ );
136
+
137
+ function handleVisualPointerMove(event: PointerEvent) {
138
+ const target = event.target;
139
+ if (!(target instanceof Element)) {
140
+ hoveredIndex = null;
141
+ return;
142
+ }
143
+ const index = Number(target.getAttribute("data-chart-index"));
144
+ hoveredIndex = Number.isInteger(index) ? index : null;
145
+ }
146
+
147
+ const classes = () => ["st-radarChart", className].filter(Boolean).join(" ");
148
+ </script>
149
+
150
+ <div class={classes()}>
151
+ <div
152
+ class="st-radarChart__visual"
153
+ role="img"
154
+ aria-label={label}
155
+ onpointermove={handleVisualPointerMove}
156
+ onpointerleave={() => (hoveredIndex = null)}
157
+ >
158
+ <svg
159
+ viewBox="0 0 {width} {height}"
160
+ preserveAspectRatio="xMidYMid meet"
161
+ width="100%"
162
+ height="100%"
163
+ focusable="false"
164
+ aria-hidden="true"
165
+ >
166
+ {#each rings as ring, i (i)}
167
+ <polygon class="st-radarChart__ring" points={ring} />
168
+ {/each}
169
+
170
+ {#each axisEntries as axis (axis.axis)}
171
+ <line class="st-radarChart__axis" x1={center.x} x2={axis.end.x} y1={center.y} y2={axis.end.y} />
172
+ <text
173
+ class="st-radarChart__axisLabel"
174
+ x={axis.labelPoint.x}
175
+ y={axis.labelPoint.y}
176
+ text-anchor="middle"
177
+ dominant-baseline="middle"
178
+ >
179
+ {axis.axis}
180
+ </text>
181
+ {/each}
182
+
183
+ {#each polygons as polygon, i (polygon.entry.label)}
184
+ <polygon
185
+ class="st-radarChart__polygon st-radarChart__polygon--{polygon.tone}"
186
+ class:st-radarChart__polygon--dim={hoveredIndex !== null && hoveredIndex !== i}
187
+ points={polygon.pointString}
188
+ data-chart-index={i}
189
+ />
190
+ {#each polygon.points as point, pointIndex (`${polygon.entry.label}-${pointIndex}`)}
191
+ <circle class="st-radarChart__point st-radarChart__point--{polygon.tone}" cx={point.x} cy={point.y} r="3" data-chart-index={i} />
192
+ {/each}
193
+ {/each}
194
+ </svg>
195
+ </div>
196
+
197
+ <ChartDataList {label} items={dataValueItems} />
198
+
199
+ {#if hoveredIndex !== null && polygons[hoveredIndex]}
200
+ {@const polygon = polygons[hoveredIndex]}
201
+ <div class="st-radarChart__tooltip" role="presentation">
202
+ <span class="st-radarChart__tooltipLabel">{polygon.entry.label}</span>
203
+ </div>
204
+ {/if}
205
+
206
+ {#if legend && legendItems.length > 0}
207
+ <ul class="st-radarChart__legend" aria-hidden="true">
208
+ {#each legendItems as item (item.label)}
209
+ <li class="st-radarChart__legendItem">
210
+ <span class="st-radarChart__legendSwatch st-radarChart__legendSwatch--{item.tone}"></span>
211
+ {item.label}
212
+ </li>
213
+ {/each}
214
+ </ul>
215
+ {/if}
216
+ </div>
217
+
218
+ <style>
219
+ .st-radarChart {
220
+ color: var(--st-semantic-text-secondary);
221
+ display: block;
222
+ font-family: inherit;
223
+ max-width: 100%;
224
+ position: relative;
225
+ width: 100%;
226
+ }
227
+
228
+ .st-radarChart svg,
229
+ .st-radarChart__visual {
230
+ display: block;
231
+ overflow: visible;
232
+ }
233
+
234
+ .st-radarChart__ring {
235
+ fill: none;
236
+ stroke: var(--st-semantic-border-subtle);
237
+ stroke-width: 1;
238
+ }
239
+
240
+ .st-radarChart__axis {
241
+ stroke: var(--st-semantic-border-subtle);
242
+ stroke-width: 1;
243
+ }
244
+
245
+ .st-radarChart__axisLabel {
246
+ fill: var(--st-semantic-text-secondary);
247
+ font-size: 0.72rem;
248
+ }
249
+
250
+ .st-radarChart__polygon {
251
+ cursor: pointer;
252
+ fill-opacity: 0.16;
253
+ stroke-width: 2;
254
+ transition: opacity 120ms ease;
255
+ }
256
+
257
+ .st-radarChart__polygon--dim {
258
+ opacity: 0.35;
259
+ }
260
+
261
+ @media (prefers-reduced-motion: reduce) {
262
+ .st-radarChart__polygon {
263
+ transition: none;
264
+ }
265
+ }
266
+
267
+ .st-radarChart__point {
268
+ stroke: var(--st-semantic-surface-default, Canvas);
269
+ stroke-width: 1;
270
+ }
271
+
272
+ .st-radarChart__polygon--category1,
273
+ .st-radarChart__point--category1,
274
+ .st-radarChart__legendSwatch--category1 { fill: var(--st-semantic-data-category1); stroke: var(--st-semantic-data-category1); background: var(--st-semantic-data-category1); }
275
+ .st-radarChart__polygon--category2,
276
+ .st-radarChart__point--category2,
277
+ .st-radarChart__legendSwatch--category2 { fill: var(--st-semantic-data-category2); stroke: var(--st-semantic-data-category2); background: var(--st-semantic-data-category2); }
278
+ .st-radarChart__polygon--category3,
279
+ .st-radarChart__point--category3,
280
+ .st-radarChart__legendSwatch--category3 { fill: var(--st-semantic-data-category3); stroke: var(--st-semantic-data-category3); background: var(--st-semantic-data-category3); }
281
+ .st-radarChart__polygon--category4,
282
+ .st-radarChart__point--category4,
283
+ .st-radarChart__legendSwatch--category4 { fill: var(--st-semantic-data-category4); stroke: var(--st-semantic-data-category4); background: var(--st-semantic-data-category4); }
284
+ .st-radarChart__polygon--category5,
285
+ .st-radarChart__point--category5,
286
+ .st-radarChart__legendSwatch--category5 { fill: var(--st-semantic-data-category5); stroke: var(--st-semantic-data-category5); background: var(--st-semantic-data-category5); }
287
+ .st-radarChart__polygon--category6,
288
+ .st-radarChart__point--category6,
289
+ .st-radarChart__legendSwatch--category6 { fill: var(--st-semantic-data-category6); stroke: var(--st-semantic-data-category6); background: var(--st-semantic-data-category6); }
290
+ .st-radarChart__polygon--category7,
291
+ .st-radarChart__point--category7,
292
+ .st-radarChart__legendSwatch--category7 { fill: var(--st-semantic-data-category7); stroke: var(--st-semantic-data-category7); background: var(--st-semantic-data-category7); }
293
+ .st-radarChart__polygon--category8,
294
+ .st-radarChart__point--category8,
295
+ .st-radarChart__legendSwatch--category8 { fill: var(--st-semantic-data-category8); stroke: var(--st-semantic-data-category8); background: var(--st-semantic-data-category8); }
296
+
297
+ .st-radarChart__legend {
298
+ display: flex;
299
+ flex-wrap: wrap;
300
+ gap: var(--st-spacing-2, 0.5rem) var(--st-spacing-4, 1rem);
301
+ list-style: none;
302
+ margin: var(--st-spacing-2, 0.5rem) 0 0;
303
+ padding: 0;
304
+ }
305
+
306
+ .st-radarChart__legendItem {
307
+ align-items: center;
308
+ color: var(--st-semantic-text-secondary);
309
+ display: inline-flex;
310
+ font-size: 0.75rem;
311
+ gap: var(--st-spacing-2, 0.5rem);
312
+ }
313
+
314
+ .st-radarChart__legendSwatch {
315
+ display: inline-block;
316
+ height: 0.625rem;
317
+ width: 0.625rem;
318
+ }
319
+
320
+ .st-radarChart__tooltip {
321
+ background: var(--st-semantic-surface-inverse);
322
+ border-radius: var(--st-radius-sm, 0.25rem);
323
+ color: var(--st-semantic-text-inverse);
324
+ display: inline-flex;
325
+ font-size: 0.75rem;
326
+ left: 50%;
327
+ line-height: 1.2;
328
+ padding: 0.375rem 0.5rem;
329
+ pointer-events: none;
330
+ position: absolute;
331
+ top: 50%;
332
+ transform: translate(-50%, -50%);
333
+ white-space: nowrap;
334
+ z-index: 1;
335
+ }
336
+
337
+ .st-radarChart__tooltipLabel {
338
+ font-weight: 600;
339
+ }
340
+ </style>
@@ -0,0 +1,43 @@
1
+ /**
2
+ * RadarChart - API canonique (référence Svelte, React/Vue doivent s'aligner)
3
+ *
4
+ * Props obligatoires :
5
+ * axes string[] - libellés des axes (N axes = polygone à N côtés)
6
+ * series RadarChartSeries[] - séries {label, values: number[], tone?}
7
+ * label string - aria-label du graphique
8
+ *
9
+ * Props optionnelles :
10
+ * maxValue number (défaut : max des valeurs, min 1) - valeur plafond du
11
+ * domaine. PAS de plancher arbitraire à 100 - l'échelle
12
+ * s'adapte aux données. React/Vue doivent supprimer leur
13
+ * `Math.max(100, …)` pour s'aligner sur ce comportement.
14
+ * levels number (défaut 4) - nombre de cercles / anneaux de grille
15
+ * legend boolean (défaut false) - affiche la légende des séries
16
+ * width number (défaut 360) - largeur du viewBox en px
17
+ * height number (défaut 320) - hauteur du viewBox en px
18
+ * class string - classe CSS supplémentaire
19
+ *
20
+ * NaN/vide : les valeurs non-finies sont exclues du calcul du domaine
21
+ * (filter Number.isFinite). Séries vides → polygone nul sans crash.
22
+ */
23
+ export type RadarChartTone = "category1" | "category2" | "category3" | "category4" | "category5" | "category6" | "category7" | "category8";
24
+ export type RadarChartSeries = {
25
+ label: string;
26
+ values: number[];
27
+ tone?: RadarChartTone;
28
+ };
29
+ type RadarChartProps = {
30
+ axes: string[];
31
+ series: RadarChartSeries[];
32
+ label: string;
33
+ legend?: boolean;
34
+ maxValue?: number;
35
+ levels?: number;
36
+ width?: number;
37
+ height?: number;
38
+ class?: string;
39
+ };
40
+ declare const RadarChart: import("svelte").Component<RadarChartProps, {}, "">;
41
+ type RadarChart = ReturnType<typeof RadarChart>;
42
+ export default RadarChart;
43
+ //# sourceMappingURL=RadarChart.svelte.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"RadarChart.svelte.d.ts","sourceRoot":"","sources":["../src/lib/RadarChart.svelte.ts"],"names":[],"mappings":"AAGE;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,MAAM,MAAM,cAAc,GACtB,WAAW,GACX,WAAW,GACX,WAAW,GACX,WAAW,GACX,WAAW,GACX,WAAW,GACX,WAAW,GACX,WAAW,CAAC;AAEhB,MAAM,MAAM,gBAAgB,GAAG;IAC7B,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,IAAI,CAAC,EAAE,cAAc,CAAC;CACvB,CAAC;AAMF,KAAK,eAAe,GAAG;IACrB,IAAI,EAAE,MAAM,EAAE,CAAC;IACf,MAAM,EAAE,gBAAgB,EAAE,CAAC;IAC3B,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB,CAAC;AAqJJ,QAAA,MAAM,UAAU,qDAAwC,CAAC;AACzD,KAAK,UAAU,GAAG,UAAU,CAAC,OAAO,UAAU,CAAC,CAAC;AAChD,eAAe,UAAU,CAAC"}