@sentropic/design-system-svelte 0.34.28 → 0.34.33

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/Badge.svelte +66 -2
  2. package/dist/Badge.svelte.d.ts +21 -0
  3. package/dist/Badge.svelte.d.ts.map +1 -1
  4. package/dist/CandlestickChart.svelte +286 -11
  5. package/dist/CandlestickChart.svelte.d.ts +17 -3
  6. package/dist/CandlestickChart.svelte.d.ts.map +1 -1
  7. package/dist/CellDecorationIcon.svelte +39 -0
  8. package/dist/CellDecorationIcon.svelte.d.ts +7 -0
  9. package/dist/CellDecorationIcon.svelte.d.ts.map +1 -0
  10. package/dist/Collapsible.svelte +55 -1
  11. package/dist/Collapsible.svelte.d.ts +15 -0
  12. package/dist/Collapsible.svelte.d.ts.map +1 -1
  13. package/dist/Collapsible.test.d.ts +2 -0
  14. package/dist/Collapsible.test.d.ts.map +1 -0
  15. package/dist/Collapsible.test.js +68 -0
  16. package/dist/ComboChart.svelte +333 -2
  17. package/dist/ComboChart.svelte.d.ts +34 -0
  18. package/dist/ComboChart.svelte.d.ts.map +1 -1
  19. package/dist/DataTable.svelte +91 -2
  20. package/dist/DataTable.svelte.d.ts +12 -0
  21. package/dist/DataTable.svelte.d.ts.map +1 -1
  22. package/dist/KpiCard.svelte +66 -1
  23. package/dist/KpiCard.svelte.d.ts +7 -0
  24. package/dist/KpiCard.svelte.d.ts.map +1 -1
  25. package/dist/OHLCChart.svelte +286 -11
  26. package/dist/OHLCChart.svelte.d.ts +17 -3
  27. package/dist/OHLCChart.svelte.d.ts.map +1 -1
  28. package/dist/ScatterPlot.svelte +260 -6
  29. package/dist/ScatterPlot.svelte.d.ts +25 -0
  30. package/dist/ScatterPlot.svelte.d.ts.map +1 -1
  31. package/dist/SelectableList.svelte +36 -17
  32. package/dist/SelectableList.svelte.d.ts.map +1 -1
  33. package/dist/SelectableRow.svelte +53 -1
  34. package/dist/SelectableRow.svelte.d.ts +10 -0
  35. package/dist/SelectableRow.svelte.d.ts.map +1 -1
  36. package/dist/cellDecoration.d.ts +36 -0
  37. package/dist/cellDecoration.d.ts.map +1 -0
  38. package/dist/cellDecoration.js +71 -0
  39. package/dist/index.d.ts +1 -0
  40. package/dist/index.d.ts.map +1 -1
  41. package/package.json +3 -3
@@ -26,6 +26,15 @@
26
26
 
27
27
  <script lang="ts">
28
28
  import ChartDataList from "./ChartDataList.svelte";
29
+ import {
30
+ annotationDataListItems,
31
+ polygonPoints,
32
+ resolveAnnotations,
33
+ type ChartAnnotation,
34
+ } from "./chartAnnotations.js";
35
+ import { formatDataLabel, normalizeDataLabels, type DataLabelsProp } from "./chartDataLabels.js";
36
+ import { resolveActiveIndex } from "./chartCrosshair.js";
37
+ import { datapointAriaLabel, datapointNavAction, rovingTabIndex } from "./chartKeyboardNav.js";
29
38
 
30
39
  type ScatterPlotProps = {
31
40
  data: ScatterPlotDatum[];
@@ -39,6 +48,29 @@
39
48
  * coordinates are folded into the axis domain. Non-finite x/y are skipped.
40
49
  */
41
50
  centroids?: ScatterPlotCentroid[];
51
+ /**
52
+ * Annotation overlay in DATA space (points, labels, axis lines, regions,
53
+ * polygons). Both axes are continuous (linear). Additive: absent ⇒ unchanged.
54
+ */
55
+ annotations?: ChartAnnotation[];
56
+ /**
57
+ * Per-point value labels. `false`/absent → none. `true` → each point's value
58
+ * (the datum `label` wins when present). Object → `format`/`position`.
59
+ * Default position `top`. Labels are `aria-hidden`.
60
+ */
61
+ dataLabels?: DataLabelsProp;
62
+ /**
63
+ * CONTROLLED synchronised hover key (FR-3). A point's key is its `label` when
64
+ * present, otherwise `"x,y"`. Absent (`undefined`) keeps the uncontrolled
65
+ * behaviour.
66
+ */
67
+ hoverKey?: string | null;
68
+ /** Emitted on hover (the key) / leave (`null`); fired even while controlled. */
69
+ onHoverKeyChange?: (key: string | null) => void;
70
+ /** FR-5 — roving-tabindex keyboard navigation of the data points. */
71
+ keyboardNav?: boolean;
72
+ /** Emitted on Enter/Space select (the key) / Escape (`null`); enables nav. */
73
+ onSelectKey?: (key: string | null) => void;
42
74
  label: string;
43
75
  class?: string;
44
76
  };
