@sentropic/design-system-svelte 0.21.0 → 0.22.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.
@@ -25,6 +25,22 @@
25
25
  height?: number;
26
26
  orientation?: "vertical" | "horizontal";
27
27
  label: string;
28
+ /**
29
+ * Keys of the currently selected bars (a bar's key is its `label`).
30
+ * CONTROLLED — the parent owns the toggle; the component never stores
31
+ * selection. When non-empty the selected bars stay full opacity (+ accent)
32
+ * and the rest dim; when empty every bar is normal. Defaults to [].
33
+ */
34
+ selectedKeys?: string[];
35
+ /**
36
+ * Called with the bar's key (its `label`) when the user selects it. When
37
+ * provided, an ACCESSIBLE row of filter chips (real <button>s) is rendered
38
+ * OUTSIDE the aria-hidden SVG — that is the keyboard + screen-reader surface.
39
+ * The SVG bars themselves stay decorative (aria-hidden) and only offer a
40
+ * mouse click shortcut for sighted pointer users. When omitted the chart is
41
+ * purely presentational (no interactivity, unchanged).
42
+ */
43
+ onSelect?: (key: string) => void;
28
44
  class?: string;
29
45
  };
30
46
 
@@ -34,6 +50,8 @@
34
50
  height = 240,
35
51
  orientation = "vertical",
36
52
  label,
53
+ selectedKeys = [],
54
+ onSelect,
37
55
  class: className
38
56
  }: BarChartProps = $props();
39
57
 
@@ -75,6 +93,12 @@
75
93
 
76
94
  let hoveredIndex: number | null = $state(null);
77
95
 
