@sia-ui/cli 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (200) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +51 -0
  3. package/dist/diff.mjs +91 -0
  4. package/dist/index.js +325 -0
  5. package/dist/lifecycle.mjs +242 -0
  6. package/dist/lock.mjs +92 -0
  7. package/dist/registry/package/components/Accordion/index.tsx +172 -0
  8. package/dist/registry/package/components/Accordion/styles.css +97 -0
  9. package/dist/registry/package/components/Alert/index.tsx +31 -0
  10. package/dist/registry/package/components/Alert/styles.css +12 -0
  11. package/dist/registry/package/components/AmountDisplay/index.tsx +11 -0
  12. package/dist/registry/package/components/AmountDisplay/styles.css +9 -0
  13. package/dist/registry/package/components/AppShell/index.tsx +6 -0
  14. package/dist/registry/package/components/AppShell/styles.css +7 -0
  15. package/dist/registry/package/components/AspectRatio/index.tsx +29 -0
  16. package/dist/registry/package/components/AspectRatio/styles.css +15 -0
  17. package/dist/registry/package/components/AsyncState/index.tsx +22 -0
  18. package/dist/registry/package/components/AsyncState/styles.css +1 -0
  19. package/dist/registry/package/components/AuditMeta/index.tsx +44 -0
  20. package/dist/registry/package/components/AuditMeta/styles.css +12 -0
  21. package/dist/registry/package/components/AuthLayout/index.tsx +39 -0
  22. package/dist/registry/package/components/AuthLayout/styles.css +6 -0
  23. package/dist/registry/package/components/Autocomplete/index.tsx +8 -0
  24. package/dist/registry/package/components/Avatar/index.tsx +23 -0
  25. package/dist/registry/package/components/Avatar/styles.css +5 -0
  26. package/dist/registry/package/components/Badge/index.tsx +62 -0
  27. package/dist/registry/package/components/Badge/styles.css +26 -0
  28. package/dist/registry/package/components/Breadcrumb/index.tsx +131 -0
  29. package/dist/registry/package/components/Breadcrumb/styles.css +76 -0
  30. package/dist/registry/package/components/Button/index.tsx +91 -0
  31. package/dist/registry/package/components/Button/styles.css +111 -0
  32. package/dist/registry/package/components/Calendar/index.tsx +192 -0
  33. package/dist/registry/package/components/Card/index.tsx +8 -0
  34. package/dist/registry/package/components/Card/styles.css +4 -0
  35. package/dist/registry/package/components/Chart/index.tsx +437 -0
  36. package/dist/registry/package/components/Chart/styles.css +182 -0
  37. package/dist/registry/package/components/Checkbox/index.tsx +193 -0
  38. package/dist/registry/package/components/Checkbox/styles.css +39 -0
  39. package/dist/registry/package/components/ColorPicker/index.tsx +6 -0
  40. package/dist/registry/package/components/ColorPicker/styles.css +2 -0
  41. package/dist/registry/package/components/CommandPalette/index.tsx +7 -0
  42. package/dist/registry/package/components/CommandPalette/styles.css +8 -0
  43. package/dist/registry/package/components/ConfirmDialog/index.tsx +24 -0
  44. package/dist/registry/package/components/Container/index.tsx +11 -0
  45. package/dist/registry/package/components/Container/styles.css +1 -0
  46. package/dist/registry/package/components/Countdown/index.tsx +3 -0
  47. package/dist/registry/package/components/CrudPage/index.tsx +9 -0
  48. package/dist/registry/package/components/CrudPage/styles.css +1 -0
  49. package/dist/registry/package/components/CurrencyInput/index.tsx +15 -0
  50. package/dist/registry/package/components/CurrencyInput/styles.css +1 -0
  51. package/dist/registry/package/components/DataTable/index.tsx +240 -0
  52. package/dist/registry/package/components/DataTable/styles.css +98 -0
  53. package/dist/registry/package/components/DatePicker/index.tsx +102 -0
  54. package/dist/registry/package/components/DateRangeFilter/index.tsx +8 -0
  55. package/dist/registry/package/components/DateRangeFilter/styles.css +4 -0
  56. package/dist/registry/package/components/DateRangePicker/index.tsx +36 -0
  57. package/dist/registry/package/components/DateTimePicker/index.tsx +32 -0
  58. package/dist/registry/package/components/Descriptions/index.tsx +7 -0
  59. package/dist/registry/package/components/Descriptions/styles.css +8 -0
  60. package/dist/registry/package/components/Divider/index.tsx +12 -0
  61. package/dist/registry/package/components/Divider/styles.css +3 -0
  62. package/dist/registry/package/components/Drawer/context.ts +14 -0
  63. package/dist/registry/package/components/Drawer/index.tsx +209 -0
  64. package/dist/registry/package/components/Drawer/styles.css +151 -0
  65. package/dist/registry/package/components/DropdownMenu/index.tsx +350 -0
  66. package/dist/registry/package/components/DropdownMenu/styles.css +100 -0
  67. package/dist/registry/package/components/DurationDisplay/index.tsx +3 -0
  68. package/dist/registry/package/components/EmptyState/index.tsx +23 -0
  69. package/dist/registry/package/components/EmptyState/styles.css +6 -0
  70. package/dist/registry/package/components/EntityMeta/index.tsx +65 -0
  71. package/dist/registry/package/components/EntityMeta/styles.css +9 -0
  72. package/dist/registry/package/components/ErrorState/index.tsx +30 -0
  73. package/dist/registry/package/components/ErrorState/styles.css +6 -0
  74. package/dist/registry/package/components/EventCalendar/index.tsx +8 -0
  75. package/dist/registry/package/components/EventCalendar/styles.css +8 -0
  76. package/dist/registry/package/components/Field/index.tsx +664 -0
  77. package/dist/registry/package/components/Field/styles.css +14 -0
  78. package/dist/registry/package/components/FileUpload/index.tsx +68 -0
  79. package/dist/registry/package/components/FileUpload/styles.css +4 -0
  80. package/dist/registry/package/components/FiltersBar/index.tsx +20 -0
  81. package/dist/registry/package/components/FiltersBar/styles.css +6 -0
  82. package/dist/registry/package/components/Form/index.tsx +211 -0
  83. package/dist/registry/package/components/Form/styles.css +80 -0
  84. package/dist/registry/package/components/FormField/index.tsx +2 -0
  85. package/dist/registry/package/components/HoverCard/index.tsx +121 -0
  86. package/dist/registry/package/components/HoverCard/styles.css +26 -0
  87. package/dist/registry/package/components/IconButton/index.tsx +27 -0
  88. package/dist/registry/package/components/IconButton/styles.css +7 -0
  89. package/dist/registry/package/components/Icons/index.tsx +185 -0
  90. package/dist/registry/package/components/ImageUpload/index.tsx +6 -0
  91. package/dist/registry/package/components/ImageUpload/styles.css +4 -0
  92. package/dist/registry/package/components/Input/index.tsx +103 -0
  93. package/dist/registry/package/components/Input/styles.css +8 -0
  94. package/dist/registry/package/components/InputNumber/index.tsx +196 -0
  95. package/dist/registry/package/components/InputNumber/styles.css +54 -0
  96. package/dist/registry/package/components/JsonEditor/index.tsx +6 -0
  97. package/dist/registry/package/components/JsonEditor/styles.css +2 -0
  98. package/dist/registry/package/components/KpiGrid/index.tsx +11 -0
  99. package/dist/registry/package/components/KpiGrid/styles.css +5 -0
  100. package/dist/registry/package/components/Label/index.tsx +43 -0
  101. package/dist/registry/package/components/Label/styles.css +25 -0
  102. package/dist/registry/package/components/MarkdownEditor/index.tsx +5 -0
  103. package/dist/registry/package/components/MiniCalendar/index.tsx +3 -0
  104. package/dist/registry/package/components/Modal/index.tsx +104 -0
  105. package/dist/registry/package/components/Modal/styles.css +20 -0
  106. package/dist/registry/package/components/MonthPicker/index.tsx +5 -0
  107. package/dist/registry/package/components/MultiSelect/index.tsx +133 -0
  108. package/dist/registry/package/components/MultiSelect/styles.css +3 -0
  109. package/dist/registry/package/components/OtpInput/index.tsx +5 -0
  110. package/dist/registry/package/components/OtpInput/styles.css +2 -0
  111. package/dist/registry/package/components/Overlay/index.tsx +140 -0
  112. package/dist/registry/package/components/Overlay/styles.css +1 -0
  113. package/dist/registry/package/components/PageHeader/index.tsx +24 -0
  114. package/dist/registry/package/components/PageHeader/styles.css +5 -0
  115. package/dist/registry/package/components/Pagination/index.tsx +155 -0
  116. package/dist/registry/package/components/Pagination/styles.css +103 -0
  117. package/dist/registry/package/components/PasswordInput/index.tsx +125 -0
  118. package/dist/registry/package/components/PasswordInput/styles.css +58 -0
  119. package/dist/registry/package/components/PermissionGate/index.tsx +3 -0
  120. package/dist/registry/package/components/PhoneInput/index.tsx +189 -0
  121. package/dist/registry/package/components/PhoneInput/styles.css +35 -0
  122. package/dist/registry/package/components/Popover/index.tsx +12 -0
  123. package/dist/registry/package/components/Popover/styles.css +12 -0
  124. package/dist/registry/package/components/Progress/index.tsx +149 -0
  125. package/dist/registry/package/components/Progress/styles.css +101 -0
  126. package/dist/registry/package/components/QrCode/index.tsx +6 -0
  127. package/dist/registry/package/components/QrCode/styles.css +5 -0
  128. package/dist/registry/package/components/RadioGroup/index.tsx +29 -0
  129. package/dist/registry/package/components/RadioGroup/styles.css +20 -0
  130. package/dist/registry/package/components/Rating/index.tsx +82 -0
  131. package/dist/registry/package/components/Rating/styles.css +41 -0
  132. package/dist/registry/package/components/ReferenceSelect/index.tsx +7 -0
  133. package/dist/registry/package/components/ReferenceSelect/styles.css +2 -0
  134. package/dist/registry/package/components/RelativeTime/index.tsx +22 -0
  135. package/dist/registry/package/components/RelativeTime/styles.css +1 -0
  136. package/dist/registry/package/components/RequireSession/index.tsx +127 -0
  137. package/dist/registry/package/components/RequireSession/styles.css +13 -0
  138. package/dist/registry/package/components/ResizablePanel/index.tsx +6 -0
  139. package/dist/registry/package/components/ResizablePanel/styles.css +5 -0
  140. package/dist/registry/package/components/RichTextEditor/index.tsx +5 -0
  141. package/dist/registry/package/components/RichTextEditor/styles.css +2 -0
  142. package/dist/registry/package/components/ScrollArea/index.tsx +114 -0
  143. package/dist/registry/package/components/ScrollArea/styles.css +74 -0
  144. package/dist/registry/package/components/SearchInput/index.tsx +121 -0
  145. package/dist/registry/package/components/SearchInput/styles.css +6 -0
  146. package/dist/registry/package/components/Select/index.tsx +324 -0
  147. package/dist/registry/package/components/Select/styles.css +58 -0
  148. package/dist/registry/package/components/Sidebar/index.tsx +232 -0
  149. package/dist/registry/package/components/Sidebar/styles.css +140 -0
  150. package/dist/registry/package/components/Skeleton/index.tsx +21 -0
  151. package/dist/registry/package/components/Skeleton/styles.css +2 -0
  152. package/dist/registry/package/components/Slider/index.tsx +193 -0
  153. package/dist/registry/package/components/Slider/styles.css +53 -0
  154. package/dist/registry/package/components/Spinner/index.tsx +28 -0
  155. package/dist/registry/package/components/Spinner/styles.css +8 -0
  156. package/dist/registry/package/components/StatCard/index.tsx +15 -0
  157. package/dist/registry/package/components/StatCard/styles.css +3 -0
  158. package/dist/registry/package/components/Statistic/index.tsx +23 -0
  159. package/dist/registry/package/components/Statistic/styles.css +6 -0
  160. package/dist/registry/package/components/Steps/index.tsx +108 -0
  161. package/dist/registry/package/components/Steps/styles.css +138 -0
  162. package/dist/registry/package/components/Switch/index.tsx +46 -0
  163. package/dist/registry/package/components/Switch/styles.css +26 -0
  164. package/dist/registry/package/components/Tabs/index.tsx +44 -0
  165. package/dist/registry/package/components/Tabs/styles.css +41 -0
  166. package/dist/registry/package/components/TagsInput/index.tsx +6 -0
  167. package/dist/registry/package/components/TagsInput/styles.css +2 -0
  168. package/dist/registry/package/components/Textarea/index.tsx +21 -0
  169. package/dist/registry/package/components/Textarea/styles.css +7 -0
  170. package/dist/registry/package/components/TimePicker/index.tsx +76 -0
  171. package/dist/registry/package/components/TimePicker/styles.css +13 -0
  172. package/dist/registry/package/components/TimeRangePicker/index.tsx +32 -0
  173. package/dist/registry/package/components/TimeRangePicker/styles.css +1 -0
  174. package/dist/registry/package/components/Timeline/index.tsx +8 -0
  175. package/dist/registry/package/components/Timeline/styles.css +12 -0
  176. package/dist/registry/package/components/Timer/index.tsx +9 -0
  177. package/dist/registry/package/components/Toast/index.tsx +426 -0
  178. package/dist/registry/package/components/Toast/styles.css +175 -0
  179. package/dist/registry/package/components/Toggle/index.tsx +181 -0
  180. package/dist/registry/package/components/Toggle/styles.css +124 -0
  181. package/dist/registry/package/components/Tooltip/index.tsx +163 -0
  182. package/dist/registry/package/components/Tooltip/styles.css +57 -0
  183. package/dist/registry/package/components/Tour/index.tsx +8 -0
  184. package/dist/registry/package/components/Tour/styles.css +6 -0
  185. package/dist/registry/package/components/Transfer/index.tsx +7 -0
  186. package/dist/registry/package/components/Transfer/styles.css +6 -0
  187. package/dist/registry/package/components/Tree/index.tsx +8 -0
  188. package/dist/registry/package/components/Tree/styles.css +6 -0
  189. package/dist/registry/package/components/TreeSelect/index.tsx +10 -0
  190. package/dist/registry/package/components/TreeSelect/styles.css +3 -0
  191. package/dist/registry/package/components/Typography/index.tsx +27 -0
  192. package/dist/registry/package/components/Typography/styles.css +16 -0
  193. package/dist/registry/package/components/UnsavedChangesGuard/index.tsx +4 -0
  194. package/dist/registry/package/components/WeekPicker/index.tsx +5 -0
  195. package/dist/registry/package/components/YearPicker/index.tsx +3 -0
  196. package/dist/registry/package/lib/form-adapter-rhf.tsx +141 -0
  197. package/dist/registry/registry.json +1780 -0
  198. package/dist/registry/styles/sia-ui.css +361 -0
  199. package/dist/source.mjs +93 -0
  200. package/package.json +55 -0