@@ -51,6 +83,12 @@
51
83
  yLabel,
52
84
  radius = 5,
53
85
  centroids,
86
+ annotations,
87
+ dataLabels,
88
+ hoverKey,
89
+ onHoverKeyChange,
90
+ keyboardNav,
91
+ onSelectKey,
54
92
  label,
55
93
  class: className
56
94
  }: ScatterPlotProps = $props();
@@ -88,6 +126,9 @@
88
126
  }
89
127
 
90
128
  let hoveredIndex: number | null = $state(null);
129
+ // FR-5 — roving keyboard focus over the data points (separate from hover).
130
+ let focusedIndex: number = $state(-1);
131
+ let datapointRefs: Array<SVGRectElement | null> = [];
91
132
 
92
133
  const TONES = ["category1","category2","category3","category4","category5","category6","category7","category8"] as const;
93
134
 
@@ -95,6 +136,11 @@
95
136
  // the plot); non-finite/negative values fall back to the global radius.
96
137
  const MAX_POINT_RADIUS = 32;
97
138
 
139
+ /** Stable hover/selection key of a point: its label, else `"x,y"`. */
140
+ function keyForPoint(d: ScatterPlotDatum): string {
141
+ return d.label ?? `${d.x},${d.y}`;
142
+ }
143
+
98
144
  // Centroids guarded once: non-finite coordinates are skipped entirely.
99
145
  const validCentroids = $derived(
100
146
  (centroids ?? []).filter((c) => Number.isFinite(c.x) && Number.isFinite(c.y))
@@ -124,6 +170,7 @@
124
170
  cy: MARGIN.top + scaleLinear(d.y, yMin, yMax, plotH, 0),
125
171
  r: typeof d.r === "number" && Number.isFinite(d.r) && d.r >= 0 ? Math.min(d.r, MAX_POINT_RADIUS) : radius,
126
172
  datum: d,
173
+ index: i,
127
174
  tone: d.tone ?? TONES[i % TONES.length]
128
175
  }));
129
176
  });
@@ -138,21 +185,110 @@
138
185
  }));
139
186
  });
140
187
 
188
+ // --- Annotation overlay ----------------------------------------------------
189
+ // Both axes continuous: linear `xScale`/`yScale`, out-of-domain ⇒ null.
190
+ const annotationXScale = $derived((v: number | string): number | null => {
191
+ const { xMin, xMax, plotW } = scales;
192
+ if (typeof v !== "number" || !Number.isFinite(v)) return null;
193
+ if (v < xMin || v > xMax) return null;
194
+ return scaleLinear(v, xMin, xMax, 0, plotW);
195
+ });
196
+ const annotationYScale = $derived((v: number): number | null => {
197
+ const { yMin, yMax, plotH } = scales;
198
+ if (!Number.isFinite(v)) return null;
199
+ if (v < yMin || v > yMax) return null;
200
+ return scaleLinear(v, yMin, yMax, plotH, 0);
201
+ });
202
+ const resolvedAnnotations = $derived(
203
+ resolveAnnotations(annotations, {
204
+ xScale: annotationXScale,
205
+ yScale: annotationYScale,
206
+ plotLeft: MARGIN.left,
207
+ plotTop: MARGIN.top,
208
+ plotWidth: scales.plotW,
209
+ plotHeight: scales.plotH
210
+ })
211
+ );
212
+ const annotationRegions = $derived(resolvedAnnotations.filter((a) => a.kind === "region"));
213
+ const annotationAbove = $derived(resolvedAnnotations.filter((a) => a.kind !== "region"));
214
+
215
+ // --- Data labels -----------------------------------------------------------
216
+ // One label per point. Default `top`. The datum `label` wins; else the y.
217
+ const dataLabelOpts = $derived(normalizeDataLabels(dataLabels));
218
+ const dataLabelItems = $derived(
219
+ dataLabelOpts.enabled
220
+ ? points.map((p) => {
221
+ const text = p.datum.label ?? formatDataLabel(p.datum.y, dataLabelOpts, fmt);
222
+ const center = dataLabelOpts.position === "center" || dataLabelOpts.position === "inside";
223
+ return {
224
+ key: p.index,
225
+ x: p.cx,
226
+ y: center ? p.cy : p.cy - (p.r + 5),
227
+ text,
228
+ baseline: (center ? "middle" : "auto") as "middle" | "auto"
229
+ };
230
+ })
231
+ : []
232
+ );
233
+
141
234
  const dataValueItems = $derived([
142
235
  ...data.map((d) => (d.label ? `${d.label}: x ${d.x}, y ${d.y}` : `x ${d.x}, y ${d.y}`)),
143
236
  ...validCentroids.map((c) =>
144
237
  c.label ? `Centroïde ${c.label}: (${c.x}, ${c.y})` : `Centroïde: (${c.x}, ${c.y})`
145
- )
238
+ ),
239
+ ...annotationDataListItems(annotations)
146
240
  ]);
147
241
 