96
+ // Selection (controlled): fast lookup + "is any bar selected" flag. Only when
97
+ // something is selected do we dim the non-selected bars.
98
+ const selectedSet = $derived(new Set<string>(selectedKeys));
99
+ const hasSelection = $derived(selectedSet.size > 0);
100
+ const interactive = $derived(typeof onSelect === "function");
101
+
78
102
  const scales = $derived.by(() => {
79
103
  const values = data.map((d) => d.value);
80
104
  const minRaw = Math.min(0, ...values);
@@ -272,20 +296,55 @@
272
296
  {/each}
273
297
 
274
298
  <!-- bars -->
299
+ <!-- The bars live inside an aria-hidden SVG, so they are NEVER an accessible
300
+ surface. When `onSelect` is provided they only carry a mouse click
301
+ shortcut (cursor:pointer) for sighted pointer users — keyboard + screen
302
+ readers use the filter-chip buttons rendered below, outside this SVG. -->
275
303
  {#each bars as bar, i (bar.datum.label)}
304
+ {@const isSelected = selectedSet.has(bar.datum.label)}
305
+ <!-- The mouse click is a deliberate sighted-pointer-only shortcut on a
306
+ decorative element inside an aria-hidden SVG; the real keyboard + AT
307
+ path is the filter-chip <button>s below. No ARIA role/keyboard here
308
+ on purpose (it would be a lie under aria-hidden). -->
309
+ <!-- svelte-ignore a11y_no_static_element_interactions -->
310
+ <!-- svelte-ignore a11y_click_events_have_key_events -->
276
311
  <rect
277
312
  class="st-barChart__bar st-barChart__bar--{bar.tone}"
313
+ class:st-barChart__bar--selected={isSelected}
314
+ class:st-barChart__bar--dim={hasSelection && !isSelected}
315
+ class:st-barChart__bar--interactive={interactive}
278
316
  x={bar.x}
279
317
  y={bar.y}
280
318
  width={bar.width}
281
319
  height={bar.height}
282
320
  rx="2"
283
321
  data-chart-index={i}
322
+ onclick={interactive ? () => onSelect?.(bar.datum.label) : undefined}
284
323
  />
285
324
  {/each}
286
325
  </svg>
287
326
  </div>
288
327
 
328
+ {#if interactive}
329
+ <!-- Accessible selection surface — real <button>s OUTSIDE the aria-hidden
330
+ SVG. This is the keyboard + screen-reader path for filtering. -->
331
+ <div class="st-barChart__filters" role="group" aria-label={`Filtrer par ${label}`}>
332
+ {#each bars as bar (bar.datum.label)}
333
+ {@const isSelected = selectedSet.has(bar.datum.label)}
334
+ <button
335
+ type="button"
336
+ class="st-barChart__filterChip st-barChart__filterChip--{bar.tone}"
337
+ class:st-barChart__filterChip--selected={isSelected}
338
+ aria-pressed={isSelected}
339
+ onclick={() => onSelect?.(bar.datum.label)}
340
+ >
341
+ <span class="st-barChart__filterSwatch" aria-hidden="true"></span>
342
+ {bar.datum.label}: {bar.datum.value}
343
+ </button>
344
+ {/each}
345
+ </div>
346
+ {/if}
347
+
289
348
  <ChartDataList {label} items={dataValueItems} />
290
349
 
291
350
  {#if hoveredIndex !== null && bars[hoveredIndex]}
@@ -339,13 +398,35 @@
339
398
 
340
399
  .st-barChart__bar {
341
400
  cursor: pointer;
342
- transition: opacity 120ms ease;
401
+ transition: opacity var(--st-motion-fast, 120ms) var(--st-motion-easing, ease);
343
402
  }
344
403
 
345
404
  .st-barChart__bar:hover {
346
405
  opacity: 0.82;
347
406
  }
348
407
 
408
+ /* Non-selected bars are dimmed while a selection is active. Floor kept high
409
+ (0.6) so the colour stays distinguishable — opacity is never the sole cue;
410
+ selection also adds a stroke (shape), and the values stay in the chips +
411
+ ChartDataList. */
412
+ .st-barChart__bar--dim {
413
+ opacity: 0.6;
414
+ }
415
+ /* Hover still lifts a dimmed bar so it stays explorable. */
416
+ .st-barChart__bar--dim:hover {
417
+ opacity: 0.8;
418
+ }
419
+
420
+ /* Selected bar: full opacity + a contrast-safe accent stroke (two signals,
421
+ never a font/size reflow). Outranks the dim rule. */
422
+ .st-barChart__bar--selected,
423
+ .st-barChart__bar--selected:hover {
424
+ opacity: 1;
425
+ stroke: var(--st-semantic-border-interactive, var(--st-semantic-action-primary));
426
+ stroke-width: 2;
427
+ paint-order: stroke;
428
+ }
429
+
349
430
  .st-barChart__bar--category1 { fill: var(--st-semantic-data-category1); }
350
431
  .st-barChart__bar--category2 { fill: var(--st-semantic-data-category2); }
351
432
  .st-barChart__bar--category3 { fill: var(--st-semantic-data-category3); }
@@ -355,6 +436,68 @@
355
436
  .st-barChart__bar--category7 { fill: var(--st-semantic-data-category7); }
356
437
  .st-barChart__bar--category8 { fill: var(--st-semantic-data-category8); }
357
438
 
439
+ /* Accessible filter chips — keyboard + screen-reader selection surface,
440
+ rendered outside the aria-hidden SVG. */
441
+ .st-barChart__filters {
442
+ display: flex;
443
+ flex-wrap: wrap;
444
+ gap: var(--st-spacing-2, 0.5rem);
445
+ margin-top: var(--st-spacing-2, 0.5rem);
446
+ }
447
+
448
+ .st-barChart__filterChip {
449
+ align-items: center;
450
+ background: var(--st-semantic-surface-subtle, #f8fafc);
451
+ border: 1px solid var(--st-semantic-border-interactive, #cbd5e1);
452
+ border-radius: var(--st-radius-pill, 999px);
453
+ color: var(--st-semantic-text-secondary, #475569);
454
+ cursor: var(--st-cursor-interactive, pointer);
455
+ display: inline-flex;
456
+ font: inherit;
457
+ font-size: 0.8125rem;
458
+ font-weight: 500;
459
+ gap: var(--st-spacing-1, 0.25rem);
460
+ line-height: 1;
461
+ padding: 0.3125rem var(--st-spacing-2, 0.5rem);
462
+ transition:
463
+ background-color var(--st-motion-fast, 120ms) var(--st-motion-easing, ease),
464
+ color var(--st-motion-fast, 120ms) var(--st-motion-easing, ease),
465
+ border-color var(--st-motion-fast, 120ms) var(--st-motion-easing, ease);
466
+ }
467
+
468
+ .st-barChart__filterChip:hover {
469
+ background: var(--st-semantic-surface-hover, #eef2f7);
470
+ }
471
+
472
+ .st-barChart__filterChip:focus-visible {
473
+ outline: 2px solid var(--st-semantic-border-interactive, var(--st-semantic-action-primary));
474
+ outline-offset: 2px;
475
+ }
476
+
477
+ /* Selected chip: solid accent fill + matching text — signalled by colour AND
478
+ by aria-pressed, never by opacity alone. */
479
+ .st-barChart__filterChip--selected {
480
+ background: var(--st-semantic-action-primary, #2563eb);
481
+ border-color: var(--st-semantic-action-primary, #2563eb);
482
+ color: var(--st-semantic-text-inverse, #fff);
483
+ }
484
+
485
+ /* Colour swatch echoing the bar tone, for quick visual mapping chip↔bar. */
486
+ .st-barChart__filterSwatch {
487
+ border-radius: var(--st-radius-sm, 0.25rem);
488
+ display: inline-block;
489
+ height: 0.625rem;
490
+ width: 0.625rem;
491
+ }
492
+ .st-barChart__filterChip--category1 .st-barChart__filterSwatch { background: var(--st-semantic-data-category1); }
493
+ .st-barChart__filterChip--category2 .st-barChart__filterSwatch { background: var(--st-semantic-data-category2); }
494
+ .st-barChart__filterChip--category3 .st-barChart__filterSwatch { background: var(--st-semantic-data-category3); }
495
+ .st-barChart__filterChip--category4 .st-barChart__filterSwatch { background: var(--st-semantic-data-category4); }
496
+ .st-barChart__filterChip--category5 .st-barChart__filterSwatch { background: var(--st-semantic-data-category5); }
497
+ .st-barChart__filterChip--category6 .st-barChart__filterSwatch { background: var(--st-semantic-data-category6); }
498
+ .st-barChart__filterChip--category7 .st-barChart__filterSwatch { background: var(--st-semantic-data-category7); }
499
+ .st-barChart__filterChip--category8 .st-barChart__filterSwatch { background: var(--st-semantic-data-category8); }
500
+
358
501
  .st-barChart__tooltip {
359
502
  background: var(--st-component-barChart-tooltipBackground, var(--st-semantic-surface-inverse));
360
503
  border-radius: var(--st-radius-sm, 0.25rem);
@@ -379,4 +522,9 @@
379
522
  .st-barChart__tooltipValue {
380
523
  opacity: 0.85;
381
524
  }
525
+
526
+ @media (prefers-reduced-motion: reduce) {
527
+ .st-barChart__bar,
528
+ .st-barChart__filterChip { transition: none; }
529
+ }
382
530
  </style>
@@ -10,6 +10,22 @@ type BarChartProps = {
10
10
  height?: number;
11
11
  orientation?: "vertical" | "horizontal";
12
12
  label: string;
13
+ /**
14
+ * Keys of the currently selected bars (a bar's key is its `label`).
15
+ * CONTROLLED — the parent owns the toggle; the component never stores
16
+ * selection. When non-empty the selected bars stay full opacity (+ accent)
17
+ * and the rest dim; when empty every bar is normal. Defaults to [].
18
+ */
19
+ selectedKeys?: string[];
20
+ /**
21
+ * Called with the bar's key (its `label`) when the user selects it. When
22
+ * provided, an ACCESSIBLE row of filter chips (real <button>s) is rendered
23
+ * OUTSIDE the aria-hidden SVG — that is the keyboard + screen-reader surface.
24
+ * The SVG bars themselves stay decorative (aria-hidden) and only offer a
25
+ * mouse click shortcut for sighted pointer users. When omitted the chart is
26
+ * purely presentational (no interactivity, unchanged).
27
+ */
28
+ onSelect?: (key: string) => void;
13
29
  class?: string;
14
30
  };
15
31
  declare const BarChart: import("svelte").Component<BarChartProps, {}, "">;
@@ -1 +1 @@
1
- {"version":3,"file":"BarChart.svelte.d.ts","sourceRoot":"","sources":["../src/lib/BarChart.svelte.ts"],"names":[],"mappings":"AAGE,MAAM,MAAM,YAAY,GACpB,WAAW,GACX,WAAW,GACX,WAAW,GACX,WAAW,GACX,WAAW,GACX,WAAW,GACX,WAAW,GACX,WAAW,CAAC;AAEhB,MAAM,MAAM,aAAa,GAAG;IAC1B,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,YAAY,CAAC;CACrB,CAAC;AAMF,KAAK,aAAa,GAAG;IACnB,IAAI,EAAE,aAAa,EAAE,CAAC;IACtB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,WAAW,CAAC,EAAE,UAAU,GAAG,YAAY,CAAC;IACxC,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB,CAAC;AAqNJ,QAAA,MAAM,QAAQ,mDAAwC,CAAC;AACvD,KAAK,QAAQ,GAAG,UAAU,CAAC,OAAO,QAAQ,CAAC,CAAC;AAC5C,eAAe,QAAQ,CAAC"}
1
+ {"version":3,"file":"BarChart.svelte.d.ts","sourceRoot":"","sources":["../src/lib/BarChart.svelte.ts"],"names":[],"mappings":"AAGE,MAAM,MAAM,YAAY,GACpB,WAAW,GACX,WAAW,GACX,WAAW,GACX,WAAW,GACX,WAAW,GACX,WAAW,GACX,WAAW,GACX,WAAW,CAAC;AAEhB,MAAM,MAAM,aAAa,GAAG;IAC1B,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,YAAY,CAAC;CACrB,CAAC;AAMF,KAAK,aAAa,GAAG;IACnB,IAAI,EAAE,aAAa,EAAE,CAAC;IACtB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,WAAW,CAAC,EAAE,UAAU,GAAG,YAAY,CAAC;IACxC,KAAK,EAAE,MAAM,CAAC;IACd;;;;;OAKG;IACH,YAAY,CAAC,EAAE,MAAM,EAAE,CAAC;IACxB;;;;;;;OAOG;IACH,QAAQ,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAC;IACjC,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB,CAAC;AA+OJ,QAAA,MAAM,QAAQ,mDAAwC,CAAC;AACvD,KAAK,QAAQ,GAAG,UAAU,CAAC,OAAO,QAAQ,CAAC,CAAC;AAC5C,eAAe,QAAQ,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sentropic/design-system-svelte",
3
- "version": "0.21.0",
3
+ "version": "0.22.1",
4
4
  "type": "module",
5
5
  "publishConfig": {
6
6
  "access": "public"