@@ -0,0 +1,437 @@
1
+ import {
2
+ useId,
3
+ useMemo,
4
+ useState,
5
+ type MouseEvent,
6
+ type ReactNode,
7
+ } from "react";
8
+ import { cn } from "@sia-ui/utils";
9
+ import type { ComponentTone } from "@sia-ui/tokens";
10
+ import "./styles.css";
11
+
12
+ export interface ChartSeries {
13
+ key: string;
14
+ label?: string;
15
+ values: Array<number | null>;
16
+ tone?: ComponentTone;
17
+ /** Couleur explicite. Prioritaire sur `tone`. */
18
+ color?: string;
19
+ }
20
+
21
+ export interface ChartProps {
22
+ series: ChartSeries[];
23
+ /** Les libellés de l'axe horizontal. Un par point. */
24
+ labels?: string[];
25
+ type?: "line" | "area" | "bar";
26
+ height?: number;
27
+ /** Bornes de l'axe vertical. Par défaut, déduites des données. */
28
+ min?: number;
29
+ max?: number;
30
+ showGrid?: boolean;
31
+ showAxis?: boolean;
32
+ showLegend?: boolean;
33
+ /** Mise en forme des valeurs — montants, pourcentages. */
34
+ format?: (value: number) => string;
35
+ /** Rend le graphique muet : ni survol, ni infobulle. */
36
+ readOnly?: boolean;
37
+ caption?: ReactNode;
38
+ className?: string;
39
+ }
40
+
41
+ const TONE_VAR: Record<ComponentTone, string> = {
42
+ primary: "var(--sia-primary)",
43
+ neutral: "var(--sia-muted)",
44
+ info: "var(--sia-info)",
45
+ success: "var(--sia-success)",
46
+ warning: "var(--sia-warning)",
47
+ danger: "var(--sia-danger)",
48
+ };
49
+
50
+ /** La palette de secours, dans l'ordre d'apparition des séries. */
51
+ const PALETTE: ComponentTone[] = [
52
+ "primary",
53
+ "info",
54
+ "success",
55
+ "warning",
56
+ "danger",
57
+ "neutral",
58
+ ];
59
+
60
+ // L'espace de coordonnées est fixe : 100 unités de large, `VIEW_H` de haut.
61
+ // Le SVG est ensuite étiré à la largeur disponible. `vector-effect` garde les
62
+ // traits à leur épaisseur réelle malgré l'étirement — c'est ce qui permet de
63
+ // ne jamais mesurer le conteneur.
64
+ const VIEW_W = 100;
65
+ const VIEW_H = 100;
66
+
67
+ function colorOf(series: ChartSeries, index: number) {
68
+ if (series.color) return series.color;
69
+ return TONE_VAR[series.tone ?? PALETTE[index % PALETTE.length]!];
70
+ }
71
+
72
+ /**
73
+ * Un graphique sans dépendance.
74
+ *
75
+ * Il couvre les trois formes qui reviennent dans un tableau de bord — courbe,
76
+ * aire, barres — et rien d'autre. `recharts` reste le bon choix dès qu'il
77
+ * faut des axes secondaires, du zoom ou des annotations; il pèse alors 90 ko
78
+ * qu'un projet choisit d'ajouter plutôt que de subir.
79
+ */
80
+ export function Chart({
81
+ series,
82
+ labels = [],
83
+ type = "line",
84
+ height = 220,
85
+ min: minProp,
86
+ max: maxProp,
87
+ showGrid = true,
88
+ showAxis = true,
89
+ showLegend = true,
90
+ format = (value) => String(value),
91
+ readOnly = false,
92
+ caption,
93
+ className,
94
+ }: ChartProps) {
95
+ const gradientId = useId();
96
+ const [hover, setHover] = useState<number | null>(null);
97
+
98
+ const points = Math.max(...series.map((entry) => entry.values.length), 0);
99
+
100
+ const { min, max } = useMemo(() => {
101
+ const all = series
102
+ .flatMap((entry) => entry.values)
103
+ .filter((value): value is number => value !== null);
104
+ if (all.length === 0) return { min: 0, max: 1 };
105
+
106
+ const low = minProp ?? Math.min(0, ...all);
107
+ const high = maxProp ?? Math.max(...all);
108
+ // Un maximum égal au minimum donnerait une division par zéro; une marge
109
+ // de 1 aplatit la courbe au milieu plutôt que de la faire disparaître.
110
+ return { min: low, max: high === low ? low + 1 : high };
111
+ }, [maxProp, minProp, series]);
112
+
113
+ const scaleY = (value: number) =>
114
+ VIEW_H - ((value - min) / (max - min)) * VIEW_H;
115
+ // La position horizontale d'un point, de 0 à 1. Le SVG la veut en unités
116
+ // de son viewBox, les repères HTML en pourcentage : une seule source.
117
+ const ratioX = (index: number) => (points <= 1 ? 0.5 : index / (points - 1));
118
+ const scaleX = (index: number) => ratioX(index) * VIEW_W;
119
+
120
+ const paths = useMemo(
121
+ () =>
122
+ series.map((entry) => {
123
+ const segments: string[] = [];
124
+ let open = false;
125
+
126
+ entry.values.forEach((value, index) => {
127
+ if (value === null) {
128
+ // Un trou dans les données coupe le trait au lieu de le tirer
129
+ // d'un point à l'autre : une ligne droite sur une semaine
130
+ // manquante se lirait comme une mesure.
131
+ open = false;
132
+ return;
133
+ }
134
+ const x = scaleX(index);
135
+ const y = scaleY(value);
136
+ segments.push(`${open ? "L" : "M"}${x.toFixed(2)} ${y.toFixed(2)}`);
137
+ open = true;
138
+ });
139
+
140
+ return segments.join(" ");
141
+ }),
142
+ // eslint-disable-next-line react-hooks/exhaustive-deps
143
+ [series, min, max, points],
144
+ );
145
+
146
+ const gridLines = [0, 0.25, 0.5, 0.75, 1];
147
+
148
+ const handleMove = (event: MouseEvent<SVGSVGElement>) => {
149
+ if (readOnly || points === 0) return;
150
+ const rect = event.currentTarget.getBoundingClientRect();
151
+ const ratio = (event.clientX - rect.left) / rect.width;
152
+ setHover(
153
+ Math.min(points - 1, Math.max(0, Math.round(ratio * (points - 1)))),
154
+ );
155
+ };
156
+
157
+ return (
158
+ <figure
159
+ className={cn(
160
+ "sia-chart",
161
+ `sia-chart--${type}`,
162
+ !showAxis && "sia-chart--no-axis",
163
+ className,
164
+ )}
165
+ >
166
+ {/* Une grille plutôt qu'une rangée : la colonne des valeurs se
167
+ dimensionne sur son contenu, et les libellés du bas se placent
168
+ sous le tracé — pas sous l'ensemble. Alignés sur la rangée
169
+ entière, ils étaient décalés de la largeur de cette colonne. */}
170
+ <div
171
+ className="sia-chart__frame"
172
+ style={{ ["--sia-chart-height" as string]: `${height}px` }}
173
+ >
174
+ {showAxis && (
175
+ <div className="sia-chart__axis-y" aria-hidden="true">
176
+ {[max, (max + min) / 2, min].map((value, index) => (
177
+ <span key={index}>{format(value)}</span>
178
+ ))}
179
+ </div>
180
+ )}
181
+
182
+ <div className="sia-chart__canvas">
183
+ <svg
184
+ className="sia-chart__svg"
185
+ viewBox={`0 0 ${VIEW_W} ${VIEW_H}`}
186
+ // L'étirement non uniforme est assumé : c'est ce qui évite de
187
+ // mesurer le conteneur et de re-rendre à chaque redimensionnement.
188
+ preserveAspectRatio="none"
189
+ role="img"
190
+ {...(caption && typeof caption === "string"
191
+ ? { "aria-label": caption }
192
+ : {})}
193
+ onMouseMove={handleMove}
194
+ onMouseLeave={() => setHover(null)}
195
+ >
196
+ <defs>
197
+ {/* Le volet qui découvre le tracé, de la gauche vers la
198
+ droite. Les tirets ne conviennent pas ici :
199
+ `vector-effect="non-scaling-stroke"` fait calculer le
200
+ pointillé en pixels écran, ce qui rend la normalisation
201
+ par `pathLength` sans effet — la courbe sort hachurée. */}
202
+ <clipPath
203
+ id={`${gradientId}-reveal`}
204
+ clipPathUnits="objectBoundingBox"
205
+ >
206
+ <rect className="sia-chart__reveal" width="1" height="1" />
207
+ </clipPath>
208
+ {series.map((entry, index) => (
209
+ <linearGradient
210
+ key={entry.key}
211
+ id={`${gradientId}-${index}`}
212
+ x1="0"
213
+ y1="0"
214
+ x2="0"
215
+ y2="1"
216
+ >
217
+ <stop
218
+ offset="0%"
219
+ stopColor={colorOf(entry, index)}
220
+ stopOpacity=".28"
221
+ />
222
+ <stop
223
+ offset="100%"
224
+ stopColor={colorOf(entry, index)}
225
+ stopOpacity="0"
226
+ />
227
+ </linearGradient>
228
+ ))}
229
+ </defs>
230
+
231
+ {showGrid &&
232
+ gridLines.map((ratio) => (
233
+ <line
234
+ key={ratio}
235
+ className="sia-chart__grid"
236
+ x1="0"
237
+ x2={VIEW_W}
238
+ y1={ratio * VIEW_H}
239
+ y2={ratio * VIEW_H}
240
+ vectorEffect="non-scaling-stroke"
241
+ />
242
+ ))}
243
+
244
+ {type === "bar"
245
+ ? series.map((entry, seriesIndex) => {
246
+ const slot = points > 0 ? VIEW_W / points : VIEW_W;
247
+ const width = (slot * 0.7) / series.length;
248
+ return (
249
+ <g key={entry.key} fill={colorOf(entry, seriesIndex)}>
250
+ {entry.values.map((value, index) =>
251
+ value === null ? null : (
252
+ <rect
253
+ key={index}
254
+ className="sia-chart__bar"
255
+ x={index * slot + slot * 0.15 + seriesIndex * width}
256
+ y={scaleY(value)}
257
+ width={width}
258
+ height={Math.max(0, VIEW_H - scaleY(value))}
259
+ data-active={index === hover || undefined}
260
+ style={{
261
+ // Les barres poussent l'une après l'autre, de la
262
+ // gauche vers la droite, comme on les lit.
263
+ animationDelay: `${Math.min(index, 24) * 30}ms`,
264
+ }}
265
+ />
266
+ ),
267
+ )}
268
+ </g>
269
+ );
270
+ })
271
+ : series.map((entry, index) => (
272
+ <g key={entry.key} clipPath={`url(#${gradientId}-reveal)`}>
273
+ {type === "area" && paths[index] && (
274
+ <path
275
+ className="sia-chart__area"
276
+ d={`${paths[index]} L${VIEW_W} ${VIEW_H} L0 ${VIEW_H} Z`}
277
+ fill={`url(#${gradientId}-${index})`}
278
+ />
279
+ )}
280
+ <path
281
+ className="sia-chart__line"
282
+ d={paths[index] ?? ""}
283
+ fill="none"
284
+ stroke={colorOf(entry, index)}
285
+ vectorEffect="non-scaling-stroke"
286
+ />
287
+ </g>
288
+ ))}
289
+
290
+ {hover !== null && type !== "bar" && (
291
+ <line
292
+ className="sia-chart__cursor"
293
+ x1={scaleX(hover)}
294
+ x2={scaleX(hover)}
295
+ y1="0"
296
+ y2={VIEW_H}
297
+ vectorEffect="non-scaling-stroke"
298
+ />
299
+ )}
300
+ </svg>
301
+
302
+ {/* Les repères sont des éléments HTML, pas des cercles SVG.
303
+
304
+ Le viewBox est étiré horizontalement — c'est ce qui évite de
305
+ mesurer le conteneur — et cet étirement déforme la géométrie :
306
+ un cercle y devient une ellipse. `vector-effect` protège
307
+ l'épaisseur du trait, jamais la forme. Posés au-dessus en
308
+ pourcentage, les repères restent ronds à toute largeur. */}
309
+ {hover !== null &&
310
+ type !== "bar" &&
311
+ series.map((entry, index) => {
312
+ const value = entry.values[hover];
313
+ if (value === null || value === undefined) return null;
314
+ return (
315
+ <span
316
+ key={entry.key}
317
+ className="sia-chart__point"
318
+ style={{
319
+ left: `${ratioX(hover) * 100}%`,
320
+ top: `${(scaleY(value) / VIEW_H) * 100}%`,
321
+ background: colorOf(entry, index),
322
+ }}
323
+ aria-hidden="true"
324
+ />
325
+ );
326
+ })}
327
+
328
+ {hover !== null && !readOnly && (
329
+ <div
330
+ className="sia-chart__tooltip"
331
+ style={{ left: `${ratioX(hover) * 100}%` }}
332
+ // Aux deux extrémités, une infobulle centrée sort du cadre.
333
+ // Elle s'accroche alors par son bord au lieu de son milieu.
334
+ data-align={
335
+ ratioX(hover) < 0.15
336
+ ? "start"
337
+ : ratioX(hover) > 0.85
338
+ ? "end"
339
+ : undefined
340
+ }
341
+ role="status"
342
+ >
343
+ {labels[hover] && (
344
+ <strong className="sia-chart__tooltip-title">
345
+ {labels[hover]}
346
+ </strong>
347
+ )}
348
+ {series.map((entry, index) => {
349
+ const value = entry.values[hover];
350
+ if (value === null || value === undefined) return null;
351
+ return (
352
+ <span key={entry.key} className="sia-chart__tooltip-row">
353
+ <span
354
+ className="sia-chart__dot"
355
+ style={{ background: colorOf(entry, index) }}
356
+ />
357
+ {entry.label && <span>{entry.label}</span>}
358
+ <b>{format(value)}</b>
359
+ </span>
360
+ );
361
+ })}
362
+ </div>
363
+ )}
364
+ </div>
365
+
366
+ {showAxis && labels.length > 0 && (
367
+ <div className="sia-chart__axis-x" aria-hidden="true">
368
+ {labels.map((label, index) => (
369
+ <span key={index} data-active={index === hover || undefined}>
370
+ {label}
371
+ </span>
372
+ ))}
373
+ </div>
374
+ )}
375
+ </div>
376
+
377
+ {showLegend && series.some((entry) => entry.label) && (
378
+ <figcaption className="sia-chart__legend">
379
+ {series.map((entry, index) => (
380
+ <span key={entry.key}>
381
+ <span
382
+ className="sia-chart__dot"
383
+ style={{ background: colorOf(entry, index) }}
384
+ />
385
+ {entry.label}
386
+ </span>
387
+ ))}
388
+ </figcaption>
389
+ )}
390
+
391
+ {caption && typeof caption !== "string" && (
392
+ <figcaption className="sia-chart__caption">{caption}</figcaption>
393
+ )}
394
+ </figure>
395
+ );
396
+ }
397
+
398
+ export interface SparklineProps {
399
+ values: Array<number | null>;
400
+ tone?: ComponentTone;
401
+ color?: string;
402
+ width?: number | string;
403
+ height?: number;
404
+ /** Ajoute l'aire sous la courbe. */
405
+ filled?: boolean;
406
+ className?: string;
407
+ }
408
+
409
+ /**
410
+ * Une courbe minuscule, sans axes ni survol.
411
+ *
412
+ * Elle tient dans une cellule de tableau ou une carte de statistique, là où
413
+ * un graphique complet ne rentrerait pas.
414
+ */
415
+ export function Sparkline({
416
+ values,
417
+ tone = "primary",
418
+ color,
419
+ width = "100%",
420
+ height = 32,
421
+ filled = false,
422
+ className,
423
+ }: SparklineProps) {
424
+ return (
425
+ <span className={cn("sia-sparkline", className)} style={{ width, height }}>
426
+ <Chart
427
+ series={[{ key: "spark", values, ...(color ? { color } : { tone }) }]}
428
+ type={filled ? "area" : "line"}
429
+ height={height}
430
+ showGrid={false}
431
+ showAxis={false}
432
+ showLegend={false}
433
+ readOnly
434
+ />
435
+ </span>
436
+ );
437
+ }
@@ -0,0 +1,182 @@
1
+ .sia-chart { display: grid; gap: .5rem; margin: 0; }
2
+
3
+ /* Deux colonnes : les valeurs à gauche, le tracé à droite. Les libellés du
4
+ bas occupent la seconde colonne, donc ils s'alignent sur le tracé et non
5
+ sur l'ensemble — c'est la largeur de la colonne des valeurs qui les
6
+ décalait. */
7
+ .sia-chart__frame {
8
+ display: grid;
9
+ grid-template-columns: auto minmax(0, 1fr);
10
+ column-gap: .5rem;
11
+ row-gap: .35rem;
12
+ }
13
+ .sia-chart--no-axis .sia-chart__frame { grid-template-columns: minmax(0, 1fr); }
14
+
15
+ /* Le repère du tracé : tout ce qui est posé en pourcentage — repères de
16
+ survol, infobulle — se mesure sur cette boîte, qui est exactement la
17
+ zone dessinée. */
18
+ .sia-chart__canvas {
19
+ position: relative;
20
+ grid-column: -2;
21
+ grid-row: 1;
22
+ min-width: 0;
23
+ height: var(--sia-chart-height, 220px);
24
+ }
25
+
26
+ .sia-chart__svg {
27
+ display: block;
28
+ width: 100%;
29
+ height: 100%;
30
+ overflow: visible;
31
+ }
32
+
33
+ .sia-chart__grid { stroke: var(--sia-border); stroke-width: 1; stroke-dasharray: 3 4; opacity: .7; }
34
+
35
+ .sia-chart__line {
36
+ stroke-width: 2;
37
+ stroke-linecap: round;
38
+ stroke-linejoin: round;
39
+ }
40
+
41
+ /* Le tracé se découvre de la gauche vers la droite.
42
+
43
+ Un volet plutôt qu'un pointillé animé : `vector-effect: non-scaling-stroke`
44
+ — indispensable pour garder l'épaisseur du trait malgré l'étirement non
45
+ uniforme du viewBox — fait calculer les tirets en pixels écran. La
46
+ normalisation par `pathLength` n'a alors plus d'effet et la courbe sort
47
+ hachurée. Le volet, lui, ne dépend d'aucune longueur. */
48
+ .sia-chart__reveal {
49
+ transform-origin: left;
50
+ animation: sia-chart-reveal 900ms var(--sia-easing-standard) both;
51
+ }
52
+ @keyframes sia-chart-reveal {
53
+ from { transform: scaleX(0); }
54
+ to { transform: scaleX(1); }
55
+ }
56
+
57
+ .sia-chart__area {
58
+ opacity: 0;
59
+ animation: sia-chart-fill 700ms var(--sia-easing-standard) 250ms forwards;
60
+ }
61
+ @keyframes sia-chart-fill {
62
+ to { opacity: 1; }
63
+ }
64
+
65
+ .sia-chart__bar {
66
+ transform-box: fill-box;
67
+ transform-origin: bottom;
68
+ animation: sia-chart-grow var(--sia-duration-slow) var(--sia-easing-spring) both;
69
+ transition: opacity var(--sia-duration-fast) var(--sia-easing-standard);
70
+ }
71
+ .sia-chart--bar:hover .sia-chart__bar { opacity: .55; }
72
+ .sia-chart--bar .sia-chart__bar[data-active],
73
+ .sia-chart--bar:not(:hover) .sia-chart__bar { opacity: 1; }
74
+ @keyframes sia-chart-grow {
75
+ from { transform: scaleY(0); }
76
+ to { transform: scaleY(1); }
77
+ }
78
+
79
+ .sia-chart__cursor { stroke: var(--sia-border-strong); stroke-width: 1; }
80
+
81
+ /* Le repère est un élément HTML posé au-dessus du tracé, pas un cercle SVG :
82
+ le viewBox est étiré horizontalement, et cet étirement transforme un
83
+ cercle en ellipse. `vector-effect` protège l'épaisseur du trait, jamais
84
+ la géométrie. */
85
+ .sia-chart__point {
86
+ position: absolute;
87
+ width: .4rem;
88
+ height: .4rem;
89
+ border: 2px solid var(--sia-background);
90
+ border-radius: 999px;
91
+ /* `content-box` garde la pastille ronde : sans lui, la bordure entre
92
+ dans les dimensions et le cercle rétrécit à mesure qu'elle épaissit. */
93
+ box-sizing: content-box;
94
+ transform: translate(-50%, -50%);
95
+ pointer-events: none;
96
+ /* Il glisse d'un point à l'autre au lieu d'apparaître : c'est ce qui rend
97
+ le déplacement du curseur lisible. Aucune animation d'entrée — elle
98
+ porterait sur la même propriété que le centrage, et le repère resterait
99
+ à l'échelle zéro partout où elle ne joue pas. */
100
+ transition:
101
+ left var(--sia-duration-fast) var(--sia-easing-standard),
102
+ top var(--sia-duration-fast) var(--sia-easing-standard);
103
+ }
104
+
105
+ .sia-chart__axis-y {
106
+ grid-column: 1;
107
+ grid-row: 1;
108
+ display: flex;
109
+ flex-direction: column;
110
+ justify-content: space-between;
111
+ color: var(--sia-muted);
112
+ font-size: .7rem;
113
+ font-variant-numeric: tabular-nums;
114
+ text-align: right;
115
+ }
116
+
117
+ .sia-chart__axis-x {
118
+ grid-column: -2;
119
+ grid-row: 2;
120
+ display: flex;
121
+ justify-content: space-between;
122
+ gap: .5rem;
123
+ color: var(--sia-muted);
124
+ font-size: .7rem;
125
+ }
126
+ .sia-chart__axis-x span {
127
+ overflow: hidden;
128
+ text-overflow: ellipsis;
129
+ white-space: nowrap;
130
+ transition: color var(--sia-duration-fast) var(--sia-easing-standard);
131
+ }
132
+ .sia-chart__axis-x span[data-active] { color: var(--sia-foreground); font-weight: 600; }
133
+
134
+ .sia-chart__tooltip {
135
+ position: absolute;
136
+ top: 0;
137
+ z-index: 2;
138
+ display: grid;
139
+ gap: .2rem;
140
+ padding: .5rem .625rem;
141
+ border: 1px solid var(--sia-border);
142
+ border-radius: var(--sia-radius);
143
+ background: var(--sia-surface-raised);
144
+ box-shadow: var(--sia-elevation-overlay);
145
+ font-size: .75rem;
146
+ white-space: nowrap;
147
+ pointer-events: none;
148
+ /* Centrée sur le point, elle suit le curseur sans jamais le recouvrir. */
149
+ transform: translateX(-50%);
150
+ max-width: 100%;
151
+ transition: left var(--sia-duration-fast) var(--sia-easing-standard);
152
+ animation: sia-fade-in var(--sia-duration-fast) var(--sia-easing-standard);
153
+ }
154
+ /* Aux deux extrémités, une infobulle centrée déborderait du cadre. Elle
155
+ s'accroche alors par son bord au point plutôt que par son milieu. */
156
+ .sia-chart__tooltip[data-align="start"] { transform: translateX(0); }
157
+ .sia-chart__tooltip[data-align="end"] { transform: translateX(-100%); }
158
+
159
+ .sia-chart__tooltip-title { color: var(--sia-foreground); font-size: .72rem; }
160
+ .sia-chart__tooltip-row { display: flex; align-items: center; gap: .4rem; color: var(--sia-muted); }
161
+ .sia-chart__tooltip-row b { margin-left: auto; color: var(--sia-foreground); font-variant-numeric: tabular-nums; }
162
+
163
+ .sia-chart__legend {
164
+ display: flex;
165
+ flex-wrap: wrap;
166
+ gap: .25rem 1rem;
167
+ color: var(--sia-muted);
168
+ font-size: .75rem;
169
+ }
170
+ .sia-chart__legend span { display: inline-flex; align-items: center; gap: .35rem; }
171
+ .sia-chart__caption { color: var(--sia-muted); font-size: .75rem; }
172
+
173
+ .sia-chart__dot { display: inline-block; width: .5rem; height: .5rem; border-radius: 999px; flex: none; }
174
+
175
+ /* ── Sparkline ─────────────────────────────────────────────────────────── */
176
+
177
+ .sia-sparkline { display: inline-block; vertical-align: middle; }
178
+ /* La hauteur vient de `--sia-chart-height`, comme partout ailleurs : un
179
+ `height: 100%` ici se résoudrait contre une grille de hauteur automatique
180
+ et rendait la vignette carrée. */
181
+ .sia-sparkline .sia-chart { gap: 0; }
182
+ .sia-sparkline .sia-chart__line { stroke-width: 1.5; }