@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.
- package/dist/BoxPlotChart.svelte +302 -0
- package/dist/BoxPlotChart.svelte.d.ts +40 -0
- package/dist/BoxPlotChart.svelte.d.ts.map +1 -0
- package/dist/Calendar.svelte +237 -42
- package/dist/Calendar.svelte.d.ts.map +1 -1
- package/dist/HeatmapChart.svelte +337 -0
- package/dist/HeatmapChart.svelte.d.ts +35 -0
- package/dist/HeatmapChart.svelte.d.ts.map +1 -0
- package/dist/HistogramChart.svelte +294 -0
- package/dist/HistogramChart.svelte.d.ts +38 -0
- package/dist/HistogramChart.svelte.d.ts.map +1 -0
- package/dist/Popper.svelte +157 -0
- package/dist/Popper.svelte.d.ts +17 -0
- package/dist/Popper.svelte.d.ts.map +1 -1
- package/dist/RadarChart.svelte +340 -0
- package/dist/RadarChart.svelte.d.ts +43 -0
- package/dist/RadarChart.svelte.d.ts.map +1 -0
- package/dist/Rating.svelte +130 -35
- package/dist/Rating.svelte.d.ts.map +1 -1
- package/dist/SankeyChart.svelte +364 -0
- package/dist/SankeyChart.svelte.d.ts +45 -0
- package/dist/SankeyChart.svelte.d.ts.map +1 -0
- package/dist/SelectableList.svelte +60 -12
- package/dist/SelectableList.svelte.d.ts.map +1 -1
- package/dist/SelectableRow.svelte +23 -8
- package/dist/SelectableRow.svelte.d.ts +5 -4
- package/dist/SelectableRow.svelte.d.ts.map +1 -1
- package/dist/SlideIndicator.svelte +17 -3
- package/dist/SlideIndicator.svelte.d.ts.map +1 -1
- package/dist/SunburstChart.svelte +388 -0
- package/dist/SunburstChart.svelte.d.ts +39 -0
- package/dist/SunburstChart.svelte.d.ts.map +1 -0
- package/dist/TimePicker.svelte +176 -13
- package/dist/TimePicker.svelte.d.ts.map +1 -1
- package/dist/chartContrast.d.ts +0 -4
- package/dist/chartContrast.d.ts.map +1 -1
- package/dist/chartContrast.js +4 -56
- package/dist/index.d.ts +12 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -0
- package/package.json +1 -1
|
@@ -60,7 +60,7 @@
|
|
|
60
60
|
|
|
61
61
|
// --- Row registry: ordered by DOM position so arrow nav matches the visual
|
|
62
62
|
// order regardless of registration timing. -------------------------------
|
|
63
|
-
type Entry = { el: HTMLElement; value: string | undefined };
|
|
63
|
+
type Entry = { el: HTMLElement; value: string | undefined; disabled?: boolean };
|
|
64
64
|
let entries = $state<Entry[]>([]);
|
|
65
65
|
|
|
66
66
|
// The element that currently holds the roving tab stop (tabindex 0). Null until
|
|
@@ -79,9 +79,14 @@
|
|
|
79
79
|
// register/unregister are called from each row's $effect. They read AND write
|
|
80
80
|
// `entries`, so the read must be untracked — otherwise the calling effect would
|
|
81
81
|
// subscribe to `entries`, and writing it would re-run the effect forever.
|
|
82
|
-
|
|
82
|
+
// Disabled rows are registered with disabled:true so navigate() can skip them
|
|
83
|
+
// explicitly, making the skip correct even when disabled state changes mid-session.
|
|
84
|
+
function register(el: HTMLElement, rowValue: string | undefined, rowDisabled = false): () => void {
|
|
83
85
|
untrack(() => {
|
|
84
|
-
entries = sortByDom([
|
|
86
|
+
entries = sortByDom([
|
|
87
|
+
...entries.filter((e) => e.el !== el),
|
|
88
|
+
{ el, value: rowValue, disabled: rowDisabled }
|
|
89
|
+
]);
|
|
85
90
|
});
|
|
86
91
|
return () => {
|
|
87
92
|
untrack(() => {
|
|
@@ -91,8 +96,30 @@
|
|
|
91
96
|
};
|
|
92
97
|
}
|
|
93
98
|
|
|
94
|
-
// Default roving stop = first
|
|
95
|
-
|
|
99
|
+
// Default roving stop = first non-disabled DOM-ordered row when none focused,
|
|
100
|
+
// or when the current tabStopEl has become disabled.
|
|
101
|
+
const effectiveTabStop = $derived.by((): HTMLElement | null => {
|
|
102
|
+
if (tabStopEl) {
|
|
103
|
+
const entry = entries.find((e) => e.el === tabStopEl);
|
|
104
|
+
if (entry && !entry.disabled) return tabStopEl;
|
|
105
|
+
}
|
|
106
|
+
return entries.find((e) => !e.disabled)?.el ?? null;
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
// Si la row qui détient le focus DOM devient disabled (in-place, sans unmount),
|
|
110
|
+
// transférer le focus vers la nouvelle cible de roving tabindex.
|
|
111
|
+
// Note : le cas du cycle unregister/register est géré dans SelectableRow via
|
|
112
|
+
// l'$effect sur `disabled` qui appelle navigate() AVANT le cleanup.
|
|
113
|
+
$effect(() => {
|
|
114
|
+
const newStop = effectiveTabStop;
|
|
115
|
+
if (!newStop) return;
|
|
116
|
+
if (tabStopEl !== null) {
|
|
117
|
+
const disabledEntry = entries.find((e) => e.el === tabStopEl && e.disabled);
|
|
118
|
+
if (disabledEntry && tabStopEl.contains(document.activeElement ?? null)) {
|
|
119
|
+
newStop.focus();
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
});
|
|
96
123
|
|
|
97
124
|
function valueOf(el: HTMLElement): string | undefined {
|
|
98
125
|
return entries.find((e) => e.el === el)?.value;
|
|
@@ -137,13 +164,34 @@
|
|
|
137
164
|
if (entries.length === 0) return;
|
|
138
165
|
const idx = entries.findIndex((e) => e.el === el);
|
|
139
166
|
if (idx === -1) return;
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
167
|
+
|
|
168
|
+
let targetIdx: number | null = null;
|
|
169
|
+
|
|
170
|
+
if (key === "ArrowDown" || key === "ArrowRight") {
|
|
171
|
+
// Walk forward from current position, find the next non-disabled entry.
|
|
172
|
+
for (let i = idx + 1; i < entries.length; i++) {
|
|
173
|
+
if (!entries[i].disabled) { targetIdx = i; break; }
|
|
174
|
+
}
|
|
175
|
+
} else if (key === "ArrowUp" || key === "ArrowLeft") {
|
|
176
|
+
// Walk backward from current position, find the previous non-disabled entry.
|
|
177
|
+
for (let i = idx - 1; i >= 0; i--) {
|
|
178
|
+
if (!entries[i].disabled) { targetIdx = i; break; }
|
|
179
|
+
}
|
|
180
|
+
} else if (key === "Home") {
|
|
181
|
+
// First non-disabled entry.
|
|
182
|
+
for (let i = 0; i < entries.length; i++) {
|
|
183
|
+
if (!entries[i].disabled) { targetIdx = i; break; }
|
|
184
|
+
}
|
|
185
|
+
} else if (key === "End") {
|
|
186
|
+
// Last non-disabled entry.
|
|
187
|
+
for (let i = entries.length - 1; i >= 0; i--) {
|
|
188
|
+
if (!entries[i].disabled) { targetIdx = i; break; }
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// If no target found (all remaining are disabled, or already at boundary), stay put.
|
|
193
|
+
if (targetIdx === null) return;
|
|
194
|
+
|
|
147
195
|
const target = entries[targetIdx]?.el;
|
|
148
196
|
if (target) {
|
|
149
197
|
tabStopEl = target;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"SelectableList.svelte.d.ts","sourceRoot":"","sources":["../src/lib/SelectableList.svelte.ts"],"names":[],"mappings":"AAGE,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,QAAQ,CAAC;AAEtC,MAAM,MAAM,mBAAmB,GAAG;IAChC,+DAA+D;IAC/D,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,+EAA+E;IAC/E,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB;;;OAGG;IACH,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB;;;;OAIG;IACH,KAAK,CAAC,EAAE,MAAM,GAAG,MAAM,EAAE,GAAG,IAAI,CAAC;IACjC;;;;OAIG;IACH,QAAQ,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,EAAE,GAAG,IAAI,KAAK,IAAI,CAAC;IACrD,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB,CAAC;
|
|
1
|
+
{"version":3,"file":"SelectableList.svelte.d.ts","sourceRoot":"","sources":["../src/lib/SelectableList.svelte.ts"],"names":[],"mappings":"AAGE,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,QAAQ,CAAC;AAEtC,MAAM,MAAM,mBAAmB,GAAG;IAChC,+DAA+D;IAC/D,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,+EAA+E;IAC/E,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB;;;OAGG;IACH,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB;;;;OAIG;IACH,KAAK,CAAC,EAAE,MAAM,GAAG,MAAM,EAAE,GAAG,IAAI,CAAC;IACjC;;;;OAIG;IACH,QAAQ,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,EAAE,GAAG,IAAI,KAAK,IAAI,CAAC;IACrD,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB,CAAC;AA0MJ,QAAA,MAAM,cAAc,yDAAwC,CAAC;AAC7D,KAAK,cAAc,GAAG,UAAU,CAAC,OAAO,cAAc,CAAC,CAAC;AACxD,eAAe,cAAc,CAAC"}
|
|
@@ -16,8 +16,8 @@
|
|
|
16
16
|
readonly managed: true;
|
|
17
17
|
/** listbox role for the wrapper → rows are "option". */
|
|
18
18
|
readonly itemRole: "option";
|
|
19
|
-
/** Register a row element; returns an unregister callback. */
|
|
20
|
-
register: (el: HTMLElement, value: string | undefined) => () => void;
|
|
19
|
+
/** Register a row element; returns an unregister callback. disabled is forwarded so the list can skip it during keyboard navigation. */
|
|
20
|
+
register: (el: HTMLElement, value: string | undefined, disabled?: boolean) => () => void;
|
|
21
21
|
/** Is the row with this element currently selected? */
|
|
22
22
|
isSelected: (el: HTMLElement) => boolean;
|
|
23
23
|
/** Should the row with this element be the roving-tabindex stop (tabindex 0)? */
|
|
@@ -44,8 +44,9 @@
|
|
|
44
44
|
/** Stable value, surfaced as `data-value` and used by the list for `value`. */
|
|
45
45
|
value?: string;
|
|
46
46
|
/**
|
|
47
|
-
* ARIA role for the standalone row. Defaults to "
|
|
48
|
-
*
|
|
47
|
+
* ARIA role for the standalone row. Defaults to "button" for standalone use —
|
|
48
|
+
* "option" is only valid inside a listbox and would be invalid without one.
|
|
49
|
+
* Inside a SelectableList the role is always forced to "option".
|
|
49
50
|
*/
|
|
50
51
|
role?: string;
|
|
51
52
|
/**
|
|
@@ -71,7 +72,7 @@
|
|
|
71
72
|
onselect,
|
|
72
73
|
disabled = false,
|
|
73
74
|
value,
|
|
74
|
-
role = "
|
|
75
|
+
role = "button",
|
|
75
76
|
accentBar = false,
|
|
76
77
|
leading,
|
|
77
78
|
trailing,
|
|
@@ -86,10 +87,23 @@
|
|
|
86
87
|
let el: HTMLElement | null = $state(null);
|
|
87
88
|
|
|
88
89
|
// Register with the parent list (if any) so it can order rows for arrow nav
|
|
89
|
-
// and compute the roving tab stop.
|
|
90
|
+
// and compute the roving tab stop. Disabled rows are registered too so the
|
|
91
|
+
// list can skip them during navigation; the list owns the skip logic.
|
|
90
92
|
$effect(() => {
|
|
91
|
-
if (!list || !el
|
|
92
|
-
return list.register(el, value);
|
|
93
|
+
if (!list || !el) return;
|
|
94
|
+
return list.register(el, value, disabled);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
// A11y edge-case : quand cette row passe à disabled=true ET qu'elle détient le
|
|
98
|
+
// focus DOM, transférer le focus vers la prochaine row enabled via navigate().
|
|
99
|
+
// On le fait ici (dans SelectableRow) pour avoir accès au focus DOM AVANT que
|
|
100
|
+
// le cycle unregister/register dans SelectableList ne perturbe l'état.
|
|
101
|
+
$effect(() => {
|
|
102
|
+
if (!disabled || !list || !el) return;
|
|
103
|
+
if (!el.contains(document.activeElement ?? null)) return;
|
|
104
|
+
// Déléguer via navigate ArrowDown (cherche prochaine row enabled vers l'avant,
|
|
105
|
+
// puis vers l'arrière si aucune). navigate appelle target.focus() directement.
|
|
106
|
+
list.navigate(el, "ArrowDown");
|
|
93
107
|
});
|
|
94
108
|
|
|
95
109
|
// Effective selected state: list-managed rows read the list; standalone rows
|
|
@@ -165,6 +179,7 @@
|
|
|
165
179
|
class={classes}
|
|
166
180
|
role={effectiveRole}
|
|
167
181
|
aria-selected={effectiveRole === "option" ? isSelected : undefined}
|
|
182
|
+
aria-pressed={effectiveRole === "button" ? isSelected : undefined}
|
|
168
183
|
aria-disabled={disabled ? "true" : undefined}
|
|
169
184
|
data-value={value}
|
|
170
185
|
{tabindex}
|
|
@@ -13,8 +13,8 @@ export type SelectableListContext = {
|
|
|
13
13
|
readonly managed: true;
|
|
14
14
|
/** listbox role for the wrapper → rows are "option". */
|
|
15
15
|
readonly itemRole: "option";
|
|
16
|
-
/** Register a row element; returns an unregister callback. */
|
|
17
|
-
register: (el: HTMLElement, value: string | undefined) => () => void;
|
|
16
|
+
/** Register a row element; returns an unregister callback. disabled is forwarded so the list can skip it during keyboard navigation. */
|
|
17
|
+
register: (el: HTMLElement, value: string | undefined, disabled?: boolean) => () => void;
|
|
18
18
|
/** Is the row with this element currently selected? */
|
|
19
19
|
isSelected: (el: HTMLElement) => boolean;
|
|
20
20
|
/** Should the row with this element be the roving-tabindex stop (tabindex 0)? */
|
|
@@ -40,8 +40,9 @@ export type SelectableRowProps = {
|
|
|
40
40
|
/** Stable value, surfaced as `data-value` and used by the list for `value`. */
|
|
41
41
|
value?: string;
|
|
42
42
|
/**
|
|
43
|
-
* ARIA role for the standalone row. Defaults to "
|
|
44
|
-
*
|
|
43
|
+
* ARIA role for the standalone row. Defaults to "button" for standalone use —
|
|
44
|
+
* "option" is only valid inside a listbox and would be invalid without one.
|
|
45
|
+
* Inside a SelectableList the role is always forced to "option".
|
|
45
46
|
*/
|
|
46
47
|
role?: string;
|
|
47
48
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"SelectableRow.svelte.d.ts","sourceRoot":"","sources":["../src/lib/SelectableRow.svelte.ts"],"names":[],"mappings":"AAGE,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,QAAQ,CAAC;AAEtC;;;;;;;GAOG;AACH,eAAO,MAAM,mBAAmB,eAA+B,CAAC;AAEhE,MAAM,MAAM,qBAAqB,GAAG;IAClC,gEAAgE;IAChE,QAAQ,CAAC,OAAO,EAAE,IAAI,CAAC;IACvB,wDAAwD;IACxD,QAAQ,CAAC,QAAQ,EAAE,QAAQ,CAAC;IAC5B,
|
|
1
|
+
{"version":3,"file":"SelectableRow.svelte.d.ts","sourceRoot":"","sources":["../src/lib/SelectableRow.svelte.ts"],"names":[],"mappings":"AAGE,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,QAAQ,CAAC;AAEtC;;;;;;;GAOG;AACH,eAAO,MAAM,mBAAmB,eAA+B,CAAC;AAEhE,MAAM,MAAM,qBAAqB,GAAG;IAClC,gEAAgE;IAChE,QAAQ,CAAC,OAAO,EAAE,IAAI,CAAC;IACvB,wDAAwD;IACxD,QAAQ,CAAC,QAAQ,EAAE,QAAQ,CAAC;IAC5B,wIAAwI;IACxI,QAAQ,EAAE,CAAC,EAAE,EAAE,WAAW,EAAE,KAAK,EAAE,MAAM,GAAG,SAAS,EAAE,QAAQ,CAAC,EAAE,OAAO,KAAK,MAAM,IAAI,CAAC;IACzF,uDAAuD;IACvD,UAAU,EAAE,CAAC,EAAE,EAAE,WAAW,KAAK,OAAO,CAAC;IACzC,iFAAiF;IACjF,SAAS,EAAE,CAAC,EAAE,EAAE,WAAW,KAAK,OAAO,CAAC;IACxC,6EAA6E;IAC7E,QAAQ,EAAE,CAAC,EAAE,EAAE,WAAW,KAAK,IAAI,CAAC;IACpC,wDAAwD;IACxD,QAAQ,EAAE,CAAC,EAAE,EAAE,WAAW,KAAK,IAAI,CAAC;IACpC,gDAAgD;IAChD,QAAQ,EAAE,CAAC,EAAE,EAAE,WAAW,EAAE,GAAG,EAAE,MAAM,KAAK,IAAI,CAAC;CAClD,CAAC;AAEF,MAAM,MAAM,kBAAkB,GAAG;IAC/B;;;;OAIG;IACH,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,8EAA8E;IAC9E,QAAQ,CAAC,EAAE,CAAC,QAAQ,EAAE,OAAO,KAAK,IAAI,CAAC;IACvC,iCAAiC;IACjC,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,+EAA+E;IAC/E,KAAK,CAAC,EAAE,MAAM,CAAC;IACf;;;;OAIG;IACH,IAAI,CAAC,EAAE,MAAM,CAAC;IACd;;;OAGG;IACH,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,oCAAoC;IACpC,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,mCAAmC;IACnC,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,oBAAoB;IACpB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB,CAAC;AAiIJ,QAAA,MAAM,aAAa,gEAAwC,CAAC;AAC5D,KAAK,aAAa,GAAG,UAAU,CAAC,OAAO,aAAa,CAAC,CAAC;AACtD,eAAe,aAAa,CAAC"}
|
|
@@ -43,6 +43,9 @@
|
|
|
43
43
|
|
|
44
44
|
const items = $derived(Array.from({ length: Math.max(0, count) }, (_, i) => i));
|
|
45
45
|
|
|
46
|
+
// Refs des boutons pour déplacer le focus programmatiquement lors de la navigation clavier.
|
|
47
|
+
let buttonRefs = $state<Record<number, HTMLElement | null>>({});
|
|
48
|
+
|
|
46
49
|
function select(index: number) {
|
|
47
50
|
if (index < 0 || index >= count || index === current) return;
|
|
48
51
|
onChange?.(index);
|
|
@@ -69,21 +72,32 @@
|
|
|
69
72
|
return;
|
|
70
73
|
}
|
|
71
74
|
event.preventDefault();
|
|
75
|
+
// Déplacer le focus DOM vers le bouton cible (roving tabindex correct).
|
|
76
|
+
const targetEl = buttonRefs[target];
|
|
77
|
+
if (targetEl) targetEl.focus();
|
|
72
78
|
select(target);
|
|
73
79
|
}
|
|
74
80
|
</script>
|
|
75
81
|
|
|
76
|
-
|
|
82
|
+
<!--
|
|
83
|
+
Choix de pattern : role="group" + boutons avec aria-current.
|
|
84
|
+
Justification : un indicateur de carrousel/pagination n'est PAS un tablist — il n'y a pas de
|
|
85
|
+
tabpanel associé. Utiliser role="tab" sans aria-controls/tabpanel trompe les lecteurs d'écran
|
|
86
|
+
qui annoncent « onglet X sur N » sans panneau contrôlé.
|
|
87
|
+
Pattern retenu (ARIA Authoring Practices Guide — Carousel) : role="group" nommé + boutons natifs
|
|
88
|
+
avec aria-current="true" sur le point courant + roving tabindex.
|
|
89
|
+
Le SR annonce « Groupe [label] — [label] 1, bouton ; [label] 2, courant, bouton ; … »
|
|
90
|
+
-->
|
|
91
|
+
<div {...rest} class={classes} role="group" aria-label={label}>
|
|
77
92
|
{#each items as index (index)}
|
|
78
93
|
<button
|
|
79
94
|
type="button"
|
|
80
95
|
class="st-slideIndicator__dot"
|
|
81
96
|
class:st-slideIndicator__dot--current={index === current}
|
|
82
|
-
role="tab"
|
|
83
|
-
aria-selected={index === current ? "true" : "false"}
|
|
84
97
|
aria-current={index === current ? "true" : undefined}
|
|
85
98
|
aria-label={`${label} ${index + 1}`}
|
|
86
99
|
tabindex={index === current ? 0 : -1}
|
|
100
|
+
bind:this={buttonRefs[index]}
|
|
87
101
|
onclick={() => select(index)}
|
|
88
102
|
onkeydown={(event) => onKeyDown(event, index)}
|
|
89
103
|
></button>
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"SlideIndicator.svelte.d.ts","sourceRoot":"","sources":["../src/lib/SlideIndicator.svelte.ts"],"names":[],"mappings":"AAGE,MAAM,MAAM,qBAAqB,GAAG,MAAM,GAAG,MAAM,CAAC;AAGtD,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAC;AAGpD,KAAK,mBAAmB,GAAG,IAAI,CAAC,cAAc,CAAC,cAAc,CAAC,EAAE,OAAO,GAAG,UAAU,CAAC,GAAG;IACtF,oCAAoC;IACpC,KAAK,EAAE,MAAM,CAAC;IACd,kDAAkD;IAClD,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,uDAAuD;IACvD,QAAQ,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IACnC,IAAI,CAAC,EAAE,IAAI,GAAG,IAAI,GAAG,IAAI,CAAC;IAC1B,OAAO,CAAC,EAAE,qBAAqB,CAAC;IAChC,2EAA2E;IAC3E,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB,CAAC;
|
|
1
|
+
{"version":3,"file":"SlideIndicator.svelte.d.ts","sourceRoot":"","sources":["../src/lib/SlideIndicator.svelte.ts"],"names":[],"mappings":"AAGE,MAAM,MAAM,qBAAqB,GAAG,MAAM,GAAG,MAAM,CAAC;AAGtD,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAC;AAGpD,KAAK,mBAAmB,GAAG,IAAI,CAAC,cAAc,CAAC,cAAc,CAAC,EAAE,OAAO,GAAG,UAAU,CAAC,GAAG;IACtF,oCAAoC;IACpC,KAAK,EAAE,MAAM,CAAC;IACd,kDAAkD;IAClD,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,uDAAuD;IACvD,QAAQ,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IACnC,IAAI,CAAC,EAAE,IAAI,GAAG,IAAI,GAAG,IAAI,CAAC;IAC1B,OAAO,CAAC,EAAE,qBAAqB,CAAC;IAChC,2EAA2E;IAC3E,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB,CAAC;AA8EJ,QAAA,MAAM,cAAc,yDAAwC,CAAC;AAC7D,KAAK,cAAc,GAAG,UAAU,CAAC,OAAO,cAAc,CAAC,CAAC;AACxD,eAAe,cAAc,CAAC"}
|
|
@@ -0,0 +1,388 @@
|
|
|
1
|
+
<script lang="ts" module>
|
|
2
|
+
/**
|
|
3
|
+
* SunburstChart - API canonique (référence Svelte, React/Vue doivent s'aligner)
|
|
4
|
+
*
|
|
5
|
+
* Props obligatoires :
|
|
6
|
+
* data SunburstChartDatum - nœud racine (label + children récursifs)
|
|
7
|
+
* label string - aria-label du graphique
|
|
8
|
+
*
|
|
9
|
+
* Props optionnelles :
|
|
10
|
+
* legend boolean (défaut false) - affiche la légende des top-level children
|
|
11
|
+
* width number (défaut 320) - largeur du viewBox en px
|
|
12
|
+
* height number (défaut 320) - hauteur du viewBox en px
|
|
13
|
+
* class string - classe CSS supplémentaire
|
|
14
|
+
*
|
|
15
|
+
* Labels d'arcs :
|
|
16
|
+
* Affichés sur les arcs de span > 0.28 rad. Couleur de texte calculée par
|
|
17
|
+
* luminance (via chartContrast.ts) pour garantir le contraste WCAG AA sur
|
|
18
|
+
* chaque fond catégoriel - pas de texte blanc fixe.
|
|
19
|
+
*
|
|
20
|
+
* Infobulle : format "parent, enfant" (séparateur ", ") - cohérent avec lot 1.
|
|
21
|
+
*/
|
|
22
|
+
export type SunburstChartTone =
|
|
23
|
+
| "category1"
|
|
24
|
+
| "category2"
|
|
25
|
+
| "category3"
|
|
26
|
+
| "category4"
|
|
27
|
+
| "category5"
|
|
28
|
+
| "category6"
|
|
29
|
+
| "category7"
|
|
30
|
+
| "category8";
|
|
31
|
+
|
|
32
|
+
export type SunburstChartDatum = {
|
|
33
|
+
label: string;
|
|
34
|
+
value?: number;
|
|
35
|
+
tone?: SunburstChartTone;
|
|
36
|
+
children?: SunburstChartDatum[];
|
|
37
|
+
};
|
|
38
|
+
</script>
|
|
39
|
+
|
|
40
|
+
<script lang="ts">
|
|
41
|
+
import ChartDataList from "./ChartDataList.svelte";
|
|
42
|
+
import { contrastTextForTone } from "./chartContrast.js";
|
|
43
|
+
|
|
44
|
+
type SunburstChartProps = {
|
|
45
|
+
data: SunburstChartDatum;
|
|
46
|
+
label: string;
|
|
47
|
+
legend?: boolean;
|
|
48
|
+
width?: number;
|
|
49
|
+
height?: number;
|
|
50
|
+
class?: string;
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
type ArcDatum = {
|
|
54
|
+
datum: SunburstChartDatum;
|
|
55
|
+
pathLabel: string[];
|
|
56
|
+
value: number;
|
|
57
|
+
tone: SunburstChartTone;
|
|
58
|
+
depth: number;
|
|
59
|
+
start: number;
|
|
60
|
+
end: number;
|
|
61
|
+
path: string;
|
|
62
|
+
labelX: number;
|
|
63
|
+
labelY: number;
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
let {
|
|
67
|
+
data,
|
|
68
|
+
label,
|
|
69
|
+
legend = false,
|
|
70
|
+
width = 320,
|
|
71
|
+
height = 320,
|
|
72
|
+
class: className
|
|
73
|
+
}: SunburstChartProps = $props();
|
|
74
|
+
|
|
75
|
+
const TONES = [
|
|
76
|
+
"category1",
|
|
77
|
+
"category2",
|
|
78
|
+
"category3",
|
|
79
|
+
"category4",
|
|
80
|
+
"category5",
|
|
81
|
+
"category6",
|
|
82
|
+
"category7",
|
|
83
|
+
"category8"
|
|
84
|
+
] as const;
|
|
85
|
+
|
|
86
|
+
function leafValue(value: number | undefined): number {
|
|
87
|
+
return Number.isFinite(value) && (value ?? 0) > 0 ? (value as number) : 0;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function sumValue(node: SunburstChartDatum): number {
|
|
91
|
+
if (node.children && node.children.length > 0) {
|
|
92
|
+
return node.children.reduce((sum, child) => sum + sumValue(child), 0);
|
|
93
|
+
}
|
|
94
|
+
return leafValue(node.value);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function maxDepth(node: SunburstChartDatum, depth = 0): number {
|
|
98
|
+
if (!node.children || node.children.length === 0) return depth;
|
|
99
|
+
return Math.max(depth, ...node.children.map((child) => maxDepth(child, depth + 1)));
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function point(cx: number, cy: number, radius: number, angle: number) {
|
|
103
|
+
return { x: cx + radius * Math.cos(angle), y: cy + radius * Math.sin(angle) };
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function arcPath(cx: number, cy: number, innerRadius: number, outerRadius: number, start: number, end: number): string {
|
|
107
|
+
const safeEnd = Math.min(end, start + Math.PI * 2 - 0.0001);
|
|
108
|
+
const large = safeEnd - start > Math.PI ? 1 : 0;
|
|
109
|
+
const outerStart = point(cx, cy, outerRadius, start);
|
|
110
|
+
const outerEnd = point(cx, cy, outerRadius, safeEnd);
|
|
111
|
+
|
|
112
|
+
if (innerRadius <= 0) {
|
|
113
|
+
return `M ${cx} ${cy} L ${outerStart.x} ${outerStart.y} A ${outerRadius} ${outerRadius} 0 ${large} 1 ${outerEnd.x} ${outerEnd.y} Z`;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const innerEnd = point(cx, cy, innerRadius, safeEnd);
|
|
117
|
+
const innerStart = point(cx, cy, innerRadius, start);
|
|
118
|
+
return `M ${outerStart.x} ${outerStart.y} A ${outerRadius} ${outerRadius} 0 ${large} 1 ${outerEnd.x} ${outerEnd.y} L ${innerEnd.x} ${innerEnd.y} A ${innerRadius} ${innerRadius} 0 ${large} 0 ${innerStart.x} ${innerStart.y} Z`;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
let hoveredIndex: number | null = $state(null);
|
|
122
|
+
|
|
123
|
+
const arcs = $derived.by<ArcDatum[]>(() => {
|
|
124
|
+
const total = sumValue(data);
|
|
125
|
+
if (total <= 0 || !data.children || data.children.length === 0) return [];
|
|
126
|
+
|
|
127
|
+
const cx = width / 2;
|
|
128
|
+
const cy = height / 2;
|
|
129
|
+
const ringCount = Math.max(1, maxDepth(data));
|
|
130
|
+
const outerLimit = Math.max(Math.min(width, height) / 2 - 6, 1);
|
|
131
|
+
const ring = outerLimit / ringCount;
|
|
132
|
+
const out: ArcDatum[] = [];
|
|
133
|
+
|
|
134
|
+
function visit(
|
|
135
|
+
node: SunburstChartDatum,
|
|
136
|
+
depth: number,
|
|
137
|
+
start: number,
|
|
138
|
+
end: number,
|
|
139
|
+
pathLabel: string[],
|
|
140
|
+
inheritedTone: SunburstChartTone,
|
|
141
|
+
siblingIndex: number
|
|
142
|
+
) {
|
|
143
|
+
if (depth > 0) {
|
|
144
|
+
const tone = node.tone ?? inheritedTone ?? TONES[siblingIndex % TONES.length];
|
|
145
|
+
const innerRadius = (depth - 1) * ring;
|
|
146
|
+
const outerRadius = depth * ring;
|
|
147
|
+
const midAngle = (start + end) / 2;
|
|
148
|
+
const midRadius = (innerRadius + outerRadius) / 2;
|
|
149
|
+
const labelPoint = point(cx, cy, midRadius, midAngle);
|
|
150
|
+
out.push({
|
|
151
|
+
datum: node,
|
|
152
|
+
pathLabel,
|
|
153
|
+
value: sumValue(node),
|
|
154
|
+
tone,
|
|
155
|
+
depth,
|
|
156
|
+
start,
|
|
157
|
+
end,
|
|
158
|
+
path: arcPath(cx, cy, innerRadius, outerRadius, start, end),
|
|
159
|
+
labelX: labelPoint.x,
|
|
160
|
+
labelY: labelPoint.y
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const children = node.children ?? [];
|
|
165
|
+
const nodeTotal = children.reduce((sum, child) => sum + sumValue(child), 0);
|
|
166
|
+
if (children.length === 0 || nodeTotal <= 0) return;
|
|
167
|
+
|
|
168
|
+
let cursor = start;
|
|
169
|
+
children.forEach((child, childIndex) => {
|
|
170
|
+
const value = sumValue(child);
|
|
171
|
+
if (value <= 0) return;
|
|
172
|
+
const span = ((end - start) * value) / nodeTotal;
|
|
173
|
+
const tone = child.tone ?? (depth === 0 ? TONES[childIndex % TONES.length] : inheritedTone);
|
|
174
|
+
visit(child, depth + 1, cursor, cursor + span, [...pathLabel, child.label], tone, childIndex);
|
|
175
|
+
cursor += span;
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
visit(data, 0, -Math.PI / 2, Math.PI * 1.5, [data.label], "category1", 0);
|
|
180
|
+
return out;
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
const leafItems = $derived.by(() => {
|
|
184
|
+
const items: string[] = [];
|
|
185
|
+
function collect(node: SunburstChartDatum, path: string[]) {
|
|
186
|
+
if (node.children && node.children.length > 0) {
|
|
187
|
+
node.children.forEach((child) => collect(child, [...path, child.label]));
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
items.push(`${path.join(", ")}: ${leafValue(node.value)}`);
|
|
191
|
+
}
|
|
192
|
+
collect(data, [data.label]);
|
|
193
|
+
return items.filter((item) => !item.endsWith(": 0"));
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
const legendItems = $derived(
|
|
197
|
+
(data.children ?? []).map((child, index) => ({
|
|
198
|
+
label: child.label,
|
|
199
|
+
tone: child.tone ?? TONES[index % TONES.length]
|
|
200
|
+
}))
|
|
201
|
+
);
|
|
202
|
+
|
|
203
|
+
function handleVisualPointerMove(event: PointerEvent) {
|
|
204
|
+
const target = event.target;
|
|
205
|
+
if (!(target instanceof Element)) {
|
|
206
|
+
hoveredIndex = null;
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
const index = Number(target.getAttribute("data-chart-index"));
|
|
210
|
+
hoveredIndex = Number.isInteger(index) ? index : null;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
const classes = () => ["st-sunburstChart", className].filter(Boolean).join(" ");
|
|
214
|
+
</script>
|
|
215
|
+
|
|
216
|
+
<div class={classes()}>
|
|
217
|
+
<div
|
|
218
|
+
class="st-sunburstChart__visual"
|
|
219
|
+
role="img"
|
|
220
|
+
aria-label={label}
|
|
221
|
+
onpointermove={handleVisualPointerMove}
|
|
222
|
+
onpointerleave={() => (hoveredIndex = null)}
|
|
223
|
+
>
|
|
224
|
+
<svg
|
|
225
|
+
viewBox="0 0 {width} {height}"
|
|
226
|
+
preserveAspectRatio="xMidYMid meet"
|
|
227
|
+
width="100%"
|
|
228
|
+
height="100%"
|
|
229
|
+
focusable="false"
|
|
230
|
+
aria-hidden="true"
|
|
231
|
+
>
|
|
232
|
+
{#each arcs as arc, i (arc.pathLabel.join("/"))}
|
|
233
|
+
<path
|
|
234
|
+
class="st-sunburstChart__arc st-sunburstChart__arc--{arc.tone}"
|
|
235
|
+
class:st-sunburstChart__arc--dim={hoveredIndex !== null && hoveredIndex !== i}
|
|
236
|
+
d={arc.path}
|
|
237
|
+
data-chart-index={i}
|
|
238
|
+
/>
|
|
239
|
+
{/each}
|
|
240
|
+
|
|
241
|
+
{#each arcs as arc (arc.pathLabel.join("/"))}
|
|
242
|
+
{#if arc.end - arc.start > 0.28}
|
|
243
|
+
<text
|
|
244
|
+
class="st-sunburstChart__label"
|
|
245
|
+
x={arc.labelX}
|
|
246
|
+
y={arc.labelY}
|
|
247
|
+
text-anchor="middle"
|
|
248
|
+
dominant-baseline="middle"
|
|
249
|
+
fill={contrastTextForTone(arc.tone)}
|
|
250
|
+
>
|
|
251
|
+
{arc.datum.label}
|
|
252
|
+
</text>
|
|
253
|
+
{/if}
|
|
254
|
+
{/each}
|
|
255
|
+
</svg>
|
|
256
|
+
</div>
|
|
257
|
+
|
|
258
|
+
<ChartDataList {label} items={leafItems} />
|
|
259
|
+
|
|
260
|
+
{#if hoveredIndex !== null && arcs[hoveredIndex]}
|
|
261
|
+
{@const arc = arcs[hoveredIndex]}
|
|
262
|
+
<div
|
|
263
|
+
class="st-sunburstChart__tooltip"
|
|
264
|
+
role="presentation"
|
|
265
|
+
style="left: {(arc.labelX / width) * 100}%; top: {(arc.labelY / height) * 100}%"
|
|
266
|
+
>
|
|
267
|
+
<span class="st-sunburstChart__tooltipLabel">{arc.pathLabel.join(", ")}</span>
|
|
268
|
+
<span class="st-sunburstChart__tooltipValue">{arc.value}</span>
|
|
269
|
+
</div>
|
|
270
|
+
{/if}
|
|
271
|
+
|
|
272
|
+
{#if legend && legendItems.length > 0}
|
|
273
|
+
<ul class="st-sunburstChart__legend" aria-hidden="true">
|
|
274
|
+
{#each legendItems as item (item.label)}
|
|
275
|
+
<li class="st-sunburstChart__legendItem">
|
|
276
|
+
<span class="st-sunburstChart__legendSwatch st-sunburstChart__legendSwatch--{item.tone}"></span>
|
|
277
|
+
{item.label}
|
|
278
|
+
</li>
|
|
279
|
+
{/each}
|
|
280
|
+
</ul>
|
|
281
|
+
{/if}
|
|
282
|
+
</div>
|
|
283
|
+
|
|
284
|
+
<style>
|
|
285
|
+
.st-sunburstChart {
|
|
286
|
+
color: var(--st-semantic-text-secondary);
|
|
287
|
+
display: block;
|
|
288
|
+
font-family: inherit;
|
|
289
|
+
max-width: 100%;
|
|
290
|
+
position: relative;
|
|
291
|
+
width: 100%;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
.st-sunburstChart svg,
|
|
295
|
+
.st-sunburstChart__visual {
|
|
296
|
+
display: block;
|
|
297
|
+
overflow: visible;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
.st-sunburstChart__arc {
|
|
301
|
+
cursor: pointer;
|
|
302
|
+
stroke: var(--st-semantic-surface-default, Canvas);
|
|
303
|
+
stroke-width: 1;
|
|
304
|
+
transition: opacity 120ms ease;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
.st-sunburstChart__arc--dim {
|
|
308
|
+
opacity: 0.4;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
.st-sunburstChart__arc--category1,
|
|
312
|
+
.st-sunburstChart__legendSwatch--category1 { fill: var(--st-semantic-data-category1); background: var(--st-semantic-data-category1); }
|
|
313
|
+
.st-sunburstChart__arc--category2,
|
|
314
|
+
.st-sunburstChart__legendSwatch--category2 { fill: var(--st-semantic-data-category2); background: var(--st-semantic-data-category2); }
|
|
315
|
+
.st-sunburstChart__arc--category3,
|
|
316
|
+
.st-sunburstChart__legendSwatch--category3 { fill: var(--st-semantic-data-category3); background: var(--st-semantic-data-category3); }
|
|
317
|
+
.st-sunburstChart__arc--category4,
|
|
318
|
+
.st-sunburstChart__legendSwatch--category4 { fill: var(--st-semantic-data-category4); background: var(--st-semantic-data-category4); }
|
|
319
|
+
.st-sunburstChart__arc--category5,
|
|
320
|
+
.st-sunburstChart__legendSwatch--category5 { fill: var(--st-semantic-data-category5); background: var(--st-semantic-data-category5); }
|
|
321
|
+
.st-sunburstChart__arc--category6,
|
|
322
|
+
.st-sunburstChart__legendSwatch--category6 { fill: var(--st-semantic-data-category6); background: var(--st-semantic-data-category6); }
|
|
323
|
+
.st-sunburstChart__arc--category7,
|
|
324
|
+
.st-sunburstChart__legendSwatch--category7 { fill: var(--st-semantic-data-category7); background: var(--st-semantic-data-category7); }
|
|
325
|
+
.st-sunburstChart__arc--category8,
|
|
326
|
+
.st-sunburstChart__legendSwatch--category8 { fill: var(--st-semantic-data-category8); background: var(--st-semantic-data-category8); }
|
|
327
|
+
|
|
328
|
+
.st-sunburstChart__label {
|
|
329
|
+
/* fill calculé par contrastTextForTone() en inline - pas de blanc fixe */
|
|
330
|
+
font-size: 0.68rem;
|
|
331
|
+
font-weight: 650;
|
|
332
|
+
pointer-events: none;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
@media (prefers-reduced-motion: reduce) {
|
|
336
|
+
.st-sunburstChart__arc {
|
|
337
|
+
transition: none;
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
.st-sunburstChart__legend {
|
|
342
|
+
display: flex;
|
|
343
|
+
flex-wrap: wrap;
|
|
344
|
+
gap: var(--st-spacing-2, 0.5rem) var(--st-spacing-4, 1rem);
|
|
345
|
+
list-style: none;
|
|
346
|
+
margin: var(--st-spacing-2, 0.5rem) 0 0;
|
|
347
|
+
padding: 0;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
.st-sunburstChart__legendItem {
|
|
351
|
+
align-items: center;
|
|
352
|
+
color: var(--st-semantic-text-secondary);
|
|
353
|
+
display: inline-flex;
|
|
354
|
+
font-size: 0.75rem;
|
|
355
|
+
gap: var(--st-spacing-2, 0.5rem);
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
.st-sunburstChart__legendSwatch {
|
|
359
|
+
display: inline-block;
|
|
360
|
+
height: 0.625rem;
|
|
361
|
+
width: 0.625rem;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
.st-sunburstChart__tooltip {
|
|
365
|
+
background: var(--st-semantic-surface-inverse);
|
|
366
|
+
border-radius: var(--st-radius-sm, 0.25rem);
|
|
367
|
+
color: var(--st-semantic-text-inverse);
|
|
368
|
+
display: inline-flex;
|
|
369
|
+
flex-direction: column;
|
|
370
|
+
font-size: 0.75rem;
|
|
371
|
+
gap: 0.125rem;
|
|
372
|
+
line-height: 1.2;
|
|
373
|
+
padding: 0.375rem 0.5rem;
|
|
374
|
+
pointer-events: none;
|
|
375
|
+
position: absolute;
|
|
376
|
+
transform: translate(-50%, -115%);
|
|
377
|
+
white-space: nowrap;
|
|
378
|
+
z-index: 1;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
.st-sunburstChart__tooltipLabel {
|
|
382
|
+
font-weight: 600;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
.st-sunburstChart__tooltipValue {
|
|
386
|
+
opacity: 0.85;
|
|
387
|
+
}
|
|
388
|
+
</style>
|