@sentropic/design-system-svelte 0.34.28 → 0.34.32

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.
@@ -12,11 +12,19 @@
12
12
  | "category6"
13
13
  | "category7"
14
14
  | "category8";
15
+
16
+ export type { CellDecoration, CellDecorationIntent } from "./cellDecoration.js";
15
17
  </script>
16
18
 
17
19
  <script lang="ts">
18
20
  import type { HTMLAttributes } from "svelte/elements";
19
21
  import Sparkline from "./Sparkline.svelte";
22
+ import {
23
+ type CellDecoration,
24
+ cellDecorationClass,
25
+ cellDecorationLabel,
26
+ } from "./cellDecoration.js";
27
+ import CellDecorationIcon from "./CellDecorationIcon.svelte";
20
28
 
21
29
  type KpiCardProps = Omit<HTMLAttributes<HTMLElement>, "class"> & {
22
30
  /** Valeur principale affichée en grand. */
@@ -42,6 +50,11 @@
42
50
  size?: KpiCardSize;
43
51
  /** Couleur catégorielle pour l'accent (bordure de gauche). */
44
52
  tone?: KpiCardTone;
53
+ /**
54
+ * Conditional formatting : décoration sémantique de la carte (intent → token
55
+ * feedback en fond teinté accessible + icône lucide optionnelle).
56
+ */
57
+ decoration?: CellDecoration;
45
58
  class?: string;
46
59
  };
47
60
 
@@ -58,6 +71,7 @@
58
71
  sparkline,
59
72
  size = "md",
60
73
  tone,
74
+ decoration,
61
75
  class: className,
62
76
  ...rest
63
77
  }: KpiCardProps = $props();
@@ -116,7 +130,13 @@
116
130
  );
117
131
 
118
132
  const ariaLabel = $derived(
119
- [label, formattedValue, unit, formattedDelta && `${formattedDelta} ${trendLabel ?? ""}`.trim()]
133
+ [
134
+ label,
135
+ formattedValue,
136
+ unit,
137
+ formattedDelta && `${formattedDelta} ${trendLabel ?? ""}`.trim(),
138
+ decoration && cellDecorationLabel[decoration.intent]
139
+ ]
120
140
  .filter(Boolean)
121
141
  .join(", ")
122
142
  );
@@ -127,6 +147,8 @@
127
147
  `st-kpiCard--${size}`,
128
148
  tone && `st-kpiCard--${tone}`,
129
149
  tone && "st-kpiCard--toned",
150
+ decoration && "st-cell",
151
+ decoration && cellDecorationClass(decoration.intent),
130
152
  className
131
153
  ]
132
154
  .filter(Boolean)
@@ -138,6 +160,9 @@
138
160
  <p class="st-kpiCard__label">{label}</p>
139
161
 
140
162
  <p class="st-kpiCard__value">
