@sentropic/design-system-svelte 0.34.27 → 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.
@@ -75,6 +75,10 @@
75
75
  /** Zone identité à droite (IdentityMenu, bouton connexion, …). */
76
76
  identity?: Snippet;
77
77
 
78
+ // ── Contrôles additionnels ─────────────────────────────────────────────────
79
+ /** Contrôles additionnels dans la zone utilitaire. */
80
+ extraSelectors?: Snippet;
81
+
78
82
  // ── Mobile ─────────────────────────────────────────────────────────────────
79
83
  /** État ouvert du tiroir mobile (contrôlé). */
80
84
  mobileMenuOpen?: boolean;
@@ -95,10 +99,10 @@
95
99
  <script lang="ts">
96
100
  import { untrack } from "svelte";
97
101
  import {
98
- Boxes,
99
102
  ChevronDown,
100
103
  Github,
101
104
  Globe,
105
+ Menu,
102
106
  Moon,
103
107
  Palette,
104
108
  Sun,
@@ -127,6 +131,7 @@
127
131
  githubHref,
128
132
  githubLabel = "GitHub",
129
133
  identity,
134
+ extraSelectors,
130
135
  mobileMenuOpen = false,
131
136
  onMobileMenuToggle,
132
137
  menuLabel = "Menu",
@@ -320,6 +325,7 @@
320
325
  {#if showLocaleSelector}{@render localeSelector()}{/if}
321
326
  {#if showGithub}{@render githubLink()}{/if}
322
327
  {#if identity}<div class="st-appChrome__identity">{@render identity()}</div>{/if}
328
+ {#if extraSelectors}<div class="st-appChrome__extraSelectors">{@render extraSelectors()}</div>{/if}
323
329
  </div>
324
330
  {/snippet}
325
331
 
@@ -334,7 +340,7 @@
334
340
  aria-controls={drawerId}
335
341
  aria-label={menuLabel}
336
342
  >
337
- <Boxes size={20} aria-hidden="true" />
343
+ <Menu size={20} aria-hidden="true" />
338
344
  </button>
339
345
  {/snippet}
340
346
 
@@ -434,11 +440,18 @@
434
440
  react/vue : la source de vérité du CSS est le bloc publié (styles.css).
435
441
  Ce <style> scoped ne fait que rendre la démo Svelte autonome. */
436
442
  .st-appChrome {
443
+ position: sticky;
444
+ top: 0;
437
445
  width: 100%;
446
+ z-index: 30;
438
447
  }
439
448
 
440
449
  :global(.st-appChrome__header .st-appHeader__bar) {
450
+ background: color-mix(in srgb, var(--st-semantic-surface-default) 96%, transparent);
451
+ backdrop-filter: blur(8px);
452
+ height: 5rem;
441
453
  max-width: none;
454
+ padding: 0 var(--st-spacing-6, 1.5rem);
442
455
  }
443
456
 
444
457
  .st-appChrome__brand {
@@ -482,7 +495,7 @@
482
495
  .st-appChrome__utilityNav {
483
496
  align-items: center;
484
497
  display: flex;
485
- gap: var(--st-spacing-2, 0.5rem);
498
+ gap: var(--st-spacing-1, 0.25rem);
486
499
  }
487
500
 
488
501
  .st-appChrome__menuWrap {
@@ -579,6 +592,12 @@
579
592
  color: var(--st-semantic-text-primary);
580
593
  }
581
594
 
595
+ .st-appChrome__extraSelectors {
596
+ align-items: center;
597
+ display: flex;
598
+ gap: var(--st-spacing-1, 0.25rem);
599
+ }
600
+
582
601
  .st-appChrome__drawer {
583
602
  background: var(--st-semantic-surface-default);
584
603
  border-bottom: 1px solid var(--st-semantic-border-subtle);
@@ -60,6 +60,8 @@ export interface AppChromeProps {
60
60
  githubLabel?: string;
61
61
  /** Zone identité à droite (IdentityMenu, bouton connexion, …). */
62
62
  identity?: Snippet;
63
+ /** Contrôles additionnels dans la zone utilitaire. */
64
+ extraSelectors?: Snippet;
63
65
  /** État ouvert du tiroir mobile (contrôlé). */
64
66
  mobileMenuOpen?: boolean;
65
67
  /** Callback de bascule du tiroir mobile. */
@@ -1 +1 @@
1
- {"version":3,"file":"AppChrome.svelte.d.ts","sourceRoot":"","sources":["../src/lib/AppChrome.svelte.ts"],"names":[],"mappings":"AAGE,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,QAAQ,CAAC;AAEtC,iDAAiD;AACjD,MAAM,WAAW,gBAAgB;IAC/B,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,CAAC;IACb,kDAAkD;IAClD,MAAM,CAAC,EAAE,OAAO,CAAC;CAClB;AAED,wCAAwC;AACxC,MAAM,WAAW,oBAAoB;IACnC,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;CACf;AAED,MAAM,MAAM,kBAAkB,GAAG,OAAO,GAAG,MAAM,GAAG,MAAM,CAAC;AAC3D,MAAM,MAAM,eAAe,GAAG,IAAI,GAAG,IAAI,CAAC;AAE1C,MAAM,WAAW,cAAc;IAE7B,4CAA4C;IAC5C,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,2EAA2E;IAC3E,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,2DAA2D;IAC3D,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,uDAAuD;IACvD,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,2CAA2C;IAC3C,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,8EAA8E;IAC9E,UAAU,CAAC,EAAE,MAAM,CAAC;IAGpB,+DAA+D;IAC/D,GAAG,CAAC,EAAE,gBAAgB,EAAE,CAAC;IACzB,uCAAuC;IACvC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAGlB,yDAAyD;IACzD,MAAM,CAAC,EAAE,oBAAoB,EAAE,CAAC;IAChC,yBAAyB;IACzB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,uCAAuC;IACvC,aAAa,CAAC,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,IAAI,CAAC;IACrC,wCAAwC;IACxC,UAAU,CAAC,EAAE,MAAM,CAAC;IAGpB,6DAA6D;IAC7D,SAAS,CAAC,EAAE,kBAAkB,CAAC;IAC/B,4DAA4D;IAC5D,iBAAiB,CAAC,EAAE,CAAC,IAAI,EAAE,kBAAkB,KAAK,IAAI,CAAC;IACvD,0DAA0D;IAC1D,eAAe,CAAC,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,CAAC;IAGhE,2DAA2D;IAC3D,MAAM,CAAC,EAAE,eAAe,CAAC;IACzB,wCAAwC;IACxC,cAAc,CAAC,EAAE,CAAC,MAAM,EAAE,eAAe,KAAK,IAAI,CAAC;IACnD,yCAAyC;IACzC,WAAW,CAAC,EAAE,MAAM,CAAC;IAGrB,qDAAqD;IACrD,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,iCAAiC;IACjC,WAAW,CAAC,EAAE,MAAM,CAAC;IAGrB,kEAAkE;IAClE,QAAQ,CAAC,EAAE,OAAO,CAAC;IAGnB,+CAA+C;IAC/C,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB,4CAA4C;IAC5C,kBAAkB,CAAC,EAAE,MAAM,IAAI,CAAC;IAChC,mCAAmC;IACnC,SAAS,CAAC,EAAE,MAAM,CAAC;IAEnB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAqRH,QAAA,MAAM,SAAS,oDAAwC,CAAC;AACxD,KAAK,SAAS,GAAG,UAAU,CAAC,OAAO,SAAS,CAAC,CAAC;AAC9C,eAAe,SAAS,CAAC"}
1
+ {"version":3,"file":"AppChrome.svelte.d.ts","sourceRoot":"","sources":["../src/lib/AppChrome.svelte.ts"],"names":[],"mappings":"AAGE,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,QAAQ,CAAC;AAEtC,iDAAiD;AACjD,MAAM,WAAW,gBAAgB;IAC/B,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,CAAC;IACb,kDAAkD;IAClD,MAAM,CAAC,EAAE,OAAO,CAAC;CAClB;AAED,wCAAwC;AACxC,MAAM,WAAW,oBAAoB;IACnC,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;CACf;AAED,MAAM,MAAM,kBAAkB,GAAG,OAAO,GAAG,MAAM,GAAG,MAAM,CAAC;AAC3D,MAAM,MAAM,eAAe,GAAG,IAAI,GAAG,IAAI,CAAC;AAE1C,MAAM,WAAW,cAAc;IAE7B,4CAA4C;IAC5C,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,2EAA2E;IAC3E,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,2DAA2D;IAC3D,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,uDAAuD;IACvD,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,2CAA2C;IAC3C,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,8EAA8E;IAC9E,UAAU,CAAC,EAAE,MAAM,CAAC;IAGpB,+DAA+D;IAC/D,GAAG,CAAC,EAAE,gBAAgB,EAAE,CAAC;IACzB,uCAAuC;IACvC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAGlB,yDAAyD;IACzD,MAAM,CAAC,EAAE,oBAAoB,EAAE,CAAC;IAChC,yBAAyB;IACzB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,uCAAuC;IACvC,aAAa,CAAC,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,IAAI,CAAC;IACrC,wCAAwC;IACxC,UAAU,CAAC,EAAE,MAAM,CAAC;IAGpB,6DAA6D;IAC7D,SAAS,CAAC,EAAE,kBAAkB,CAAC;IAC/B,4DAA4D;IAC5D,iBAAiB,CAAC,EAAE,CAAC,IAAI,EAAE,kBAAkB,KAAK,IAAI,CAAC;IACvD,0DAA0D;IAC1D,eAAe,CAAC,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,CAAC;IAGhE,2DAA2D;IAC3D,MAAM,CAAC,EAAE,eAAe,CAAC;IACzB,wCAAwC;IACxC,cAAc,CAAC,EAAE,CAAC,MAAM,EAAE,eAAe,KAAK,IAAI,CAAC;IACnD,yCAAyC;IACzC,WAAW,CAAC,EAAE,MAAM,CAAC;IAGrB,qDAAqD;IACrD,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,iCAAiC;IACjC,WAAW,CAAC,EAAE,MAAM,CAAC;IAGrB,kEAAkE;IAClE,QAAQ,CAAC,EAAE,OAAO,CAAC;IAGnB,sDAAsD;IACtD,cAAc,CAAC,EAAE,OAAO,CAAC;IAGzB,+CAA+C;IAC/C,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB,4CAA4C;IAC5C,kBAAkB,CAAC,EAAE,MAAM,IAAI,CAAC;IAChC,mCAAmC;IACnC,SAAS,CAAC,EAAE,MAAM,CAAC;IAEnB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAuRH,QAAA,MAAM,SAAS,oDAAwC,CAAC;AACxD,KAAK,SAAS,GAAG,UAAU,CAAC,OAAO,SAAS,CAAC,CAAC;AAC9C,eAAe,SAAS,CAAC"}
@@ -120,3 +120,13 @@ describe("AppChrome — mobile burger + tiroir", () => {
120
120
  expect(onMobileMenuToggle).toHaveBeenCalledTimes(1);
121
121
  });
122
122
  });
123
+ describe("AppChrome — extraSelectors", () => {
124
+ it("renders extraSelectors snippet content in the utility nav", () => {
125
+ const { container } = render(AppChrome, { props: { extraSelectors: snippet("extra-ctrl") } });
126
+ expect(container.querySelector(".st-appChrome__extraSelectors")?.textContent).toContain("extra-ctrl");
127
+ });
128
+ it("does not render extraSelectors div when not provided", () => {
129
+ const { container } = render(AppChrome);
130
+ expect(container.querySelector(".st-appChrome__extraSelectors")).toBeNull();
131
+ });
132
+ });
@@ -8,9 +8,15 @@
8
8
  * label string
9
9
  *
10
10
  * Props optionnelles :
11
- * width number (défaut 480)
12
- * height number (défaut 240)
13
- * class string
11
+ * width number (défaut 480)
12
+ * height number (défaut 240)
13
+ * annotations ChartAnnotation[] - overlay support/résistance/zones/events
14
+ * dataLabels boolean | { format?, position? } - étiquette close par bougie
15
+ * hoverKey string | null - crosshair contrôlé (clé = date/catégorie)
16
+ * onHoverKeyChange (key) => void - émet la date survolée (ou null)
17
+ * keyboardNav boolean - navigation clavier (roving tabindex)
18
+ * onSelectKey (key) => void - sélection clavier (Enter/Space) ; null = Escape
19
+ * class string
14
20
  */
15
21
  export type CandlestickChartDatum = {
16
22
  label: string;
@@ -23,12 +29,27 @@
23
29
 
24
30
  <script lang="ts">
25
31
  import ChartDataList from "./ChartDataList.svelte";
32
+ import {
33
+ resolveAnnotations,
34
+ annotationDataListItems,
35
+ polygonPoints,
36
+ type ChartAnnotation
37
+ } from "./chartAnnotations.js";
38
+ import { formatDataLabel, normalizeDataLabels, type DataLabelsProp } from "./chartDataLabels.js";
39
+ import { resolveActiveIndex } from "./chartCrosshair.js";
40
+ import { datapointAriaLabel, datapointNavAction, rovingTabIndex } from "./chartKeyboardNav.js";
26
41
 
27
42
  type CandlestickChartProps = {
28
43
  data: CandlestickChartDatum[];
29
44
  label: string;
30
45
  width?: number;
31
46
  height?: number;
47
+ annotations?: ChartAnnotation[];
48
+ dataLabels?: DataLabelsProp;
49
+ hoverKey?: string | null;
50
+ onHoverKeyChange?: (key: string | null) => void;
51
+ keyboardNav?: boolean;
52
+ onSelectKey?: (key: string | null) => void;
32
53
  class?: string;
33
54
  };
34
55
 
@@ -37,6 +58,12 @@
37
58
  label,
38
59
  width = 480,
39
60
  height = 240,
61
+ annotations,
62
+ dataLabels,
63
+ hoverKey,
64
+ onHoverKeyChange,
65
+ keyboardNav,
66
+ onSelectKey,
40
67
  class: className
41
68
  }: CandlestickChartProps = $props();
42
69
 
@@ -77,6 +104,9 @@
77
104
  }
78
105
 
79
106
  let hoveredIndex: number | null = $state(null);
107
+ // FR-5 — roving keyboard focus over the data points (separate from hover).
108
+ let focusedIndex: number = $state(-1);
109
+ let datapointRefs: Array<SVGRectElement | null> = [];
80
110
 
81
111
  const plotWidth = $derived(Math.max(width - MARGIN.left - MARGIN.right, 1));
82
112
  const plotHeight = $derived(Math.max(height - MARGIN.top - MARGIN.bottom, 1));
@@ -131,6 +161,7 @@
131
161
  index: i,
132
162
  bullish,
133
163
  centerX,
164
+ band,
134
165
  bodyX: centerX - bodyW / 2,
135
166
  bodyY: bodyTop,
136
167
  bodyW,
@@ -142,15 +173,103 @@
142
173
  });
143
174
  });
144
175
 
145
- const dataValueItems = $derived(
146
- validData.map((d) => `${d.label}: O ${d.open} H ${d.high} L ${d.low} C ${d.close}`)
176
+ // --- Annotation overlay ---------------------------------------------------
177
+ // The x coordinate is CATEGORICAL (a candle `label` centre of band); the y
178
+ // coordinate is a price-axis number. Regions render behind the candles, every
179
+ // other kind above. The resolver maps each x via `xScale` (category → pixel)
180
+ // and each y via `yScale` (price → pixel), relative to the plot origin.
181
+ const priceY = $derived((v: number): number | null => {
182
+ if (!Number.isFinite(v)) return null;
183
+ return scaleLinear(v, domainMin, domainMax, plotHeight, 0);
184
+ });
185
+ const categoryPixel = $derived((v: number | string): number | null => {
186
+ const candle = candles.find((c) => c.datum.label === String(v));
187
+ if (!candle) return null;
188
+ return candle.centerX - MARGIN.left;
189
+ });
190
+ const resolvedAnnotations = $derived(
191
+ resolveAnnotations(annotations, {
192
+ xScale: categoryPixel,
193
+ yScale: priceY,
194
+ plotLeft: MARGIN.left,
195
+ plotTop: MARGIN.top,
196
+ plotWidth,
197
+ plotHeight
198
+ })
147
199
  );
200
+ const annotationRegions = $derived(resolvedAnnotations.filter((a) => a.kind === "region"));
201
+ const annotationAbove = $derived(resolvedAnnotations.filter((a) => a.kind !== "region"));
202
+
203
+ // --- Data labels ----------------------------------------------------------
204
+ // One `close` value label per candle, placed just above it. aria-hidden.
205
+ const dataLabelOpts = $derived(normalizeDataLabels(dataLabels));
206
+ const dataLabelItems = $derived.by(() => {
207
+ if (!dataLabelOpts.enabled) return [] as { key: string; x: number; y: number; text: string }[];
208
+ return candles.map((candle) => ({
209
+ key: candle.datum.label,
210
+ x: candle.centerX,
211
+ y: candle.wickHighY - 6,
212
+ text: formatDataLabel(candle.datum.close, dataLabelOpts, formatTick)
213
+ }));
214
+ });
148
215
 
216
+ const dataValueItems = $derived([
217
+ ...validData.map((d) => `${d.label}: O ${d.open} H ${d.high} L ${d.low} C ${d.close}`),
218
+ ...annotationDataListItems(annotations)
219
+ ]);
220
+
221
+ // Stable key per candle (FR-3): its `label`. Resolves a controlled `hoverKey`
222
+ // to an index and feeds `onHoverKeyChange` from pointer events.
223
+ const hoverKeys = $derived(candles.map((c) => c.datum.label));
224
+ function emitHoverKey(index: number | null) {
225
+ onHoverKeyChange?.(index == null ? null : hoverKeys[index] ?? null);
226
+ }
227
+ function handleLeave() {
228
+ hoveredIndex = null;
229
+ emitHoverKey(null);
230
+ }
149
231
  function handlePointerMove(event: PointerEvent) {
150
232
  const target = event.target;
151
- if (!(target instanceof Element)) { hoveredIndex = null; return; }
152
- const idx = Number(target.getAttribute("data-chart-index"));
153
- hoveredIndex = Number.isInteger(idx) ? idx : null;
233
+ if (!(target instanceof Element)) {
234
+ hoveredIndex = null;
235
+ emitHoverKey(null);
236
+ return;
237
+ }
238
+ const raw = Number(target.getAttribute("data-chart-index"));
239
+ const index = Number.isInteger(raw) ? raw : null;
240
+ hoveredIndex = index;
241
+ emitHoverKey(index);
242
+ }
243
+
244
+ // Index whose crosshair/tooltip is DISPLAYED: the controlled `hoverKey` when
245
+ // provided (resolved against `hoverKeys`), else the internal pointer index.
246
+ const activeIndex = $derived(resolveActiveIndex(hoverKey, hoveredIndex, hoverKeys));
247
+
248
+ // --- Keyboard navigation (FR-5) ------------------------------------------
249
+ // Active when wired explicitly (`keyboardNav`) or implicitly (`onSelectKey`).
250
+ const navEnabled = $derived((keyboardNav === true || onSelectKey !== undefined) && candles.length > 0);
251
+ function focusDatum(index: number) {
252
+ focusedIndex = index;
253
+ datapointRefs[index]?.focus();
254
+ emitHoverKey(index);
255
+ }
256
+ function handleDatapointKeyDown(event: KeyboardEvent, index: number) {
257
+ const action = datapointNavAction(event.key, index, candles.length);
258
+ if (!action) return;
259
+ event.preventDefault();
260
+ if (action.kind === "move") {
261
+ focusDatum(action.index);
262
+ } else if (action.kind === "select") {
263
+ onSelectKey?.(candles[index].datum.label);
264
+ } else {
265
+ focusedIndex = -1;
266
+ emitHoverKey(null);
267
+ onSelectKey?.(null);
268
+ (event.currentTarget as SVGElement).blur();
269
+ }
270
+ }
271
+ function ohlcAriaLabel(d: CandlestickChartDatum): string {
272
+ return datapointAriaLabel(d.label, `O ${d.open} H ${d.high} L ${d.low} C ${d.close}`);
154
273
  }
155
274
 
156
275
  const classes = () => ["st-candlestickChart", className].filter(Boolean).join(" ");
@@ -162,7 +281,7 @@
162
281
  role="img"
163
282
  aria-label={label}
164
283
  onpointermove={handlePointerMove}
165
- onpointerleave={() => (hoveredIndex = null)}
284
+ onpointerleave={handleLeave}
166
285
  >
167
286
  <svg
168
287
  viewBox="0 0 {width} {height}"
@@ -185,6 +304,20 @@
185
304
  <line class="st-candlestickChart__axis" x1={MARGIN.left} x2={MARGIN.left} y1={MARGIN.top} y2={height - MARGIN.bottom} />
186
305
  <line class="st-candlestickChart__axis" x1={MARGIN.left} x2={width - MARGIN.right} y1={height - MARGIN.bottom} y2={height - MARGIN.bottom} />
187
306
 
307
+ <!-- Annotation regions sit BEHIND the candles (filled bands). -->
308
+ {#if annotationRegions.length > 0}
309
+ <g class="st-candlestickChart__annotations st-candlestickChart__annotations--behind">
310
+ {#each annotationRegions as a (a.key)}
311
+ {#if a.kind === "region"}
312
+ <rect class="st-candlestickChart__annotationRegion" x={a.x} y={a.y} width={a.width} height={a.height} />
313
+ {#if a.label}
314
+ <text class="st-candlestickChart__annotationLabel" x={a.x + 4} y={a.y + 11}>{a.label}</text>
315
+ {/if}
316
+ {/if}
317
+ {/each}
318
+ </g>
319
+ {/if}
320
+
188
321
  <!-- FIX #7 : clé composite pour éviter les doublons -->
189
322
  {#each candles as c, i (`${i}-${c.datum.label}`)}
190
323
  <!-- wick -->
@@ -217,13 +350,95 @@
217
350
  {c.datum.label}
218
351
  </text>
219
352
  {/each}
353
+
354
+ <!-- Annotations ABOVE the candles: lines, shapes, points, labels. -->
355
+ {#if annotationAbove.length > 0}
356
+ <g class="st-candlestickChart__annotations st-candlestickChart__annotations--above">
357
+ {#each annotationAbove as a (a.key)}
358
+ {#if a.kind === "line"}
359
+ <line class="st-candlestickChart__annotationLine" x1={a.x1} y1={a.y1} x2={a.x2} y2={a.y2} />
360
+ {#if a.label}
361
+ <text
362
+ class="st-candlestickChart__annotationLabel"
363
+ x={a.axis === "x" ? a.x1 + 4 : MARGIN.left + plotWidth - 4}
364
+ y={a.axis === "x" ? MARGIN.top + 11 : a.y1 - 4}
365
+ text-anchor={a.axis === "x" ? "start" : "end"}
366
+ >{a.label}</text>
367
+ {/if}
368
+ {:else if a.kind === "shape"}
369
+ <polygon class="st-candlestickChart__annotationShape" points={polygonPoints(a.points)} />
370
+ {#if a.label}
371
+ <text class="st-candlestickChart__annotationLabel" x={a.labelX} y={a.labelY} text-anchor="middle">{a.label}</text>
372
+ {/if}
373
+ {:else if a.kind === "point"}
374
+ <circle class="st-candlestickChart__annotationPoint" cx={a.x} cy={a.y} r="4.5" />
375
+ {#if a.label}
376
+ <text class="st-candlestickChart__annotationLabel" x={a.x} y={a.y - 8} text-anchor="middle">{a.label}</text>
377
+ {/if}
378
+ {:else}
379
+ <text class="st-candlestickChart__annotationText" x={a.x} y={a.y} text-anchor={a.anchor}>{a.text}</text>
380
+ {/if}
381
+ {/each}
382
+ </g>
383
+ {/if}
384
+
385
+ <!-- Data labels — one close value per candle, drawn on top. aria-hidden. -->
386
+ {#if dataLabelItems.length > 0}
387
+ <g class="st-candlestickChart__dataLabels" aria-hidden="true">
388
+ {#each dataLabelItems as d (d.key)}
389
+ <text class="st-candlestickChart__dataLabel" x={d.x} y={d.y} text-anchor="middle" dominant-baseline="auto">{d.text}</text>
390
+ {/each}
391
+ </g>
392
+ {/if}
393
+
394
+ <!-- Crosshair (FR-3) — a tokenised dashed vertical line at the active candle.
395
+ Decorative (aria-hidden); the value is in the tooltip + ChartDataList. -->
396
+ {#if activeIndex >= 0 && candles[activeIndex]}
397
+ {@const cc = candles[activeIndex]}
398
+ <g class="st-candlestickChart__crosshair" aria-hidden="true">
399
+ <line class="st-candlestickChart__crosshairLine" x1={cc.centerX} x2={cc.centerX} y1={MARGIN.top} y2={MARGIN.top + plotHeight} />
400
+ </g>
401
+ {/if}
220
402
  </svg>
403
+
404
+ <!-- Keyboard navigation overlay (FR-5) — a focusable, transparent hit layer
405
+ over the candles. NOT aria-hidden: it is the accessible roving cursor. -->
406
+ {#if navEnabled}
407
+ <svg
408
+ class="st-candlestickChart__navLayer"
409
+ viewBox="0 0 {width} {height}"
410
+ preserveAspectRatio="xMidYMid meet"
411
+ width="100%"
412
+ height="100%"
413
+ role="group"
414
+ aria-label={`${label} — points de données`}
415
+ >
416
+ {#each candles as candle, i (`${i}-${candle.datum.label}`)}
417
+ <rect
418
+ bind:this={datapointRefs[i]}
419
+ class="st-candlestickChart__navDatum"
420
+ x={candle.centerX - candle.band / 2}
421
+ y={MARGIN.top}
422
+ width={candle.band}
423
+ height={plotHeight}
424
+ role="img"
425
+ tabindex={rovingTabIndex(i, focusedIndex, candles.length)}
426
+ aria-label={ohlcAriaLabel(candle.datum)}
427
+ onkeydown={(event) => handleDatapointKeyDown(event, i)}
428
+ onfocus={() => {
429
+ focusedIndex = i;
430
+ emitHoverKey(i);
431
+ }}
432
+ />
433
+ {/each}
434
+ </svg>
435
+ {/if}
221
436
  </div>
222
437
 
223
438
  <ChartDataList {label} items={dataValueItems} />
224
439
 
225
- {#if hoveredIndex !== null && candles[hoveredIndex]}
226
- {@const c = candles[hoveredIndex]}
440
+ {#if activeIndex >= 0 && candles[activeIndex]}
441
+ {@const c = candles[activeIndex]}
227
442
  <div
228
443
  class="st-candlestickChart__tooltip"
229
444
  role="presentation"
@@ -251,6 +466,7 @@
251
466
 
252
467
  .st-candlestickChart__visual {
253
468
  display: block;
469
+ position: relative;
254
470
  }
255
471
 
256
472
  .st-candlestickChart__axis {
@@ -294,6 +510,65 @@
294
510
  fill: var(--st-semantic-feedback-error);
295
511
  }
296
512
 
513
+ /* --- Annotation layer ----------------------------------------------------
514
+ Regions render BEHIND the candles; lines/shapes/points/labels render ABOVE. */
515
+ .st-candlestickChart__annotationRegion {
516
+ fill: color-mix(in srgb, var(--st-semantic-feedback-info) 12%, transparent);
517
+ stroke: none;
518
+ }
519
+ .st-candlestickChart__annotationLine {
520
+ stroke: var(--st-semantic-feedback-info);
521
+ stroke-width: 1.5;
522
+ stroke-dasharray: 4 3;
523
+ }
524
+ .st-candlestickChart__annotationShape {
525
+ fill: color-mix(in srgb, var(--st-semantic-feedback-info) 14%, transparent);
526
+ stroke: var(--st-semantic-feedback-info);
527
+ stroke-width: 1.5;
528
+ }
529
+ .st-candlestickChart__annotationPoint {
530
+ fill: var(--st-semantic-feedback-info);
531
+ stroke: var(--st-semantic-surface-default);
532
+ stroke-width: 1.5;
533
+ }
534
+ .st-candlestickChart__annotationLabel,
535
+ .st-candlestickChart__annotationText {
536
+ fill: var(--st-semantic-text-primary);
537
+ font-size: 0.625rem;
538
+ font-weight: 600;
539
+ }
540
+
541
+ /* Data labels — per-candle close value, drawn on top. Token-only colour. */
542
+ .st-candlestickChart__dataLabel {
543
+ fill: var(--st-semantic-text-primary);
544
+ font-size: 0.6875rem;
545
+ font-weight: 600;
546
+ }
547
+
548
+ /* Crosshair (FR-3) — a tokenised dashed vertical line at the active candle. */
549
+ .st-candlestickChart__crosshairLine {
550
+ stroke: var(--st-semantic-border-strong);
551
+ stroke-width: 1;
552
+ stroke-dasharray: 3 3;
553
+ opacity: 0.7;
554
+ }
555
+
556
+ /* Keyboard navigation layer (FR-5) — a focusable, transparent overlay of one
557
+ hit-rect per candle. Carries the roving tab stop; the focus ring is tokenised. */
558
+ .st-candlestickChart__navLayer {
559
+ inset: 0;
560
+ position: absolute;
561
+ }
562
+ .st-candlestickChart__navDatum {
563
+ fill: transparent;
564
+ outline: none;
565
+ }
566
+ .st-candlestickChart__navDatum:focus-visible {
567
+ fill: color-mix(in srgb, var(--st-semantic-border-interactive) 12%, transparent);
568
+ outline: 2px solid var(--st-semantic-border-interactive);
569
+ outline-offset: 1px;
570
+ }
571
+
297
572
  @media (prefers-reduced-motion: reduce) {
298
573
  .st-candlestickChart__body {
299
574
  transition: none;
@@ -7,9 +7,15 @@
7
7
  * label string
8
8
  *
9
9
  * Props optionnelles :
10
- * width number (défaut 480)
11
- * height number (défaut 240)
12
- * class string
10
+ * width number (défaut 480)
11
+ * height number (défaut 240)
12
+ * annotations ChartAnnotation[] - overlay support/résistance/zones/events
13
+ * dataLabels boolean | { format?, position? } - étiquette close par bougie
14
+ * hoverKey string | null - crosshair contrôlé (clé = date/catégorie)
15
+ * onHoverKeyChange (key) => void - émet la date survolée (ou null)
16
+ * keyboardNav boolean - navigation clavier (roving tabindex)
17
+ * onSelectKey (key) => void - sélection clavier (Enter/Space) ; null = Escape
18
+ * class string
13
19
  */
14
20
  export type CandlestickChartDatum = {
15
21
  label: string;
@@ -18,11 +24,19 @@ export type CandlestickChartDatum = {
18
24
  low: number;
19
25
  close: number;
20
26
  };
27
+ import { type ChartAnnotation } from "./chartAnnotations.js";
28
+ import { type DataLabelsProp } from "./chartDataLabels.js";
21
29
  type CandlestickChartProps = {
22
30
  data: CandlestickChartDatum[];
23
31
  label: string;
24
32
  width?: number;
25
33
  height?: number;
34
+ annotations?: ChartAnnotation[];
35
+ dataLabels?: DataLabelsProp;
36
+ hoverKey?: string | null;
37
+ onHoverKeyChange?: (key: string | null) => void;
38
+ keyboardNav?: boolean;
39
+ onSelectKey?: (key: string | null) => void;
26
40
  class?: string;
27
41
  };
28
42
  declare const CandlestickChart: import("svelte").Component<CandlestickChartProps, {}, "">;
@@ -1 +1 @@
1
- {"version":3,"file":"CandlestickChart.svelte.d.ts","sourceRoot":"","sources":["../src/lib/CandlestickChart.svelte.ts"],"names":[],"mappings":"AAGE;;;;;;;;;;;;GAYG;AACH,MAAM,MAAM,qBAAqB,GAAG;IAClC,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,qBAAqB,GAAG;IAC3B,IAAI,EAAE,qBAAqB,EAAE,CAAC;IAC9B,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB,CAAC;AAgLJ,QAAA,MAAM,gBAAgB,2DAAwC,CAAC;AAC/D,KAAK,gBAAgB,GAAG,UAAU,CAAC,OAAO,gBAAgB,CAAC,CAAC;AAC5D,eAAe,gBAAgB,CAAC"}
1
+ {"version":3,"file":"CandlestickChart.svelte.d.ts","sourceRoot":"","sources":["../src/lib/CandlestickChart.svelte.ts"],"names":[],"mappings":"AAGE;;;;;;;;;;;;;;;;;;GAkBG;AACH,MAAM,MAAM,qBAAqB,GAAG;IAClC,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,qBAAqB,GAAG;IAC3B,IAAI,EAAE,qBAAqB,EAAE,CAAC;IAC9B,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;AA2VJ,QAAA,MAAM,gBAAgB,2DAAwC,CAAC;AAC/D,KAAK,gBAAgB,GAAG,UAAU,CAAC,OAAO,gBAAgB,CAAC,CAAC;AAC5D,eAAe,gBAAgB,CAAC"}
@@ -0,0 +1,39 @@
1
+ <script lang="ts">
2
+ import { cellDecorationIcon } from "./cellDecoration.js";
3
+
4
+ let { icon }: { icon?: string } = $props();
5
+
6
+ const nodes = $derived(cellDecorationIcon(icon));
7
+ </script>
8
+
9
+ {#if nodes}
10
+ <svg
11
+ class="st-cell__icon"
12
+ width="14"
13
+ height="14"
14
+ viewBox="0 0 24 24"
15
+ fill="none"
16
+ stroke="currentColor"
17
+ stroke-width="2"
18
+ stroke-linecap="round"
19
+ stroke-linejoin="round"
20
+ aria-hidden="true"
21
+ focusable="false"
22
+ >
23
+ {#each nodes as [tag, attrs]}
24
+ {#if tag === "path"}
25
+ <path {...attrs} />
26
+ {:else if tag === "circle"}
27
+ <circle {...attrs} />
28
+ {:else if tag === "line"}
29
+ <line {...attrs} />
30
+ {/if}
31
+ {/each}
32
+ </svg>
33
+ {/if}
34
+
35
+ <style>
36
+ .st-cell__icon {
37
+ flex: 0 0 auto;
38
+ }
39
+ </style>
@@ -0,0 +1,7 @@
1
+ type $$ComponentProps = {
2
+ icon?: string;
3
+ };
4
+ declare const CellDecorationIcon: import("svelte").Component<$$ComponentProps, {}, "">;
5
+ type CellDecorationIcon = ReturnType<typeof CellDecorationIcon>;
6
+ export default CellDecorationIcon;
7
+ //# sourceMappingURL=CellDecorationIcon.svelte.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"CellDecorationIcon.svelte.d.ts","sourceRoot":"","sources":["../src/lib/CellDecorationIcon.svelte.ts"],"names":[],"mappings":"AAKC,KAAK,gBAAgB,GAAI;IAAE,IAAI,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC;AA2B5C,QAAA,MAAM,kBAAkB,sDAAwC,CAAC;AACjE,KAAK,kBAAkB,GAAG,UAAU,CAAC,OAAO,kBAAkB,CAAC,CAAC;AAChE,eAAe,kBAAkB,CAAC"}