@sentropic/design-system-svelte 0.34.34 → 0.34.36

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.
@@ -0,0 +1,416 @@
1
+ <script lang="ts" module>
2
+ /**
3
+ * StatusHistoryChart - grille TEMPS × ENTITÉ dont chaque cellule encode un
4
+ * STATUT DISCRET (façon Grafana « status history »). Une ligne (series) =
5
+ * une séquence de buckets temporels ; chaque bucket est une cellule colorée
6
+ * par son statut. Distinct de HeatmapChart (dégradé continu Low→High) : la
7
+ * coloration suit un statut discret, comme StateTimelineChart encode l'état,
8
+ * mais en GRILLE de buckets au lieu de segments contigus.
9
+ * API canonique (référence Svelte, React/Vue doivent s'aligner).
10
+ *
11
+ * Modèle : lignes = series, colonnes = buckets temporels (ordre d'apparition
12
+ * des `at`). La couleur suit `tone` si fourni, sinon dérive une teinte stable
13
+ * par `value` (cycle sur category1..8). Étiquette de series à gauche
14
+ * (ellipsis). Légende des statuts sous le graphe via ChartDataList.
15
+ *
16
+ * Props obligatoires :
17
+ * data StatusHistorySeries[] - tableau {series, buckets[]}
18
+ *
19
+ * Props optionnelles :
20
+ * label string
21
+ * width number (défaut 640)
22
+ * height number (défaut 320)
23
+ * size number (alias de width)
24
+ * class string
25
+ */
26
+ export type StatusHistoryTone =
27
+ | "neutral" | "info" | "success" | "warning" | "error"
28
+ | "category1" | "category2" | "category3" | "category4"
29
+ | "category5" | "category6" | "category7" | "category8";
30
+
31
+ export type StatusHistoryBucket = {
32
+ at: number;
33
+ value: string | number;
34
+ tone?: StatusHistoryTone;
35
+ };
36
+
37
+ export type StatusHistorySeries = {
38
+ series: string;
39
+ buckets: StatusHistoryBucket[];
40
+ };
41
+ </script>
42
+
43
+ <script lang="ts">
44
+ import ChartDataList from "./ChartDataList.svelte";
45
+
46
+ type StatusHistoryChartProps = {
47
+ data: StatusHistorySeries[];
48
+ label?: string;
49
+ width?: number;
50
+ height?: number;
51
+ size?: number;
52
+ class?: string;
53
+ };
54
+
55
+ let {
56
+ data = [],
57
+ label,
58
+ width,
59
+ height = 320,
60
+ size,
61
+ class: className
62
+ }: StatusHistoryChartProps = $props();
63
+
64
+ const resolvedWidth = $derived(width ?? size ?? 640);
65
+
66
+ const MARGIN = { top: 16, right: 16, bottom: 32, left: 132 };
67
+
68
+ // Tronque une étiquette à la largeur de la marge gauche (approx. par char).
69
+ function ellipsize(text: string, maxChars: number): string {
70
+ if (text.length <= maxChars) return text;
71
+ if (maxChars <= 1) return "…";
72
+ return `${text.slice(0, maxChars - 1)}…`;
73
+ }
74
+
75
+ function formatTick(v: number): string {
76
+ if (Math.abs(v) >= 1000) return `${(v / 1000).toFixed(v % 1000 === 0 ? 0 : 1)}k`;
77
+ if (Number.isInteger(v)) return String(v);
78
+ return v.toFixed(1);
79
+ }
80
+
81
+ let hoveredKey: string | null = $state(null);
82
+
83
+ const plotWidth = $derived(Math.max(resolvedWidth - MARGIN.left - MARGIN.right, 1));
84
+ const plotHeight = $derived(Math.max(height - MARGIN.top - MARGIN.bottom, 1));
85
+
86
+ // Normalise : filtre les series sans libellé et les buckets non finis.
87
+ const validData = $derived(
88
+ data
89
+ .filter((d) => typeof d.series === "string" && d.series.length > 0)
90
+ .map((d) => ({
91
+ series: d.series,
92
+ buckets: (d.buckets ?? [])
93
+ .filter((b) => Number.isFinite(b.at))
94
+ .map((b) => ({ at: b.at, value: b.value, tone: b.tone }))
95
+ }))
96
+ );
97
+
98
+ // Colonnes temporelles distinctes (par `at`, croissant).
99
+ const columnOrder = $derived.by(() => {
100
+ const seen: number[] = [];
101
+ for (const d of validData) {
102
+ for (const b of d.buckets) {
103
+ if (!seen.includes(b.at)) seen.push(b.at);
104
+ }
105
+ }
106
+ return seen.sort((a, b) => a - b);
107
+ });
108
+
109
+ // Statuts distincts (ordre d'apparition) → index categoryN (1..8, cyclé) si
110
+ // aucun `tone` explicite. Un `tone` sur un bucket l'emporte sur la dérivation.
111
+ const statusOrder = $derived.by(() => {
112
+ const seen: string[] = [];
113
+ for (const d of validData) {
114
+ for (const b of d.buckets) {
115
+ const key = String(b.value);
116
+ if (!seen.includes(key)) seen.push(key);
117
+ }
118
+ }
119
+ return seen;
120
+ });
121
+ const explicitToneByStatus = $derived.by(() => {
122
+ const map = new Map<string, StatusHistoryTone>();
123
+ for (const d of validData) {
124
+ for (const b of d.buckets) {
125
+ if (b.tone) map.set(String(b.value), b.tone);
126
+ }
127
+ }
128
+ return map;
129
+ });
130
+ const toneOf = (bucket: { value: string | number; tone?: StatusHistoryTone }): StatusHistoryTone => {
131
+ if (bucket.tone) return bucket.tone;
132
+ const key = String(bucket.value);
133
+ const explicit = explicitToneByStatus.get(key);
134
+ if (explicit) return explicit;
135
+ const idx = statusOrder.indexOf(key);
136
+ return `category${((idx < 0 ? 0 : idx) % 8) + 1}` as StatusHistoryTone;
137
+ };
138
+ const legendItems = $derived(
139
+ statusOrder.map((value) => ({ value, tone: toneOf({ value }) }))
140
+ );
141
+ const hasLegend = $derived(statusOrder.length > 0);
142
+
143
+ const rows = $derived.by(() => {
144
+ if (validData.length === 0 || columnOrder.length === 0) return [];
145
+ const band = plotHeight / validData.length;
146
+ const rowHeight = Math.min(band * 0.62, 28);
147
+ const colWidth = plotWidth / columnOrder.length;
148
+ return validData.map((d, i) => {
149
+ const y = MARGIN.top + band * i + (band - rowHeight) / 2;
150
+ const cells = d.buckets.map((b, j) => {
151
+ const colIndex = Math.max(0, columnOrder.indexOf(b.at));
152
+ const x = MARGIN.left + colIndex * colWidth;
153
+ const w = Math.max(colWidth - 2, 1);
154
+ return {
155
+ key: `${i}-${j}`,
156
+ datum: b,
157
+ x,
158
+ width: w,
159
+ cx: x + w / 2,
160
+ tone: toneOf(b)
161
+ };
162
+ });
163
+ return {
164
+ datum: d,
165
+ index: i,
166
+ y,
167
+ height: rowHeight,
168
+ rowCenterY: MARGIN.top + band * (i + 0.5),
169
+ cells
170
+ };
171
+ });
172
+ });
173
+
174
+ const columns = $derived.by(() => {
175
+ if (columnOrder.length === 0) return [];
176
+ const colWidth = plotWidth / columnOrder.length;
177
+ return columnOrder.map((at, index) => ({
178
+ at,
179
+ cx: MARGIN.left + (index + 0.5) * colWidth
180
+ }));
181
+ });
182
+
183
+ const dataValueItems = $derived(
184
+ validData.map(
185
+ (d) => `${d.series}: ${d.buckets.map((b) => `${b.at} = ${b.value}`).join(", ")}`
186
+ )
187
+ );
188
+
189
+ function handlePointerMove(event: PointerEvent) {
190
+ const target = event.target;
191
+ if (!(target instanceof Element)) {
192
+ hoveredKey = null;
193
+ return;
194
+ }
195
+ const key = target.getAttribute("data-chart-key");
196
+ hoveredKey = key ?? null;
197
+ }
198
+
199
+ const hoveredCell = $derived.by(() => {
200
+ if (hoveredKey === null) return null;
201
+ for (const row of rows) {
202
+ for (const cell of row.cells) {
203
+ if (cell.key === hoveredKey) return { row, cell };
204
+ }
205
+ }
206
+ return null;
207
+ });
208
+
209
+ const classes = () => ["st-statusHistoryChart", className].filter(Boolean).join(" ");
210
+ </script>
211
+
212
+ <div class={classes()}>
213
+ <div
214
+ class="st-statusHistoryChart__visual"
215
+ role="img"
216
+ aria-label={label}
217
+ onpointermove={handlePointerMove}
218
+ onpointerleave={() => (hoveredKey = null)}
219
+ >
220
+ <svg
221
+ viewBox="0 0 {resolvedWidth} {height}"
222
+ preserveAspectRatio="xMidYMid meet"
223
+ width="100%"
224
+ height="100%"
225
+ focusable="false"
226
+ aria-hidden="true"
227
+ >
228
+ <!-- tick labels (axe X temporel) -->
229
+ {#each columns as column (column.at)}
230
+ <text class="st-statusHistoryChart__tickLabel" x={column.cx} y={height - MARGIN.bottom + 16} text-anchor="middle">
231
+ {formatTick(column.at)}
232
+ </text>
233
+ {/each}
234
+
235
+ <!-- axes -->
236
+ <line class="st-statusHistoryChart__axis" x1={MARGIN.left} x2={MARGIN.left} y1={MARGIN.top} y2={height - MARGIN.bottom} />
237
+ <line class="st-statusHistoryChart__axis" x1={MARGIN.left} x2={resolvedWidth - MARGIN.right} y1={height - MARGIN.bottom} y2={height - MARGIN.bottom} />
238
+
239
+ <!-- une ligne par series : étiquette à gauche + cellules de statut par bucket -->
240
+ {#each rows as row (`${row.index}-${row.datum.series}`)}
241
+ <text
242
+ class="st-statusHistoryChart__seriesLabel"
243
+ x={MARGIN.left - 8}
244
+ y={row.rowCenterY}
245
+ text-anchor="end"
246
+ dominant-baseline="middle"
247
+ >
248
+ {ellipsize(row.datum.series, 18)}
249
+ </text>
250
+ {#each row.cells as cell (cell.key)}
251
+ <rect
252
+ class="st-statusHistoryChart__cell st-statusHistoryChart__cell--{cell.tone}"
253
+ class:st-statusHistoryChart__cell--dim={hoveredKey !== null && hoveredKey !== cell.key}
254
+ x={cell.x}
255
+ y={row.y}
256
+ width={cell.width}
257
+ height={row.height}
258
+ rx="2"
259
+ data-chart-key={cell.key}
260
+ />
261
+ {/each}
262
+ {/each}
263
+ </svg>
264
+ </div>
265
+
266
+ {#if hasLegend}
267
+ <ul class="st-statusHistoryChart__legend" aria-label={`Statuts de ${label ?? "status history"}`}>
268
+ {#each legendItems as item (item.value)}
269
+ <li class="st-statusHistoryChart__legendItem">
270
+ <span class="st-statusHistoryChart__legendSwatch st-statusHistoryChart__legendSwatch--{item.tone}" aria-hidden="true"></span>
271
+ {item.value}
272
+ </li>
273
+ {/each}
274
+ </ul>
275
+ {/if}
276
+
277
+ <ChartDataList label={label ?? "status history"} items={dataValueItems} />
278
+
279
+ {#if hoveredCell}
280
+ {@const cell = hoveredCell.cell}
281
+ {@const row = hoveredCell.row}
282
+ <div
283
+ class="st-statusHistoryChart__tooltip"
284
+ role="presentation"
285
+ style="left: {(cell.cx / resolvedWidth) * 100}%; top: {(row.rowCenterY / height) * 100}%"
286
+ >
287
+ <span class="st-statusHistoryChart__tooltipLabel">{row.datum.series} · {cell.datum.at}</span>
288
+ <span class="st-statusHistoryChart__tooltipValue">{cell.datum.value}</span>
289
+ </div>
290
+ {/if}
291
+ </div>
292
+
293
+ <style>
294
+ .st-statusHistoryChart {
295
+ color: var(--st-semantic-text-secondary);
296
+ display: block;
297
+ font-family: inherit;
298
+ position: relative;
299
+ width: 100%;
300
+ }
301
+
302
+ .st-statusHistoryChart svg {
303
+ display: block;
304
+ overflow: visible;
305
+ }
306
+
307
+ .st-statusHistoryChart__visual {
308
+ display: block;
309
+ }
310
+
311
+ .st-statusHistoryChart__axis {
312
+ stroke: var(--st-semantic-border-subtle);
313
+ stroke-width: 1;
314
+ }
315
+
316
+ .st-statusHistoryChart__tickLabel,
317
+ .st-statusHistoryChart__seriesLabel {
318
+ fill: var(--st-semantic-text-secondary);
319
+ font-size: 0.6875rem;
320
+ }
321
+
322
+ .st-statusHistoryChart__cell {
323
+ cursor: pointer;
324
+ stroke: var(--st-semantic-surface-default, Canvas);
325
+ stroke-width: 1;
326
+ transition: opacity 120ms ease;
327
+ }
328
+
329
+ .st-statusHistoryChart__cell--dim {
330
+ opacity: 0.4;
331
+ }
332
+
333
+ .st-statusHistoryChart__cell--neutral { fill: var(--st-semantic-border-strong, var(--st-semantic-surface-subtle)); }
334
+ .st-statusHistoryChart__cell--info { fill: var(--st-semantic-feedback-info, var(--st-semantic-action-primary)); }
335
+ .st-statusHistoryChart__cell--success { fill: var(--st-semantic-feedback-success); }
336
+ .st-statusHistoryChart__cell--warning { fill: var(--st-semantic-feedback-warning); }
337
+ .st-statusHistoryChart__cell--error { fill: var(--st-semantic-feedback-error); }
338
+
339
+ .st-statusHistoryChart__cell--category1 { fill: var(--st-semantic-data-category1); }
340
+ .st-statusHistoryChart__cell--category2 { fill: var(--st-semantic-data-category2); }
341
+ .st-statusHistoryChart__cell--category3 { fill: var(--st-semantic-data-category3); }
342
+ .st-statusHistoryChart__cell--category4 { fill: var(--st-semantic-data-category4); }
343
+ .st-statusHistoryChart__cell--category5 { fill: var(--st-semantic-data-category5); }
344
+ .st-statusHistoryChart__cell--category6 { fill: var(--st-semantic-data-category6); }
345
+ .st-statusHistoryChart__cell--category7 { fill: var(--st-semantic-data-category7); }
346
+ .st-statusHistoryChart__cell--category8 { fill: var(--st-semantic-data-category8); }
347
+
348
+ .st-statusHistoryChart__legend {
349
+ display: flex;
350
+ flex-wrap: wrap;
351
+ gap: var(--st-spacing-3, 0.75rem);
352
+ list-style: none;
353
+ margin: var(--st-spacing-2, 0.5rem) 0 0 0;
354
+ padding: 0;
355
+ }
356
+
357
+ .st-statusHistoryChart__legendItem {
358
+ align-items: center;
359
+ color: var(--st-semantic-text-secondary);
360
+ display: inline-flex;
361
+ font-size: 0.75rem;
362
+ gap: var(--st-spacing-1, 0.25rem);
363
+ line-height: 1;
364
+ }
365
+
366
+ .st-statusHistoryChart__legendSwatch {
367
+ border-radius: var(--st-radius-sm, 0.25rem);
368
+ display: inline-block;
369
+ height: 0.625rem;
370
+ width: 0.625rem;
371
+ }
372
+ .st-statusHistoryChart__legendSwatch--neutral { background: var(--st-semantic-border-strong, var(--st-semantic-surface-subtle)); }
373
+ .st-statusHistoryChart__legendSwatch--info { background: var(--st-semantic-feedback-info, var(--st-semantic-action-primary)); }
374
+ .st-statusHistoryChart__legendSwatch--success { background: var(--st-semantic-feedback-success); }
375
+ .st-statusHistoryChart__legendSwatch--warning { background: var(--st-semantic-feedback-warning); }
376
+ .st-statusHistoryChart__legendSwatch--error { background: var(--st-semantic-feedback-error); }
377
+ .st-statusHistoryChart__legendSwatch--category1 { background: var(--st-semantic-data-category1); }
378
+ .st-statusHistoryChart__legendSwatch--category2 { background: var(--st-semantic-data-category2); }
379
+ .st-statusHistoryChart__legendSwatch--category3 { background: var(--st-semantic-data-category3); }
380
+ .st-statusHistoryChart__legendSwatch--category4 { background: var(--st-semantic-data-category4); }
381
+ .st-statusHistoryChart__legendSwatch--category5 { background: var(--st-semantic-data-category5); }
382
+ .st-statusHistoryChart__legendSwatch--category6 { background: var(--st-semantic-data-category6); }
383
+ .st-statusHistoryChart__legendSwatch--category7 { background: var(--st-semantic-data-category7); }
384
+ .st-statusHistoryChart__legendSwatch--category8 { background: var(--st-semantic-data-category8); }
385
+
386
+ .st-statusHistoryChart__tooltip {
387
+ background: var(--st-semantic-surface-inverse);
388
+ border-radius: var(--st-radius-sm, 0.25rem);
389
+ color: var(--st-semantic-text-inverse);
390
+ display: inline-flex;
391
+ flex-direction: column;
392
+ font-size: 0.75rem;
393
+ gap: 0.125rem;
394
+ line-height: 1.2;
395
+ padding: 0.375rem 0.5rem;
396
+ pointer-events: none;
397
+ position: absolute;
398
+ transform: translate(-50%, calc(-100% - 8px));
399
+ white-space: nowrap;
400
+ z-index: 1;
401
+ }
402
+
403
+ .st-statusHistoryChart__tooltipLabel {
404
+ font-weight: 600;
405
+ }
406
+
407
+ .st-statusHistoryChart__tooltipValue {
408
+ opacity: 0.85;
409
+ }
410
+
411
+ @media (prefers-reduced-motion: reduce) {
412
+ .st-statusHistoryChart__cell {
413
+ transition: none;
414
+ }
415
+ }
416
+ </style>
@@ -0,0 +1,46 @@
1
+ /**
2
+ * StatusHistoryChart - grille TEMPS × ENTITÉ dont chaque cellule encode un
3
+ * STATUT DISCRET (façon Grafana « status history »). Une ligne (series) =
4
+ * une séquence de buckets temporels ; chaque bucket est une cellule colorée
5
+ * par son statut. Distinct de HeatmapChart (dégradé continu Low→High) : la
6
+ * coloration suit un statut discret, comme StateTimelineChart encode l'état,
7
+ * mais en GRILLE de buckets au lieu de segments contigus.
8
+ * API canonique (référence Svelte, React/Vue doivent s'aligner).
9
+ *
10
+ * Modèle : lignes = series, colonnes = buckets temporels (ordre d'apparition
11
+ * des `at`). La couleur suit `tone` si fourni, sinon dérive une teinte stable
12
+ * par `value` (cycle sur category1..8). Étiquette de series à gauche
13
+ * (ellipsis). Légende des statuts sous le graphe via ChartDataList.
14
+ *
15
+ * Props obligatoires :
16
+ * data StatusHistorySeries[] - tableau {series, buckets[]}
17
+ *
18
+ * Props optionnelles :
19
+ * label string
20
+ * width number (défaut 640)
21
+ * height number (défaut 320)
22
+ * size number (alias de width)
23
+ * class string
24
+ */
25
+ export type StatusHistoryTone = "neutral" | "info" | "success" | "warning" | "error" | "category1" | "category2" | "category3" | "category4" | "category5" | "category6" | "category7" | "category8";
26
+ export type StatusHistoryBucket = {
27
+ at: number;
28
+ value: string | number;
29
+ tone?: StatusHistoryTone;
30
+ };
31
+ export type StatusHistorySeries = {
32
+ series: string;
33
+ buckets: StatusHistoryBucket[];
34
+ };
35
+ type StatusHistoryChartProps = {
36
+ data: StatusHistorySeries[];
37
+ label?: string;
38
+ width?: number;
39
+ height?: number;
40
+ size?: number;
41
+ class?: string;
42
+ };
43
+ declare const StatusHistoryChart: import("svelte").Component<StatusHistoryChartProps, {}, "">;
44
+ type StatusHistoryChart = ReturnType<typeof StatusHistoryChart>;
45
+ export default StatusHistoryChart;
46
+ //# sourceMappingURL=StatusHistoryChart.svelte.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"StatusHistoryChart.svelte.d.ts","sourceRoot":"","sources":["../src/lib/StatusHistoryChart.svelte.ts"],"names":[],"mappings":"AAGE;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,MAAM,MAAM,iBAAiB,GACzB,SAAS,GAAG,MAAM,GAAG,SAAS,GAAG,SAAS,GAAG,OAAO,GACpD,WAAW,GAAG,WAAW,GAAG,WAAW,GAAG,WAAW,GACrD,WAAW,GAAG,WAAW,GAAG,WAAW,GAAG,WAAW,CAAC;AAE1D,MAAM,MAAM,mBAAmB,GAAG;IAChC,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,GAAG,MAAM,CAAC;IACvB,IAAI,CAAC,EAAE,iBAAiB,CAAC;CAC1B,CAAC;AAEF,MAAM,MAAM,mBAAmB,GAAG;IAChC,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,mBAAmB,EAAE,CAAC;CAChC,CAAC;AAMF,KAAK,uBAAuB,GAAG;IAC7B,IAAI,EAAE,mBAAmB,EAAE,CAAC;IAC5B,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB,CAAC;AAyNJ,QAAA,MAAM,kBAAkB,6DAAwC,CAAC;AACjE,KAAK,kBAAkB,GAAG,UAAU,CAAC,OAAO,kBAAkB,CAAC,CAAC;AAChE,eAAe,kBAAkB,CAAC"}