@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.
- package/LICENSE +21 -0
- package/README.md +51 -0
- package/dist/diff.mjs +91 -0
- package/dist/index.js +325 -0
- package/dist/lifecycle.mjs +242 -0
- package/dist/lock.mjs +92 -0
- package/dist/registry/package/components/Accordion/index.tsx +172 -0
- package/dist/registry/package/components/Accordion/styles.css +97 -0
- package/dist/registry/package/components/Alert/index.tsx +31 -0
- package/dist/registry/package/components/Alert/styles.css +12 -0
- package/dist/registry/package/components/AmountDisplay/index.tsx +11 -0
- package/dist/registry/package/components/AmountDisplay/styles.css +9 -0
- package/dist/registry/package/components/AppShell/index.tsx +6 -0
- package/dist/registry/package/components/AppShell/styles.css +7 -0
- package/dist/registry/package/components/AspectRatio/index.tsx +29 -0
- package/dist/registry/package/components/AspectRatio/styles.css +15 -0
- package/dist/registry/package/components/AsyncState/index.tsx +22 -0
- package/dist/registry/package/components/AsyncState/styles.css +1 -0
- package/dist/registry/package/components/AuditMeta/index.tsx +44 -0
- package/dist/registry/package/components/AuditMeta/styles.css +12 -0
- package/dist/registry/package/components/AuthLayout/index.tsx +39 -0
- package/dist/registry/package/components/AuthLayout/styles.css +6 -0
- package/dist/registry/package/components/Autocomplete/index.tsx +8 -0
- package/dist/registry/package/components/Avatar/index.tsx +23 -0
- package/dist/registry/package/components/Avatar/styles.css +5 -0
- package/dist/registry/package/components/Badge/index.tsx +62 -0
- package/dist/registry/package/components/Badge/styles.css +26 -0
- package/dist/registry/package/components/Breadcrumb/index.tsx +131 -0
- package/dist/registry/package/components/Breadcrumb/styles.css +76 -0
- package/dist/registry/package/components/Button/index.tsx +91 -0
- package/dist/registry/package/components/Button/styles.css +111 -0
- package/dist/registry/package/components/Calendar/index.tsx +192 -0
- package/dist/registry/package/components/Card/index.tsx +8 -0
- package/dist/registry/package/components/Card/styles.css +4 -0
- package/dist/registry/package/components/Chart/index.tsx +437 -0
- package/dist/registry/package/components/Chart/styles.css +182 -0
- package/dist/registry/package/components/Checkbox/index.tsx +193 -0
- package/dist/registry/package/components/Checkbox/styles.css +39 -0
- package/dist/registry/package/components/ColorPicker/index.tsx +6 -0
- package/dist/registry/package/components/ColorPicker/styles.css +2 -0
- package/dist/registry/package/components/CommandPalette/index.tsx +7 -0
- package/dist/registry/package/components/CommandPalette/styles.css +8 -0
- package/dist/registry/package/components/ConfirmDialog/index.tsx +24 -0
- package/dist/registry/package/components/Container/index.tsx +11 -0
- package/dist/registry/package/components/Container/styles.css +1 -0
- package/dist/registry/package/components/Countdown/index.tsx +3 -0
- package/dist/registry/package/components/CrudPage/index.tsx +9 -0
- package/dist/registry/package/components/CrudPage/styles.css +1 -0
- package/dist/registry/package/components/CurrencyInput/index.tsx +15 -0
- package/dist/registry/package/components/CurrencyInput/styles.css +1 -0
- package/dist/registry/package/components/DataTable/index.tsx +240 -0
- package/dist/registry/package/components/DataTable/styles.css +98 -0
- package/dist/registry/package/components/DatePicker/index.tsx +102 -0
- package/dist/registry/package/components/DateRangeFilter/index.tsx +8 -0
- package/dist/registry/package/components/DateRangeFilter/styles.css +4 -0
- package/dist/registry/package/components/DateRangePicker/index.tsx +36 -0
- package/dist/registry/package/components/DateTimePicker/index.tsx +32 -0
- package/dist/registry/package/components/Descriptions/index.tsx +7 -0
- package/dist/registry/package/components/Descriptions/styles.css +8 -0
- package/dist/registry/package/components/Divider/index.tsx +12 -0
- package/dist/registry/package/components/Divider/styles.css +3 -0
- package/dist/registry/package/components/Drawer/context.ts +14 -0
- package/dist/registry/package/components/Drawer/index.tsx +209 -0
- package/dist/registry/package/components/Drawer/styles.css +151 -0
- package/dist/registry/package/components/DropdownMenu/index.tsx +350 -0
- package/dist/registry/package/components/DropdownMenu/styles.css +100 -0
- package/dist/registry/package/components/DurationDisplay/index.tsx +3 -0
- package/dist/registry/package/components/EmptyState/index.tsx +23 -0
- package/dist/registry/package/components/EmptyState/styles.css +6 -0
- package/dist/registry/package/components/EntityMeta/index.tsx +65 -0
- package/dist/registry/package/components/EntityMeta/styles.css +9 -0
- package/dist/registry/package/components/ErrorState/index.tsx +30 -0
- package/dist/registry/package/components/ErrorState/styles.css +6 -0
- package/dist/registry/package/components/EventCalendar/index.tsx +8 -0
- package/dist/registry/package/components/EventCalendar/styles.css +8 -0
- package/dist/registry/package/components/Field/index.tsx +664 -0
- package/dist/registry/package/components/Field/styles.css +14 -0
- package/dist/registry/package/components/FileUpload/index.tsx +68 -0
- package/dist/registry/package/components/FileUpload/styles.css +4 -0
- package/dist/registry/package/components/FiltersBar/index.tsx +20 -0
- package/dist/registry/package/components/FiltersBar/styles.css +6 -0
- package/dist/registry/package/components/Form/index.tsx +211 -0
- package/dist/registry/package/components/Form/styles.css +80 -0
- package/dist/registry/package/components/FormField/index.tsx +2 -0
- package/dist/registry/package/components/HoverCard/index.tsx +121 -0
- package/dist/registry/package/components/HoverCard/styles.css +26 -0
- package/dist/registry/package/components/IconButton/index.tsx +27 -0
- package/dist/registry/package/components/IconButton/styles.css +7 -0
- package/dist/registry/package/components/Icons/index.tsx +185 -0
- package/dist/registry/package/components/ImageUpload/index.tsx +6 -0
- package/dist/registry/package/components/ImageUpload/styles.css +4 -0
- package/dist/registry/package/components/Input/index.tsx +103 -0
- package/dist/registry/package/components/Input/styles.css +8 -0
- package/dist/registry/package/components/InputNumber/index.tsx +196 -0
- package/dist/registry/package/components/InputNumber/styles.css +54 -0
- package/dist/registry/package/components/JsonEditor/index.tsx +6 -0
- package/dist/registry/package/components/JsonEditor/styles.css +2 -0
- package/dist/registry/package/components/KpiGrid/index.tsx +11 -0
- package/dist/registry/package/components/KpiGrid/styles.css +5 -0
- package/dist/registry/package/components/Label/index.tsx +43 -0
- package/dist/registry/package/components/Label/styles.css +25 -0
- package/dist/registry/package/components/MarkdownEditor/index.tsx +5 -0
- package/dist/registry/package/components/MiniCalendar/index.tsx +3 -0
- package/dist/registry/package/components/Modal/index.tsx +104 -0
- package/dist/registry/package/components/Modal/styles.css +20 -0
- package/dist/registry/package/components/MonthPicker/index.tsx +5 -0
- package/dist/registry/package/components/MultiSelect/index.tsx +133 -0
- package/dist/registry/package/components/MultiSelect/styles.css +3 -0
- package/dist/registry/package/components/OtpInput/index.tsx +5 -0
- package/dist/registry/package/components/OtpInput/styles.css +2 -0
- package/dist/registry/package/components/Overlay/index.tsx +140 -0
- package/dist/registry/package/components/Overlay/styles.css +1 -0
- package/dist/registry/package/components/PageHeader/index.tsx +24 -0
- package/dist/registry/package/components/PageHeader/styles.css +5 -0
- package/dist/registry/package/components/Pagination/index.tsx +155 -0
- package/dist/registry/package/components/Pagination/styles.css +103 -0
- package/dist/registry/package/components/PasswordInput/index.tsx +125 -0
- package/dist/registry/package/components/PasswordInput/styles.css +58 -0
- package/dist/registry/package/components/PermissionGate/index.tsx +3 -0
- package/dist/registry/package/components/PhoneInput/index.tsx +189 -0
- package/dist/registry/package/components/PhoneInput/styles.css +35 -0
- package/dist/registry/package/components/Popover/index.tsx +12 -0
- package/dist/registry/package/components/Popover/styles.css +12 -0
- package/dist/registry/package/components/Progress/index.tsx +149 -0
- package/dist/registry/package/components/Progress/styles.css +101 -0
- package/dist/registry/package/components/QrCode/index.tsx +6 -0
- package/dist/registry/package/components/QrCode/styles.css +5 -0
- package/dist/registry/package/components/RadioGroup/index.tsx +29 -0
- package/dist/registry/package/components/RadioGroup/styles.css +20 -0
- package/dist/registry/package/components/Rating/index.tsx +82 -0
- package/dist/registry/package/components/Rating/styles.css +41 -0
- package/dist/registry/package/components/ReferenceSelect/index.tsx +7 -0
- package/dist/registry/package/components/ReferenceSelect/styles.css +2 -0
- package/dist/registry/package/components/RelativeTime/index.tsx +22 -0
- package/dist/registry/package/components/RelativeTime/styles.css +1 -0
- package/dist/registry/package/components/RequireSession/index.tsx +127 -0
- package/dist/registry/package/components/RequireSession/styles.css +13 -0
- package/dist/registry/package/components/ResizablePanel/index.tsx +6 -0
- package/dist/registry/package/components/ResizablePanel/styles.css +5 -0
- package/dist/registry/package/components/RichTextEditor/index.tsx +5 -0
- package/dist/registry/package/components/RichTextEditor/styles.css +2 -0
- package/dist/registry/package/components/ScrollArea/index.tsx +114 -0
- package/dist/registry/package/components/ScrollArea/styles.css +74 -0
- package/dist/registry/package/components/SearchInput/index.tsx +121 -0
- package/dist/registry/package/components/SearchInput/styles.css +6 -0
- package/dist/registry/package/components/Select/index.tsx +324 -0
- package/dist/registry/package/components/Select/styles.css +58 -0
- package/dist/registry/package/components/Sidebar/index.tsx +232 -0
- package/dist/registry/package/components/Sidebar/styles.css +140 -0
- package/dist/registry/package/components/Skeleton/index.tsx +21 -0
- package/dist/registry/package/components/Skeleton/styles.css +2 -0
- package/dist/registry/package/components/Slider/index.tsx +193 -0
- package/dist/registry/package/components/Slider/styles.css +53 -0
- package/dist/registry/package/components/Spinner/index.tsx +28 -0
- package/dist/registry/package/components/Spinner/styles.css +8 -0
- package/dist/registry/package/components/StatCard/index.tsx +15 -0
- package/dist/registry/package/components/StatCard/styles.css +3 -0
- package/dist/registry/package/components/Statistic/index.tsx +23 -0
- package/dist/registry/package/components/Statistic/styles.css +6 -0
- package/dist/registry/package/components/Steps/index.tsx +108 -0
- package/dist/registry/package/components/Steps/styles.css +138 -0
- package/dist/registry/package/components/Switch/index.tsx +46 -0
- package/dist/registry/package/components/Switch/styles.css +26 -0
- package/dist/registry/package/components/Tabs/index.tsx +44 -0
- package/dist/registry/package/components/Tabs/styles.css +41 -0
- package/dist/registry/package/components/TagsInput/index.tsx +6 -0
- package/dist/registry/package/components/TagsInput/styles.css +2 -0
- package/dist/registry/package/components/Textarea/index.tsx +21 -0
- package/dist/registry/package/components/Textarea/styles.css +7 -0
- package/dist/registry/package/components/TimePicker/index.tsx +76 -0
- package/dist/registry/package/components/TimePicker/styles.css +13 -0
- package/dist/registry/package/components/TimeRangePicker/index.tsx +32 -0
- package/dist/registry/package/components/TimeRangePicker/styles.css +1 -0
- package/dist/registry/package/components/Timeline/index.tsx +8 -0
- package/dist/registry/package/components/Timeline/styles.css +12 -0
- package/dist/registry/package/components/Timer/index.tsx +9 -0
- package/dist/registry/package/components/Toast/index.tsx +426 -0
- package/dist/registry/package/components/Toast/styles.css +175 -0
- package/dist/registry/package/components/Toggle/index.tsx +181 -0
- package/dist/registry/package/components/Toggle/styles.css +124 -0
- package/dist/registry/package/components/Tooltip/index.tsx +163 -0
- package/dist/registry/package/components/Tooltip/styles.css +57 -0
- package/dist/registry/package/components/Tour/index.tsx +8 -0
- package/dist/registry/package/components/Tour/styles.css +6 -0
- package/dist/registry/package/components/Transfer/index.tsx +7 -0
- package/dist/registry/package/components/Transfer/styles.css +6 -0
- package/dist/registry/package/components/Tree/index.tsx +8 -0
- package/dist/registry/package/components/Tree/styles.css +6 -0
- package/dist/registry/package/components/TreeSelect/index.tsx +10 -0
- package/dist/registry/package/components/TreeSelect/styles.css +3 -0
- package/dist/registry/package/components/Typography/index.tsx +27 -0
- package/dist/registry/package/components/Typography/styles.css +16 -0
- package/dist/registry/package/components/UnsavedChangesGuard/index.tsx +4 -0
- package/dist/registry/package/components/WeekPicker/index.tsx +5 -0
- package/dist/registry/package/components/YearPicker/index.tsx +3 -0
- package/dist/registry/package/lib/form-adapter-rhf.tsx +141 -0
- package/dist/registry/registry.json +1780 -0
- package/dist/registry/styles/sia-ui.css +361 -0
- package/dist/source.mjs +93 -0
- 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; }
|