242
+ // Stable key per point (FR-3): `label` else `"x,y"`.
243
+ const hoverKeys = $derived(data.map((d) => keyForPoint(d)));
244
+ function emitHoverKey(index: number | null) {
245
+ onHoverKeyChange?.(index == null ? null : hoverKeys[index] ?? null);
246
+ }
247
+ function handleLeave() {
248
+ hoveredIndex = null;
249
+ emitHoverKey(null);
250
+ }
148
251
  function handleVisualPointerMove(event: PointerEvent) {
149
252
  const target = event.target;
150
253
  if (!(target instanceof Element)) {
151
254
  hoveredIndex = null;
255
+ emitHoverKey(null);
152
256
  return;
153
257
  }
154
- const index = Number(target.getAttribute("data-chart-index"));
155
- hoveredIndex = Number.isInteger(index) ? index : null;
258
+ const raw = Number(target.getAttribute("data-chart-index"));
259
+ const index = Number.isInteger(raw) ? raw : null;
260
+ hoveredIndex = index;
261
+ emitHoverKey(index);
262
+ }
263
+
264
+ // Index whose crosshair/tooltip is DISPLAYED: the controlled `hoverKey` when
265
+ // provided (resolved against `hoverKeys`), else the internal pointer index.
266
+ const activeIndex = $derived(resolveActiveIndex(hoverKey, hoveredIndex, hoverKeys));
267
+
268
+ // --- Keyboard navigation (FR-5) --------------------------------------------
269
+ // Active when wired explicitly (`keyboardNav`) or implicitly (`onSelectKey`).
270
+ const navEnabled = $derived((keyboardNav === true || onSelectKey !== undefined) && points.length > 0);
271
+ // Comfortable square hit area centred on each dot.
272
+ const NAV_HIT = 18;
273
+ function focusDatum(index: number) {
274
+ focusedIndex = index;
275
+ datapointRefs[index]?.focus();
276
+ emitHoverKey(index);
277
+ }
278
+ function handleDatapointKeyDown(event: KeyboardEvent, index: number) {
279
+ const action = datapointNavAction(event.key, index, points.length);
280
+ if (!action) return;
281
+ event.preventDefault();
282
+ if (action.kind === "move") {
283
+ focusDatum(action.index);
284
+ } else if (action.kind === "select") {
285
+ onSelectKey?.(hoverKeys[index] ?? null);
286
+ } else {
287
+ focusedIndex = -1;
288
+ emitHoverKey(null);
289
+ onSelectKey?.(null);
290
+ (event.currentTarget as SVGElement).blur();
291
+ }
156
292
  }
157
293
 
158
294
  const classes = () => ["st-scatterPlot", className].filter(Boolean).join(" ");
@@ -164,7 +300,7 @@
164
300
  role="img"
165
301
  aria-label={label}
166
302
  onpointermove={handleVisualPointerMove}
167
- onpointerleave={() => (hoveredIndex = null)}
303
+ onpointerleave={handleLeave}
168
304
  >
169
305
  <svg viewBox="0 0 {width} {height}" preserveAspectRatio="xMidYMid meet" width="100%" height="100%" focusable="false" aria-hidden="true">
170
306
  <!-- gridlines + ticks Y -->
@@ -190,6 +326,20 @@
190
326
  <text class="st-scatterPlot__axisLabel" x={12} y={MARGIN.top + scales.plotH / 2} text-anchor="middle" transform="rotate(-90 12 {MARGIN.top + scales.plotH / 2})">{yLabel}</text>
191
327
  {/if}
192
328
 