163
+ {#if decoration}
164
+ <CellDecorationIcon icon={decoration.icon} />
165
+ {/if}
141
166
  <span class="st-kpiCard__number">{formattedValue}</span>
142
167
  {#if unit}
143
168
  <span class="st-kpiCard__unit">{unit}</span>
@@ -235,6 +260,46 @@
235
260
  margin: 0;
236
261
  }
237
262
 
263
+ /* Conditional formatting (« classe Power-BI ») : fond teinté accessible
264
+ (color-mix 14% sur token feedback, comme Badge/Tag) + icône alignée sur la
265
+ valeur. Le fond n'est jamais la seule indication (icône + texte SR). */
266
+ .st-kpiCard.st-cell {
267
+ padding: var(--st-spacing-4, 1rem);
268
+ }
269
+
270
+ .st-kpiCard.st-cell .st-kpiCard__value {
271
+ align-items: center;
272
+ }
273
+
274
+ .st-kpiCard.st-cell .st-kpiCard__number {
275
+ color: inherit;
276
+ }
277
+
278
+ .st-cell--intent-positive {
279
+ background: color-mix(in srgb, var(--st-semantic-feedback-success) 14%, white);
280
+ color: var(--st-semantic-feedback-success);
281
+ }
282
+
283
+ .st-cell--intent-negative {
284
+ background: color-mix(in srgb, var(--st-semantic-feedback-error) 14%, white);
285
+ color: var(--st-semantic-feedback-error);
286
+ }
287
+
288
+ .st-cell--intent-warning {
289
+ background: color-mix(in srgb, var(--st-semantic-feedback-warning) 14%, white);
290
+ color: var(--st-semantic-feedback-warning);
291
+ }
292
+
293
+ .st-cell--intent-info {
294
+ background: color-mix(in srgb, var(--st-semantic-feedback-info) 14%, white);
295
+ color: var(--st-semantic-feedback-info);
296
+ }
297
+
298
+ .st-cell--intent-neutral {
299
+ background: var(--st-semantic-surface-subtle);
300
+ color: var(--st-semantic-text-secondary);
301
+ }
302
+
238
303
  .st-kpiCard__number {
239
304
  font-size: 1.5rem;
240
305
  font-weight: 700;
@@ -3,7 +3,9 @@ export type KpiCardTrend = "up" | "down" | "flat";
3
3
  export type KpiCardFormat = "number" | "currency" | "percent";
4
4
  export type KpiCardDeltaFormat = "percent" | "absolute";
5
5
  export type KpiCardTone = "category1" | "category2" | "category3" | "category4" | "category5" | "category6" | "category7" | "category8";
6
+ export type { CellDecoration, CellDecorationIntent } from "./cellDecoration.js";
6
7
  import type { HTMLAttributes } from "svelte/elements";
8
+ import { type CellDecoration } from "./cellDecoration.js";
7
9
  type KpiCardProps = Omit<HTMLAttributes<HTMLElement>, "class"> & {
8
10
  /** Valeur principale affichée en grand. */
9
11
  value: number | string;
@@ -28,6 +30,11 @@ type KpiCardProps = Omit<HTMLAttributes<HTMLElement>, "class"> & {
28
30
  size?: KpiCardSize;
29
31
  /** Couleur catégorielle pour l'accent (bordure de gauche). */
30
32
  tone?: KpiCardTone;
33
+ /**
34
+ * Conditional formatting : décoration sémantique de la carte (intent → token
35
+ * feedback en fond teinté accessible + icône lucide optionnelle).
36
+ */
37
+ decoration?: CellDecoration;
31
38
  class?: string;
32
39
  };
33
40
  declare const KpiCard: import("svelte").Component<KpiCardProps, {}, "">;
@@ -1 +1 @@
1
- {"version":3,"file":"KpiCard.svelte.d.ts","sourceRoot":"","sources":["../src/lib/KpiCard.svelte.ts"],"names":[],"mappings":"AAGE,MAAM,MAAM,WAAW,GAAG,IAAI,GAAG,IAAI,GAAG,IAAI,CAAC;AAC7C,MAAM,MAAM,YAAY,GAAG,IAAI,GAAG,MAAM,GAAG,MAAM,CAAC;AAClD,MAAM,MAAM,aAAa,GAAG,QAAQ,GAAG,UAAU,GAAG,SAAS,CAAC;AAC9D,MAAM,MAAM,kBAAkB,GAAG,SAAS,GAAG,UAAU,CAAC;AACxD,MAAM,MAAM,WAAW,GACnB,WAAW,GACX,WAAW,GACX,WAAW,GACX,WAAW,GACX,WAAW,GACX,WAAW,GACX,WAAW,GACX,WAAW,CAAC;AAGlB,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAC;AAIpD,KAAK,YAAY,GAAG,IAAI,CAAC,cAAc,CAAC,WAAW,CAAC,EAAE,OAAO,CAAC,GAAG;IAC/D,2CAA2C;IAC3C,KAAK,EAAE,MAAM,GAAG,MAAM,CAAC;IACvB,0DAA0D;IAC1D,KAAK,EAAE,MAAM,CAAC;IACd,qDAAqD;IACrD,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,uEAAuE;IACvE,WAAW,CAAC,EAAE,kBAAkB,CAAC;IACjC,uDAAuD;IACvD,KAAK,CAAC,EAAE,YAAY,CAAC;IACrB,+DAA+D;IAC/D,MAAM,CAAC,EAAE,aAAa,CAAC;IACvB,2DAA2D;IAC3D,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,gEAAgE;IAChE,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,0FAA0F;IAC1F,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,4CAA4C;IAC5C,SAAS,CAAC,EAAE,MAAM,EAAE,CAAC;IACrB,IAAI,CAAC,EAAE,WAAW,CAAC;IACnB,8DAA8D;IAC9D,IAAI,CAAC,EAAE,WAAW,CAAC;IACnB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB,CAAC;AAiIJ,QAAA,MAAM,OAAO,kDAAwC,CAAC;AACtD,KAAK,OAAO,GAAG,UAAU,CAAC,OAAO,OAAO,CAAC,CAAC;AAC1C,eAAe,OAAO,CAAC"}
1
+ {"version":3,"file":"KpiCard.svelte.d.ts","sourceRoot":"","sources":["../src/lib/KpiCard.svelte.ts"],"names":[],"mappings":"AAGE,MAAM,MAAM,WAAW,GAAG,IAAI,GAAG,IAAI,GAAG,IAAI,CAAC;AAC7C,MAAM,MAAM,YAAY,GAAG,IAAI,GAAG,MAAM,GAAG,MAAM,CAAC;AAClD,MAAM,MAAM,aAAa,GAAG,QAAQ,GAAG,UAAU,GAAG,SAAS,CAAC;AAC9D,MAAM,MAAM,kBAAkB,GAAG,SAAS,GAAG,UAAU,CAAC;AACxD,MAAM,MAAM,WAAW,GACnB,WAAW,GACX,WAAW,GACX,WAAW,GACX,WAAW,GACX,WAAW,GACX,WAAW,GACX,WAAW,GACX,WAAW,CAAC;AAEhB,YAAY,EAAE,cAAc,EAAE,oBAAoB,EAAE,MAAM,qBAAqB,CAAC;AAGlF,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAC;AAEtD,OAAO,EACH,KAAK,cAAc,EAGpB,MAAM,qBAAqB,CAAC;AAI7B,KAAK,YAAY,GAAG,IAAI,CAAC,cAAc,CAAC,WAAW,CAAC,EAAE,OAAO,CAAC,GAAG;IAC/D,2CAA2C;IAC3C,KAAK,EAAE,MAAM,GAAG,MAAM,CAAC;IACvB,0DAA0D;IAC1D,KAAK,EAAE,MAAM,CAAC;IACd,qDAAqD;IACrD,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,uEAAuE;IACvE,WAAW,CAAC,EAAE,kBAAkB,CAAC;IACjC,uDAAuD;IACvD,KAAK,CAAC,EAAE,YAAY,CAAC;IACrB,+DAA+D;IAC/D,MAAM,CAAC,EAAE,aAAa,CAAC;IACvB,2DAA2D;IAC3D,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,gEAAgE;IAChE,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,0FAA0F;IAC1F,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,4CAA4C;IAC5C,SAAS,CAAC,EAAE,MAAM,EAAE,CAAC;IACrB,IAAI,CAAC,EAAE,WAAW,CAAC;IACnB,8DAA8D;IAC9D,IAAI,CAAC,EAAE,WAAW,CAAC;IACnB;;;OAGG;IACH,UAAU,CAAC,EAAE,cAAc,CAAC;IAC5B,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB,CAAC;AA+IJ,QAAA,MAAM,OAAO,kDAAwC,CAAC;AACtD,KAAK,OAAO,GAAG,UAAU,CAAC,OAAO,OAAO,CAAC,CAAC;AAC1C,eAAe,OAAO,CAAC"}
@@ -10,9 +10,15 @@
10
10
  * label string
11
11
  *
12
12
  * Props optionnelles :
13
- * width number (défaut 480)
14
- * height number (défaut 240)
15
- * class string
13
+ * width number (défaut 480)
14
+ * height number (défaut 240)
15
+ * annotations ChartAnnotation[] - overlay support/résistance/zones/events
16
+ * dataLabels boolean | { format?, position? } - étiquette close par bâton
17
+ * hoverKey string | null - crosshair contrôlé (clé = date/catégorie)
18
+ * onHoverKeyChange (key) => void - émet la date survolée (ou null)
19
+ * keyboardNav boolean - navigation clavier (roving tabindex)
20
+ * onSelectKey (key) => void - sélection clavier (Enter/Space) ; null = Escape
21
+ * class string
16
22
  */
17
23
  export type OHLCChartDatum = {
18
24
  label: string;
@@ -25,12 +31,27 @@
25
31
 
26
32
  <script lang="ts">
27
33
  import ChartDataList from "./ChartDataList.svelte";
34
+ import {
35
+ resolveAnnotations,
36
+ annotationDataListItems,
37
+ polygonPoints,
38
+ type ChartAnnotation
39
+ } from "./chartAnnotations.js";
40
+ import { formatDataLabel, normalizeDataLabels, type DataLabelsProp } from "./chartDataLabels.js";
41
+ import { resolveActiveIndex } from "./chartCrosshair.js";
42
+ import { datapointAriaLabel, datapointNavAction, rovingTabIndex } from "./chartKeyboardNav.js";
28
43
 
29
44
  type OHLCChartProps = {
30
45
  data: OHLCChartDatum[];
31
46
  label: string;
32
47
  width?: number;
33
48
  height?: number;
49
+ annotations?: ChartAnnotation[];
50
+ dataLabels?: DataLabelsProp;
51
+ hoverKey?: string | null;
52
+ onHoverKeyChange?: (key: string | null) => void;
53
+ keyboardNav?: boolean;
54
+ onSelectKey?: (key: string | null) => void;
34
55
  class?: string;
35
56
  };
36
57
 
@@ -39,6 +60,12 @@
39
60
  label,
40
61
  width = 480,
41
62
  height = 240,
63
+ annotations,
64
+ dataLabels,
65
+ hoverKey,
66
+ onHoverKeyChange,
67
+ keyboardNav,
68
+ onSelectKey,
42
69
  class: className
43
70
  }: OHLCChartProps = $props();
44
71
 
@@ -79,6 +106,9 @@
79
106
  }
80
107
 
81
108
  let hoveredIndex: number | null = $state(null);
109
+ // FR-5 — roving keyboard focus over the data points (separate from hover).
110
+ let focusedIndex: number = $state(-1);
111
+ let datapointRefs: Array<SVGRectElement | null> = [];
82
112
 
83
113
  const plotWidth = $derived(Math.max(width - MARGIN.left - MARGIN.right, 1));
84
114
  const plotHeight = $derived(Math.max(height - MARGIN.top - MARGIN.bottom, 1));
@@ -134,6 +164,7 @@
134
164
  index: i,
135
165
  bullish,
136
166
  centerX,
167
+ band,
137
168
  barHighY: highY,
138
169
  barLowY: lowY,
139
170
  openY,
@@ -145,15 +176,103 @@
145
176
  });
146
177
  });
147
178
 
148
- const dataValueItems = $derived(
149
- validData.map((d) => `${d.label}: O ${d.open} H ${d.high} L ${d.low} C ${d.close}`)
179
+ // --- Annotation overlay ---------------------------------------------------
180
+ // The x coordinate is CATEGORICAL (a bar `label` centre of band); the y
181
+ // coordinate is a price-axis number. Regions render behind the bars, every
182
+ // other kind above. The resolver maps each x via `xScale` (category → pixel)
183
+ // and each y via `yScale` (price → pixel), relative to the plot origin.
184
+ const priceY = $derived((v: number): number | null => {
185
+ if (!Number.isFinite(v)) return null;
186
+ return scaleLinear(v, domainMin, domainMax, plotHeight, 0);
187
+ });
188
+ const categoryPixel = $derived((v: number | string): number | null => {
189
+ const bar = bars.find((b) => b.datum.label === String(v));
190
+ if (!bar) return null;
191
+ return bar.centerX - MARGIN.left;
192
+ });
193
+ const resolvedAnnotations = $derived(
194
+ resolveAnnotations(annotations, {
195
+ xScale: categoryPixel,
196
+ yScale: priceY,
197
+ plotLeft: MARGIN.left,
198
+ plotTop: MARGIN.top,
199
+ plotWidth,
200
+ plotHeight
201
+ })
150
202
  );
203
+ const annotationRegions = $derived(resolvedAnnotations.filter((a) => a.kind === "region"));
204
+ const annotationAbove = $derived(resolvedAnnotations.filter((a) => a.kind !== "region"));
205
+
206
+ // --- Data labels ----------------------------------------------------------
207
+ // One `close` value label per bar, placed just above it. aria-hidden.
208
+ const dataLabelOpts = $derived(normalizeDataLabels(dataLabels));
209
+ const dataLabelItems = $derived.by(() => {
210
+ if (!dataLabelOpts.enabled) return [] as { key: string; x: number; y: number; text: string }[];
211
+ return bars.map((bar) => ({
212
+ key: bar.datum.label,
213
+ x: bar.centerX,
214
+ y: bar.barHighY - 6,
215
+ text: formatDataLabel(bar.datum.close, dataLabelOpts, formatTick)
216
+ }));
217
+ });
151
218
 
219
+ const dataValueItems = $derived([
220
+ ...validData.map((d) => `${d.label}: O ${d.open} H ${d.high} L ${d.low} C ${d.close}`),
221
+ ...annotationDataListItems(annotations)
222
+ ]);
223
+
224
+ // Stable key per bar (FR-3): its `label`. Resolves a controlled `hoverKey` to
225
+ // an index and feeds `onHoverKeyChange` from pointer events.
226
+ const hoverKeys = $derived(bars.map((b) => b.datum.label));
227
+ function emitHoverKey(index: number | null) {
228
+ onHoverKeyChange?.(index == null ? null : hoverKeys[index] ?? null);
229
+ }
230
+ function handleLeave() {
231
+ hoveredIndex = null;
232
+ emitHoverKey(null);
233
+ }
152
234
  function handlePointerMove(event: PointerEvent) {
153
235
  const target = event.target;
154
- if (!(target instanceof Element)) { hoveredIndex = null; return; }
155
- const idx = Number(target.getAttribute("data-chart-index"));
156
- hoveredIndex = Number.isInteger(idx) ? idx : null;
236
+ if (!(target instanceof Element)) {
237
+ hoveredIndex = null;
238
+ emitHoverKey(null);
239
+ return;
240
+ }
241
+ const raw = Number(target.getAttribute("data-chart-index"));
242
+ const index = Number.isInteger(raw) ? raw : null;
243
+ hoveredIndex = index;
244
+ emitHoverKey(index);
245
+ }
246
+
247
+ // Index whose crosshair/tooltip is DISPLAYED: the controlled `hoverKey` when
248
+ // provided (resolved against `hoverKeys`), else the internal pointer index.
249
+ const activeIndex = $derived(resolveActiveIndex(hoverKey, hoveredIndex, hoverKeys));
250
+
251
+ // --- Keyboard navigation (FR-5) ------------------------------------------
252
+ // Active when wired explicitly (`keyboardNav`) or implicitly (`onSelectKey`).
253
+ const navEnabled = $derived((keyboardNav === true || onSelectKey !== undefined) && bars.length > 0);
254
+ function focusDatum(index: number) {
255
+ focusedIndex = index;
256
+ datapointRefs[index]?.focus();
257
+ emitHoverKey(index);
258
+ }
259
+ function handleDatapointKeyDown(event: KeyboardEvent, index: number) {
260
+ const action = datapointNavAction(event.key, index, bars.length);
261
+ if (!action) return;
262
+ event.preventDefault();
263
+ if (action.kind === "move") {
264
+ focusDatum(action.index);
265
+ } else if (action.kind === "select") {
266
+ onSelectKey?.(bars[index].datum.label);
267
+ } else {
268
+ focusedIndex = -1;
269
+ emitHoverKey(null);
270
+ onSelectKey?.(null);
271
+ (event.currentTarget as SVGElement).blur();
272
+ }
273
+ }
274
+ function ohlcAriaLabel(d: OHLCChartDatum): string {
275
+ return datapointAriaLabel(d.label, `O ${d.open} H ${d.high} L ${d.low} C ${d.close}`);
157
276
  }
158
277
 
159
278
  const classes = () => ["st-ohlcChart", className].filter(Boolean).join(" ");
@@ -165,7 +284,7 @@
165
284
  role="img"
166
285
  aria-label={label}
167
286
  onpointermove={handlePointerMove}
168
- onpointerleave={() => (hoveredIndex = null)}
287
+ onpointerleave={handleLeave}
169
288
  >
170
289
  <svg
171
290
  viewBox="0 0 {width} {height}"
@@ -188,6 +307,20 @@
188
307
  <line class="st-ohlcChart__axis" x1={MARGIN.left} x2={MARGIN.left} y1={MARGIN.top} y2={height - MARGIN.bottom} />
189
308
  <line class="st-ohlcChart__axis" x1={MARGIN.left} x2={width - MARGIN.right} y1={height - MARGIN.bottom} y2={height - MARGIN.bottom} />
190
309
 
310
+ <!-- Annotation regions sit BEHIND the bars (filled bands). -->
311
+ {#if annotationRegions.length > 0}
312
+ <g class="st-ohlcChart__annotations st-ohlcChart__annotations--behind">
313
+ {#each annotationRegions as a (a.key)}
314
+ {#if a.kind === "region"}
315
+ <rect class="st-ohlcChart__annotationRegion" x={a.x} y={a.y} width={a.width} height={a.height} />
316
+ {#if a.label}
317
+ <text class="st-ohlcChart__annotationLabel" x={a.x + 4} y={a.y + 11}>{a.label}</text>
318
+ {/if}
319
+ {/if}
320
+ {/each}
321
+ </g>
322
+ {/if}
323
+
191
324
  <!-- clé composite pour éviter les doublons -->
192
325
  {#each bars as b, i (`${i}-${b.datum.label}`)}
193
326
  <g
@@ -232,13 +365,95 @@
232
365
  {b.datum.label}
233
366
  </text>
234
367
  {/each}
368
+
369
+ <!-- Annotations ABOVE the bars: lines, shapes, points, labels. -->
370
+ {#if annotationAbove.length > 0}
371
+ <g class="st-ohlcChart__annotations st-ohlcChart__annotations--above">
372
+ {#each annotationAbove as a (a.key)}
373
+ {#if a.kind === "line"}
374
+ <line class="st-ohlcChart__annotationLine" x1={a.x1} y1={a.y1} x2={a.x2} y2={a.y2} />
375
+ {#if a.label}
376
+ <text
377
+ class="st-ohlcChart__annotationLabel"
378
+ x={a.axis === "x" ? a.x1 + 4 : MARGIN.left + plotWidth - 4}
379
+ y={a.axis === "x" ? MARGIN.top + 11 : a.y1 - 4}
380
+ text-anchor={a.axis === "x" ? "start" : "end"}
381
+ >{a.label}</text>
382
+ {/if}
383
+ {:else if a.kind === "shape"}
384
+ <polygon class="st-ohlcChart__annotationShape" points={polygonPoints(a.points)} />
385
+ {#if a.label}
386
+ <text class="st-ohlcChart__annotationLabel" x={a.labelX} y={a.labelY} text-anchor="middle">{a.label}</text>
387
+ {/if}
388
+ {:else if a.kind === "point"}
389
+ <circle class="st-ohlcChart__annotationPoint" cx={a.x} cy={a.y} r="4.5" />
390
+ {#if a.label}
391
+ <text class="st-ohlcChart__annotationLabel" x={a.x} y={a.y - 8} text-anchor="middle">{a.label}</text>
392
+ {/if}
393
+ {:else}
394
+ <text class="st-ohlcChart__annotationText" x={a.x} y={a.y} text-anchor={a.anchor}>{a.text}</text>
395
+ {/if}
396
+ {/each}
397
+ </g>
398
+ {/if}
399
+
400
+ <!-- Data labels — one close value per bar, drawn on top. aria-hidden. -->
401
+ {#if dataLabelItems.length > 0}
402
+ <g class="st-ohlcChart__dataLabels" aria-hidden="true">
403
+ {#each dataLabelItems as d (d.key)}
404
+ <text class="st-ohlcChart__dataLabel" x={d.x} y={d.y} text-anchor="middle" dominant-baseline="auto">{d.text}</text>
405
+ {/each}
406
+ </g>
407
+ {/if}
408
+
409
+ <!-- Crosshair (FR-3) — a tokenised dashed vertical line at the active bar.
410
+ Decorative (aria-hidden); the value is in the tooltip + ChartDataList. -->
411
+ {#if activeIndex >= 0 && bars[activeIndex]}
412
+ {@const cb = bars[activeIndex]}
413
+ <g class="st-ohlcChart__crosshair" aria-hidden="true">
414
+ <line class="st-ohlcChart__crosshairLine" x1={cb.centerX} x2={cb.centerX} y1={MARGIN.top} y2={MARGIN.top + plotHeight} />
415
+ </g>
416
+ {/if}
235
417
  </svg>
418
+
419
+ <!-- Keyboard navigation overlay (FR-5) — a focusable, transparent hit layer
420
+ over the bars. NOT aria-hidden: it is the accessible roving cursor. -->
421
+ {#if navEnabled}
422
+ <svg
423
+ class="st-ohlcChart__navLayer"
424
+ viewBox="0 0 {width} {height}"
425
+ preserveAspectRatio="xMidYMid meet"
426
+ width="100%"
427
+ height="100%"
428
+ role="group"
429
+ aria-label={`${label} — points de données`}
430
+ >
431
+ {#each bars as bar, i (`${i}-${bar.datum.label}`)}
432
+ <rect
433
+ bind:this={datapointRefs[i]}
434
+ class="st-ohlcChart__navDatum"
435
+ x={bar.centerX - bar.band / 2}
436
+ y={MARGIN.top}
437
+ width={bar.band}
438
+ height={plotHeight}
439
+ role="img"
440
+ tabindex={rovingTabIndex(i, focusedIndex, bars.length)}
441
+ aria-label={ohlcAriaLabel(bar.datum)}
442
+ onkeydown={(event) => handleDatapointKeyDown(event, i)}
443
+ onfocus={() => {
444
+ focusedIndex = i;
445
+ emitHoverKey(i);
446
+ }}
447
+ />
448
+ {/each}
449
+ </svg>
450
+ {/if}
236
451
  </div>
237
452
 
238
453
  <ChartDataList {label} items={dataValueItems} />
239
454
 
240
- {#if hoveredIndex !== null && bars[hoveredIndex]}
241
- {@const b = bars[hoveredIndex]}
455
+ {#if activeIndex >= 0 && bars[activeIndex]}
456
+ {@const b = bars[activeIndex]}
242
457
  <div
243
458
  class="st-ohlcChart__tooltip"
244
459
  role="presentation"
@@ -266,6 +481,7 @@
266
481
 
267
482
  .st-ohlcChart__visual {
268
483
  display: block;
484
+ position: relative;
269
485
  }
270
486
 
271
487
  .st-ohlcChart__axis {
@@ -304,6 +520,65 @@
304
520
  stroke: var(--st-semantic-feedback-error);
305
521
  }
306
522
 
523
+ /* --- Annotation layer ----------------------------------------------------
524
+ Regions render BEHIND the bars; lines/shapes/points/labels render ABOVE. */
525
+ .st-ohlcChart__annotationRegion {
526
+ fill: color-mix(in srgb, var(--st-semantic-feedback-info) 12%, transparent);
527
+ stroke: none;
528
+ }
529
+ .st-ohlcChart__annotationLine {
530
+ stroke: var(--st-semantic-feedback-info);
531
+ stroke-width: 1.5;
532
+ stroke-dasharray: 4 3;
533
+ }
534
+ .st-ohlcChart__annotationShape {
535
+ fill: color-mix(in srgb, var(--st-semantic-feedback-info) 14%, transparent);
536
+ stroke: var(--st-semantic-feedback-info);
537
+ stroke-width: 1.5;
538
+ }
539
+ .st-ohlcChart__annotationPoint {
540
+ fill: var(--st-semantic-feedback-info);
541
+ stroke: var(--st-semantic-surface-default);
542
+ stroke-width: 1.5;
543
+ }
544
+ .st-ohlcChart__annotationLabel,
545
+ .st-ohlcChart__annotationText {
546
+ fill: var(--st-semantic-text-primary);
547
+ font-size: 0.625rem;
548
+ font-weight: 600;
549
+ }
550
+
551
+ /* Data labels — per-bar close value, drawn on top. Token-only colour. */
552
+ .st-ohlcChart__dataLabel {
553
+ fill: var(--st-semantic-text-primary);
554
+ font-size: 0.6875rem;
555
+ font-weight: 600;
556
+ }
557
+
558
+ /* Crosshair (FR-3) — a tokenised dashed vertical line at the active bar. */
559
+ .st-ohlcChart__crosshairLine {
560
+ stroke: var(--st-semantic-border-strong);
561
+ stroke-width: 1;
562
+ stroke-dasharray: 3 3;
563
+ opacity: 0.7;
564
+ }
565
+
566
+ /* Keyboard navigation layer (FR-5) — a focusable, transparent overlay of one
567
+ hit-rect per bar. Carries the roving tab stop; the focus ring is tokenised. */
568
+ .st-ohlcChart__navLayer {
569
+ inset: 0;
570
+ position: absolute;
571
+ }
572
+ .st-ohlcChart__navDatum {
573
+ fill: transparent;
574
+ outline: none;
575
+ }
576
+ .st-ohlcChart__navDatum:focus-visible {
577
+ fill: color-mix(in srgb, var(--st-semantic-border-interactive) 12%, transparent);
578
+ outline: 2px solid var(--st-semantic-border-interactive);
579
+ outline-offset: 1px;
580
+ }
581
+
307
582
  @media (prefers-reduced-motion: reduce) {
308
583
  .st-ohlcChart__bar {
309
584
  transition: none;
@@ -9,9 +9,15 @@
9
9
  * label string
10
10
  *
11
11
  * Props optionnelles :
12
- * width number (défaut 480)
13
- * height number (défaut 240)
14
- * class string
12
+ * width number (défaut 480)
13
+ * height number (défaut 240)
14
+ * annotations ChartAnnotation[] - overlay support/résistance/zones/events
15
+ * dataLabels boolean | { format?, position? } - étiquette close par bâton
16
+ * hoverKey string | null - crosshair contrôlé (clé = date/catégorie)
17
+ * onHoverKeyChange (key) => void - émet la date survolée (ou null)
18
+ * keyboardNav boolean - navigation clavier (roving tabindex)
19
+ * onSelectKey (key) => void - sélection clavier (Enter/Space) ; null = Escape
20
+ * class string
15
21
  */
16
22
  export type OHLCChartDatum = {
17
23
  label: string;
@@ -20,11 +26,19 @@ export type OHLCChartDatum = {
20
26
  low: number;
21
27
  close: number;
22
28
  };
29
+ import { type ChartAnnotation } from "./chartAnnotations.js";
30
+ import { type DataLabelsProp } from "./chartDataLabels.js";
23
31
  type OHLCChartProps = {
24
32
  data: OHLCChartDatum[];
25
33
  label: string;
26
34
  width?: number;
27
35
  height?: number;
36
+ annotations?: ChartAnnotation[];
37
+ dataLabels?: DataLabelsProp;
38
+ hoverKey?: string | null;
39
+ onHoverKeyChange?: (key: string | null) => void;
40
+ keyboardNav?: boolean;
41
+ onSelectKey?: (key: string | null) => void;
28
42
  class?: string;
29
43
  };
30
44
  declare const OHLCChart: import("svelte").Component<OHLCChartProps, {}, "">;
@@ -1 +1 @@
1
- {"version":3,"file":"OHLCChart.svelte.d.ts","sourceRoot":"","sources":["../src/lib/OHLCChart.svelte.ts"],"names":[],"mappings":"AAGE;;;;;;;;;;;;;;GAcG;AACH,MAAM,MAAM,cAAc,GAAG;IAC3B,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,EAAE,MAAM,CAAC;CACf,CAAC;AAMF,KAAK,cAAc,GAAG;IACpB,IAAI,EAAE,cAAc,EAAE,CAAC;IACvB,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB,CAAC;AAqLJ,QAAA,MAAM,SAAS,oDAAwC,CAAC;AACxD,KAAK,SAAS,GAAG,UAAU,CAAC,OAAO,SAAS,CAAC,CAAC;AAC9C,eAAe,SAAS,CAAC"}
1
+ {"version":3,"file":"OHLCChart.svelte.d.ts","sourceRoot":"","sources":["../src/lib/OHLCChart.svelte.ts"],"names":[],"mappings":"AAGE;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,MAAM,MAAM,cAAc,GAAG;IAC3B,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,EAAE,MAAM,CAAC;CACf,CAAC;AAIJ,OAAO,EAIH,KAAK,eAAe,EACrB,MAAM,uBAAuB,CAAC;AACjC,OAAO,EAAwC,KAAK,cAAc,EAAE,MAAM,sBAAsB,CAAC;AAK/F,KAAK,cAAc,GAAG;IACpB,IAAI,EAAE,cAAc,EAAE,CAAC;IACvB,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,WAAW,CAAC,EAAE,eAAe,EAAE,CAAC;IAChC,UAAU,CAAC,EAAE,cAAc,CAAC;IAC5B,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,gBAAgB,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,KAAK,IAAI,CAAC;IAChD,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,WAAW,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,KAAK,IAAI,CAAC;IAC3C,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB,CAAC;AAgWJ,QAAA,MAAM,SAAS,oDAAwC,CAAC;AACxD,KAAK,SAAS,GAAG,UAAU,CAAC,OAAO,SAAS,CAAC,CAAC;AAC9C,eAAe,SAAS,CAAC"}