329
+ <!-- Annotation regions sit BEHIND the points (filled bands). -->
330
+ {#if annotationRegions.length > 0}
331
+ <g class="st-scatterPlot__annotations st-scatterPlot__annotations--behind">
332
+ {#each annotationRegions as a (a.key)}
333
+ {#if a.kind === "region"}
334
+ <rect class="st-scatterPlot__annotationRegion" x={a.x} y={a.y} width={a.width} height={a.height} />
335
+ {#if a.label}
336
+ <text class="st-scatterPlot__annotationLabel" x={a.x + 4} y={a.y + 11}>{a.label}</text>
337
+ {/if}
338
+ {/if}
339
+ {/each}
340
+ </g>
341
+ {/if}
342
+
193
343
  <!-- points -->
194
344
  {#each points as p, i (i)}
195
345
  <circle
@@ -209,13 +359,100 @@
209
359
  <line class="st-scatterPlot__centroidCross" x1={c.cx} x2={c.cx} y1={c.cy - 3.5} y2={c.cy + 3.5} />
210
360
  </g>
211
361
  {/each}
362
+
363
+ <!-- Annotations ABOVE the points: lines, shapes, points, labels. -->
364
+ {#if annotationAbove.length > 0}
365
+ <g class="st-scatterPlot__annotations st-scatterPlot__annotations--above">
366
+ {#each annotationAbove as a (a.key)}
367
+ {#if a.kind === "line"}
368
+ <line class="st-scatterPlot__annotationLine" x1={a.x1} y1={a.y1} x2={a.x2} y2={a.y2} />
369
+ {#if a.label}
370
+ <text
371
+ class="st-scatterPlot__annotationLabel"
372
+ x={a.axis === "x" ? a.x1 + 4 : MARGIN.left + scales.plotW - 4}
373
+ y={a.axis === "x" ? MARGIN.top + 11 : a.y1 - 4}
374
+ text-anchor={a.axis === "x" ? "start" : "end"}
375
+ >{a.label}</text>
376
+ {/if}
377
+ {:else if a.kind === "shape"}
378
+ <polygon class="st-scatterPlot__annotationShape" points={polygonPoints(a.points)} />
379
+ {#if a.label}
380
+ <text class="st-scatterPlot__annotationLabel" x={a.labelX} y={a.labelY} text-anchor="middle">{a.label}</text>
381
+ {/if}
382
+ {:else if a.kind === "point"}
383
+ <circle class="st-scatterPlot__annotationPoint" cx={a.x} cy={a.y} r="4.5" />
384
+ {#if a.label}
385
+ <text class="st-scatterPlot__annotationLabel" x={a.x} y={a.y - 8} text-anchor="middle">{a.label}</text>
386
+ {/if}
387
+ {:else if a.kind === "label"}
388
+ <text class="st-scatterPlot__annotationText" x={a.x} y={a.y} text-anchor={a.anchor}>{a.text}</text>
389
+ {/if}
390
+ {/each}
391
+ </g>
392
+ {/if}
393
+
394
+ <!-- Data labels — one value/label per point, drawn on top. aria-hidden. -->
395
+ {#if dataLabelItems.length > 0}
396
+ <g class="st-scatterPlot__dataLabels" aria-hidden="true">
397
+ {#each dataLabelItems as d (d.key)}
398
+ <text class="st-scatterPlot__dataLabel" x={d.x} y={d.y} text-anchor="middle" dominant-baseline={d.baseline}>{d.text}</text>
399
+ {/each}
400
+ </g>
401
+ {/if}
402
+
403
+ <!-- Crosshair (FR-3) — a tokenised CROSSED pair (vertical + horizontal) at
404
+ the active key, plus a marker on the point. Decorative (aria-hidden). -->
405
+ {#if activeIndex >= 0 && points[activeIndex]}
406
+ {@const cp = points[activeIndex]}
407
+ <g class="st-scatterPlot__crosshair" aria-hidden="true">
408
+ <line class="st-scatterPlot__crosshairLine" x1={cp.cx} x2={cp.cx} y1={MARGIN.top} y2={MARGIN.top + scales.plotH} />
409
+ <line class="st-scatterPlot__crosshairLine" x1={MARGIN.left} x2={MARGIN.left + scales.plotW} y1={cp.cy} y2={cp.cy} />
410
+ <circle class="st-scatterPlot__crosshairMarker" cx={cp.cx} cy={cp.cy} r="5" />
411
+ </g>
412
+ {/if}
212
413
  </svg>
414
+
415
+ <!-- Keyboard navigation overlay (FR-5) — a focusable, transparent hit layer
416
+ over the points. NOT aria-hidden: it is the accessible roving cursor.
417
+ Each rect announces its key + value; the focus ring is tokenised via
418
+ CSS. Absent unless keyboard nav is enabled. -->
419
+ {#if navEnabled}
420
+ <svg
421
+ class="st-scatterPlot__navLayer"
422
+ viewBox="0 0 {width} {height}"
423
+ preserveAspectRatio="xMidYMid meet"
424
+ width="100%"
425
+ height="100%"
426
+ role="group"
427
+ aria-label={`${label} — points de données`}
428
+ >
429
+ {#each points as p, i (p.index)}
430
+ <rect
431
+ bind:this={datapointRefs[i]}
432
+ class="st-scatterPlot__navDatum"
433
+ x={p.cx - NAV_HIT / 2}
434
+ y={p.cy - NAV_HIT / 2}
435
+ width={NAV_HIT}
436
+ height={NAV_HIT}
437
+ rx="3"
438
+ role="img"
439
+ tabindex={rovingTabIndex(i, focusedIndex, points.length)}
440
+ aria-label={datapointAriaLabel(p.datum.label ?? `${p.datum.x}, ${p.datum.y}`, p.datum.y)}
441
+ onkeydown={(event) => handleDatapointKeyDown(event, i)}
442
+ onfocus={() => {
443
+ focusedIndex = i;
444
+ emitHoverKey(i);
445
+ }}
446
+ />
447
+ {/each}
448
+ </svg>
449
+ {/if}
213
450
  </div>
214
451
 
215
452
  <ChartDataList {label} items={dataValueItems} />
216
453
 
217
- {#if hoveredIndex !== null && points[hoveredIndex]}
218
- {@const p = points[hoveredIndex]}
454
+ {#if activeIndex >= 0 && points[activeIndex]}
455
+ {@const p = points[activeIndex]}
219
456
  <div class="st-scatterPlot__tooltip" role="presentation" style="left: {(p.cx / width) * 100}%; top: {(p.cy / height) * 100}%">
220
457
  {#if p.datum.label}<span class="st-scatterPlot__tooltipLabel">{p.datum.label}</span>{/if}
221
458
  <span class="st-scatterPlot__tooltipValue">x {p.datum.x} · y {p.datum.y}</span>
@@ -252,6 +489,23 @@
252
489
  .st-scatterPlot__centroid--category6 { color: var(--st-semantic-data-category6); }
253
490
  .st-scatterPlot__centroid--category7 { color: var(--st-semantic-data-category7); }
254
491
  .st-scatterPlot__centroid--category8 { color: var(--st-semantic-data-category8); }
492
+ /* --- Annotation layer ----------------------------------------------------
493
+ Regions render BEHIND the points (color-mix tint, never raw opacity so the
494
+ data keeps contrast); lines/shapes/points/labels render ABOVE. */
495
+ .st-scatterPlot__annotationRegion { fill: color-mix(in srgb, var(--st-semantic-feedback-info) 12%, transparent); stroke: none; }
496
+ .st-scatterPlot__annotationLine { stroke: var(--st-semantic-feedback-info); stroke-width: 1.5; stroke-dasharray: 4 3; }
497
+ .st-scatterPlot__annotationShape { fill: color-mix(in srgb, var(--st-semantic-feedback-info) 14%, transparent); stroke: var(--st-semantic-feedback-info); stroke-width: 1.5; }
498
+ .st-scatterPlot__annotationPoint { fill: var(--st-semantic-feedback-info); stroke: var(--st-semantic-surface-default); stroke-width: 1.5; }
499
+ .st-scatterPlot__annotationLabel, .st-scatterPlot__annotationText { fill: var(--st-semantic-text-primary); font-size: 0.625rem; font-weight: 600; }
500
+ /* Data labels — per-point value/label, drawn on top. Token-only colour. */
501
+ .st-scatterPlot__dataLabel { fill: var(--st-semantic-text-primary); font-size: 0.6875rem; font-weight: 600; }
502
+ /* --- Crosshair layer (FR-3) — tokenised CROSSED pair + marker. -------------- */
503
+ .st-scatterPlot__crosshairLine { stroke: var(--st-semantic-border-strong); stroke-width: 1; stroke-dasharray: 3 3; opacity: 0.7; }
504
+ .st-scatterPlot__crosshairMarker { fill: currentColor; stroke: var(--st-semantic-surface-default); stroke-width: 2; }
505
+ /* --- Keyboard navigation layer (FR-5) — focusable transparent hit-rects. ---- */
506
+ .st-scatterPlot__navLayer { inset: 0; position: absolute; }
507
+ .st-scatterPlot__navDatum { fill: transparent; outline: none; }
508
+ .st-scatterPlot__navDatum:focus-visible { fill: color-mix(in srgb, var(--st-semantic-border-interactive) 12%, transparent); outline: 2px solid var(--st-semantic-border-interactive); outline-offset: 1px; }
255
509
  @media (prefers-reduced-motion: reduce) {
256
510
  .st-scatterPlot__point { transition: none; }
257
511
  }
@@ -17,6 +17,8 @@ export type ScatterPlotCentroid = {
17
17
  tone?: ScatterPlotTone;
18
18
  label?: string;
19
19
  };
20
+ import { type ChartAnnotation } from "./chartAnnotations.js";
21
+ import { type DataLabelsProp } from "./chartDataLabels.js";
20
22
  type ScatterPlotProps = {
21
23
  data: ScatterPlotDatum[];
22
24
  width?: number;
@@ -29,6 +31,29 @@ type ScatterPlotProps = {
29
31
  * coordinates are folded into the axis domain. Non-finite x/y are skipped.
30
32
  */
31
33
  centroids?: ScatterPlotCentroid[];
34
+ /**
35
+ * Annotation overlay in DATA space (points, labels, axis lines, regions,
36
+ * polygons). Both axes are continuous (linear). Additive: absent ⇒ unchanged.
37
+ */
38
+ annotations?: ChartAnnotation[];
39
+ /**
40
+ * Per-point value labels. `false`/absent → none. `true` → each point's value
41
+ * (the datum `label` wins when present). Object → `format`/`position`.
42
+ * Default position `top`. Labels are `aria-hidden`.
43
+ */
44
+ dataLabels?: DataLabelsProp;
45
+ /**
46
+ * CONTROLLED synchronised hover key (FR-3). A point's key is its `label` when
47
+ * present, otherwise `"x,y"`. Absent (`undefined`) keeps the uncontrolled
48
+ * behaviour.
49
+ */
50
+ hoverKey?: string | null;
51
+ /** Emitted on hover (the key) / leave (`null`); fired even while controlled. */
52
+ onHoverKeyChange?: (key: string | null) => void;
53
+ /** FR-5 — roving-tabindex keyboard navigation of the data points. */
54
+ keyboardNav?: boolean;
55
+ /** Emitted on Enter/Space select (the key) / Escape (`null`); enables nav. */
56
+ onSelectKey?: (key: string | null) => void;
32
57
  label: string;
33
58
  class?: string;
34
59
  };
@@ -1 +1 @@
1
- {"version":3,"file":"ScatterPlot.svelte.d.ts","sourceRoot":"","sources":["../src/lib/ScatterPlot.svelte.ts"],"names":[],"mappings":"AAGE,MAAM,MAAM,eAAe,GACvB,WAAW,GAAG,WAAW,GAAG,WAAW,GAAG,WAAW,GACrD,WAAW,GAAG,WAAW,GAAG,WAAW,GAAG,WAAW,CAAC;AAE1D,MAAM,MAAM,gBAAgB,GAAG;IAC7B,CAAC,EAAE,MAAM,CAAC;IACV,CAAC,EAAE,MAAM,CAAC;IACV,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,IAAI,CAAC,EAAE,eAAe,CAAC;IACvB;;;OAGG;IACH,CAAC,CAAC,EAAE,MAAM,CAAC;CACZ,CAAC;AAEF,2EAA2E;AAC3E,MAAM,MAAM,mBAAmB,GAAG;IAChC,CAAC,EAAE,MAAM,CAAC;IACV,CAAC,EAAE,MAAM,CAAC;IACV,IAAI,CAAC,EAAE,eAAe,CAAC;IACvB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB,CAAC;AAMF,KAAK,gBAAgB,GAAG;IACtB,IAAI,EAAE,gBAAgB,EAAE,CAAC;IACzB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB;;;OAGG;IACH,SAAS,CAAC,EAAE,mBAAmB,EAAE,CAAC;IAClC,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB,CAAC;AAmLJ,QAAA,MAAM,WAAW,sDAAwC,CAAC;AAC1D,KAAK,WAAW,GAAG,UAAU,CAAC,OAAO,WAAW,CAAC,CAAC;AAClD,eAAe,WAAW,CAAC"}
1
+ {"version":3,"file":"ScatterPlot.svelte.d.ts","sourceRoot":"","sources":["../src/lib/ScatterPlot.svelte.ts"],"names":[],"mappings":"AAGE,MAAM,MAAM,eAAe,GACvB,WAAW,GAAG,WAAW,GAAG,WAAW,GAAG,WAAW,GACrD,WAAW,GAAG,WAAW,GAAG,WAAW,GAAG,WAAW,CAAC;AAE1D,MAAM,MAAM,gBAAgB,GAAG;IAC7B,CAAC,EAAE,MAAM,CAAC;IACV,CAAC,EAAE,MAAM,CAAC;IACV,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,IAAI,CAAC,EAAE,eAAe,CAAC;IACvB;;;OAGG;IACH,CAAC,CAAC,EAAE,MAAM,CAAC;CACZ,CAAC;AAEF,2EAA2E;AAC3E,MAAM,MAAM,mBAAmB,GAAG;IAChC,CAAC,EAAE,MAAM,CAAC;IACV,CAAC,EAAE,MAAM,CAAC;IACV,IAAI,CAAC,EAAE,eAAe,CAAC;IACvB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB,CAAC;AAIJ,OAAO,EAIH,KAAK,eAAe,EACrB,MAAM,uBAAuB,CAAC;AACjC,OAAO,EAAwC,KAAK,cAAc,EAAE,MAAM,sBAAsB,CAAC;AAK/F,KAAK,gBAAgB,GAAG;IACtB,IAAI,EAAE,gBAAgB,EAAE,CAAC;IACzB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB;;;OAGG;IACH,SAAS,CAAC,EAAE,mBAAmB,EAAE,CAAC;IAClC;;;OAGG;IACH,WAAW,CAAC,EAAE,eAAe,EAAE,CAAC;IAChC;;;;OAIG;IACH,UAAU,CAAC,EAAE,cAAc,CAAC;IAC5B;;;;OAIG;IACH,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,gFAAgF;IAChF,gBAAgB,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,KAAK,IAAI,CAAC;IAChD,qEAAqE;IACrE,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,8EAA8E;IAC9E,WAAW,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,KAAK,IAAI,CAAC;IAC3C,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB,CAAC;AAsWJ,QAAA,MAAM,WAAW,sDAAwC,CAAC;AAC1D,KAAK,WAAW,GAAG,UAAU,CAAC,OAAO,WAAW,CAAC,CAAC;AAClD,eAAe,WAAW,CAAC"}
@@ -58,8 +58,11 @@
58
58
  let internal = $state<Set<string>>(new Set());
59
59
  const selectedValues = $derived(controlled ? toSet(value) : internal);
60
60
 
61
- // --- Row registry: ordered by DOM position so arrow nav matches the visual
62
- // order regardless of registration timing. -------------------------------
61
+ // --- Row registry. Rows are stored in INSERTION order (registration is O(1));
62
+ // the DOM-ordered view is computed LAZILY (see `orderedEntries`) and only
63
+ // when a consumer actually needs visual order (arrow nav / roving tab stop).
64
+ // Sorting eagerly on every register() was O(n) per mount → O(n²) for the
65
+ // whole list (issue #26); deferring it makes a large list mount in O(n).
63
66
  type Entry = { el: HTMLElement; value: string | undefined; disabled?: boolean };
64
67
  let entries = $state<Entry[]>([]);
65
68
 
@@ -76,17 +79,28 @@
76
79
  });
77
80
  }
78
81
 
82
+ // DOM-ordered view of the registry. Memoised by `$derived.by` so the O(n log n)
83
+ // `compareDocumentPosition` sort runs at most ONCE per registry change (a batch
84
+ // of mounts in the same tick collapses into a single recompute), not once per
85
+ // register() call. ORDER-dependent readers (`navigate`, `effectiveTabStop`'s
86
+ // "first enabled row") read THIS so visual order is correct regardless of
87
+ // registration timing; order-INDEPENDENT lookups (`valueOf`/`isSelected`,
88
+ // disabled-membership) read the raw `entries`. register() itself stays O(1).
89
+ const orderedEntries = $derived.by(() => sortByDom(entries));
90
+
79
91
  // register/unregister are called from each row's $effect. They read AND write
80
92
  // `entries`, so the read must be untracked — otherwise the calling effect would
81
93
  // subscribe to `entries`, and writing it would re-run the effect forever.
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.
94
+ // register() APPENDS in insertion order (O(1)); the DOM sort is deferred to the
95
+ // lazy `orderedEntries`. Disabled rows are registered with disabled:true so
96
+ // navigate() can skip them explicitly, making the skip correct even when the
97
+ // disabled state changes mid-session.
84
98
  function register(el: HTMLElement, rowValue: string | undefined, rowDisabled = false): () => void {
85
99
  untrack(() => {
86
- entries = sortByDom([
100
+ entries = [
87
101
  ...entries.filter((e) => e.el !== el),
88
102
  { el, value: rowValue, disabled: rowDisabled }
89
- ]);
103
+ ];
90
104
  });
91
105
  return () => {
92
106
  untrack(() => {
@@ -103,7 +117,8 @@
103
117
  const entry = entries.find((e) => e.el === tabStopEl);
104
118
  if (entry && !entry.disabled) return tabStopEl;
105
119
  }
106
- return entries.find((e) => !e.disabled)?.el ?? null;
120
+ // "First enabled row" must be in DOM order, so read the ordered view.
121
+ return orderedEntries.find((e) => !e.disabled)?.el ?? null;
107
122
  });
108
123
 
109
124
  // Si la row qui détient le focus DOM devient disabled (in-place, sans unmount),
@@ -161,38 +176,42 @@
161
176
  }
162
177
 
163
178
  function navigate(el: HTMLElement, key: string) {
164
- if (entries.length === 0) return;
165
- const idx = entries.findIndex((e) => e.el === el);
179
+ // Keyboard navigation walks rows in VISUAL (DOM) order, so read the lazily
180
+ // sorted view here. This is the first point the deferred sort is forced — the
181
+ // O(n²) register-time sort storm is gone, the sort runs once on demand.
182
+ const ordered = orderedEntries;
183
+ if (ordered.length === 0) return;
184
+ const idx = ordered.findIndex((e) => e.el === el);
166
185
  if (idx === -1) return;
167
186
 
168
187
  let targetIdx: number | null = null;
169
188
 
170
189
  if (key === "ArrowDown" || key === "ArrowRight") {
171
190
  // 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; }
191
+ for (let i = idx + 1; i < ordered.length; i++) {
192
+ if (!ordered[i].disabled) { targetIdx = i; break; }
174
193
  }
175
194
  } else if (key === "ArrowUp" || key === "ArrowLeft") {
176
195
  // Walk backward from current position, find the previous non-disabled entry.
177
196
  for (let i = idx - 1; i >= 0; i--) {
178
- if (!entries[i].disabled) { targetIdx = i; break; }
197
+ if (!ordered[i].disabled) { targetIdx = i; break; }
179
198
  }
180
199
  } else if (key === "Home") {
181
200
  // First non-disabled entry.
182
- for (let i = 0; i < entries.length; i++) {
183
- if (!entries[i].disabled) { targetIdx = i; break; }
201
+ for (let i = 0; i < ordered.length; i++) {
202
+ if (!ordered[i].disabled) { targetIdx = i; break; }
184
203
  }
185
204
  } else if (key === "End") {
186
205
  // Last non-disabled entry.
187
- for (let i = entries.length - 1; i >= 0; i--) {
188
- if (!entries[i].disabled) { targetIdx = i; break; }
206
+ for (let i = ordered.length - 1; i >= 0; i--) {
207
+ if (!ordered[i].disabled) { targetIdx = i; break; }
189
208
  }
190
209
  }
191
210
 
192
211
  // If no target found (all remaining are disabled, or already at boundary), stay put.
193
212
  if (targetIdx === null) return;
194
213
 
195
- const target = entries[targetIdx]?.el;
214
+ const target = ordered[targetIdx]?.el;
196
215
  if (target) {
197
216
  tabStopEl = target;
198
217
  target.focus();
@@ -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;AA0MJ,QAAA,MAAM,cAAc,yDAAwC,CAAC;AAC7D,KAAK,cAAc,GAAG,UAAU,CAAC,OAAO,cAAc,CAAC,CAAC;AACxD,eAAe,cAAc,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;AA6NJ,QAAA,MAAM,cAAc,yDAAwC,CAAC;AAC7D,KAAK,cAAc,GAAG,UAAU,CAAC,OAAO,cAAc,CAAC,CAAC;AACxD,eAAe,cAAc,CAAC"}
@@ -60,6 +60,16 @@
60
60
  trailing?: Snippet;
61
61
  /** Main content. */
62
62
  children?: Snippet;
63
+ /**
64
+ * Optional secondary line (the "legend") rendered MUTED and smaller UNDER
65
+ * `children`. When present the content column stacks vertically (a
66
+ * `--hasCaption` modifier); when absent the row stays single-line and
67
+ * byte-identical. The caption joins the row's accessible name by default (the
68
+ * SR reads "label, caption"); wrap it `aria-hidden` if it is purely
69
+ * decorative. MUST NOT contain interactive controls — a row is a single tab
70
+ * stop.
71
+ */
72
+ caption?: Snippet;
63
73
  class?: string;
64
74
  };
65
75
  </script>
@@ -77,6 +87,7 @@
77
87
  leading,
78
88
  trailing,
79
89
  children,
90
+ caption,
80
91
  class: className
81
92
  }: SelectableRowProps = $props();
82
93
 
@@ -125,6 +136,7 @@
125
136
  isSelected ? "st-selectableRow--selected" : null,
126
137
  disabled ? "st-selectableRow--disabled" : null,
127
138
  accentBar ? "st-selectableRow--accentBar" : null,
139
+ caption ? "st-selectableRow--hasCaption" : null,
128
140
  className
129
141
  ]
130
142
  .filter(Boolean)
@@ -190,7 +202,17 @@
190
202
  {#if leading}
191
203
  <span class="st-selectableRow__leading">{@render leading()}</span>
192
204
  {/if}
193
- <span class="st-selectableRow__content">{@render children?.()}</span>
205
+ {#if caption}
206
+ <!-- Caption present: the content column stacks the primary label over a muted
207
+ second line. Both lines truncate independently (each min-width:0 + ellipsis)
208
+ so a long caption never pushes the row width. -->
209
+ <span class="st-selectableRow__content st-selectableRow__content--stacked">
210
+ <span class="st-selectableRow__label">{@render children?.()}</span>
211
+ <span class="st-selectableRow__caption">{@render caption()}</span>
212
+ </span>
213
+ {:else}
214
+ <span class="st-selectableRow__content">{@render children?.()}</span>
215
+ {/if}
194
216
  {#if trailing}
195
217
  <span class="st-selectableRow__trailing">{@render trailing()}</span>
196
218
  {/if}
@@ -300,6 +322,36 @@
300
322
  white-space: nowrap;
301
323
  }
302
324
 
325
+ /* Caption variant (additive). Rows WITHOUT a caption keep the single-line
326
+ `.st-selectableRow__content` above byte-identically (no `--stacked` rule
327
+ applies). `--stacked` overlays a vertical column: the primary `__label` keeps
328
+ its own single-line ellipsis, and the muted `__caption` truncates
329
+ independently so a long legend never pushes the row width. Every leaf falls
330
+ back to a base literal so a theme that emits no
331
+ `--st-component-selectableRow-caption*` renders the caption identically. */
332
+ .st-selectableRow__content--stacked {
333
+ display: flex;
334
+ flex-direction: column;
335
+ /* The column stack drops the inline ellipsis/nowrap (each line truncates on
336
+ its own child); keep min-width:0 so the column can shrink and ellipsize. */
337
+ overflow: visible;
338
+ white-space: normal;
339
+ gap: var(--st-component-selectableRow-captionGap, 0.125rem);
340
+ }
341
+
342
+ .st-selectableRow__label,
343
+ .st-selectableRow__caption {
344
+ min-width: 0;
345
+ overflow: hidden;
346
+ text-overflow: ellipsis;
347
+ white-space: nowrap;
348
+ }
349
+
350
+ .st-selectableRow__caption {
351
+ color: var(--st-component-selectableRow-captionColor, var(--st-semantic-text-muted));
352
+ font-size: var(--st-component-selectableRow-captionFontSize, 0.75rem);
353
+ }
354
+
303
355
  @media (prefers-reduced-motion: reduce) {
304
356
  .st-selectableRow { transition: none; }
305
357
  }
@@ -56,6 +56,16 @@ export type SelectableRowProps = {
56
56
  trailing?: Snippet;
57
57
  /** Main content. */
58
58
  children?: Snippet;
59
+ /**
60
+ * Optional secondary line (the "legend") rendered MUTED and smaller UNDER
61
+ * `children`. When present the content column stacks vertically (a
62
+ * `--hasCaption` modifier); when absent the row stays single-line and
63
+ * byte-identical. The caption joins the row's accessible name by default (the
64
+ * SR reads "label, caption"); wrap it `aria-hidden` if it is purely
65
+ * decorative. MUST NOT contain interactive controls — a row is a single tab
66
+ * stop.
67
+ */
68
+ caption?: Snippet;
59
69
  class?: string;
60
70
  };
61
71
  declare const SelectableRow: import("svelte").Component<SelectableRowProps, {}, "selected">;
@@ -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,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"}
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;;;;;;;;OAQG;IACH,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB,CAAC;AA2IJ,QAAA,MAAM,aAAa,gEAAwC,CAAC;AAC5D,KAAK,aAAa,GAAG,UAAU,CAAC,OAAO,aAAa,CAAC,CAAC;AACtD,eAAe,aAAa,CAAC"}