@sia-ui/cli 0.2.1 → 0.3.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 (100) hide show
  1. package/LICENSE +21 -21
  2. package/dist/index.js +2 -0
  3. package/dist/registry/package/components/Alert/index.tsx +7 -0
  4. package/dist/registry/package/components/AmountDisplay/index.tsx +8 -0
  5. package/dist/registry/package/components/AppShell/index.tsx +413 -3
  6. package/dist/registry/package/components/AppShell/styles.css +259 -7
  7. package/dist/registry/package/components/AppShell/tab-bar.tsx +134 -0
  8. package/dist/registry/package/components/AsyncState/index.tsx +7 -0
  9. package/dist/registry/package/components/AuditMeta/index.tsx +8 -0
  10. package/dist/registry/package/components/AuthLayout/index.tsx +7 -0
  11. package/dist/registry/package/components/Autocomplete/index.tsx +7 -0
  12. package/dist/registry/package/components/Avatar/index.tsx +333 -12
  13. package/dist/registry/package/components/Avatar/styles.css +236 -5
  14. package/dist/registry/package/components/Badge/index.tsx +7 -0
  15. package/dist/registry/package/components/Button/index.tsx +7 -0
  16. package/dist/registry/package/components/Calendar/index.tsx +11 -0
  17. package/dist/registry/package/components/Card/index.tsx +8 -0
  18. package/dist/registry/package/components/Checkbox/index.tsx +8 -0
  19. package/dist/registry/package/components/ColorPicker/index.tsx +7 -0
  20. package/dist/registry/package/components/CommandPalette/index.tsx +7 -0
  21. package/dist/registry/package/components/ConfirmDialog/index.tsx +7 -0
  22. package/dist/registry/package/components/Container/index.tsx +7 -0
  23. package/dist/registry/package/components/Countdown/index.tsx +7 -0
  24. package/dist/registry/package/components/CrudPage/dialogs.tsx +230 -0
  25. package/dist/registry/package/components/CrudPage/index.tsx +546 -5
  26. package/dist/registry/package/components/CrudPage/styles.css +25 -1
  27. package/dist/registry/package/components/CurrencyInput/index.tsx +7 -0
  28. package/dist/registry/package/components/DataTable/index.tsx +548 -200
  29. package/dist/registry/package/components/DataTable/query.ts +68 -0
  30. package/dist/registry/package/components/DataTable/row-actions.tsx +158 -0
  31. package/dist/registry/package/components/DataTable/styles.css +285 -73
  32. package/dist/registry/package/components/DataTable/types.ts +194 -0
  33. package/dist/registry/package/components/DataTable/values.ts +28 -0
  34. package/dist/registry/package/components/DatePicker/index.tsx +7 -0
  35. package/dist/registry/package/components/DateRangeFilter/index.tsx +7 -0
  36. package/dist/registry/package/components/DateRangePicker/index.tsx +7 -0
  37. package/dist/registry/package/components/DateTimePicker/index.tsx +7 -0
  38. package/dist/registry/package/components/Descriptions/index.tsx +7 -0
  39. package/dist/registry/package/components/Divider/index.tsx +7 -0
  40. package/dist/registry/package/components/Drawer/index.tsx +8 -0
  41. package/dist/registry/package/components/DurationDisplay/index.tsx +6 -0
  42. package/dist/registry/package/components/EmptyState/index.tsx +7 -0
  43. package/dist/registry/package/components/EntityMeta/index.tsx +7 -0
  44. package/dist/registry/package/components/ErrorState/index.tsx +8 -0
  45. package/dist/registry/package/components/EventCalendar/index.tsx +7 -0
  46. package/dist/registry/package/components/Field/index.tsx +12 -0
  47. package/dist/registry/package/components/FileUpload/index.tsx +7 -0
  48. package/dist/registry/package/components/FiltersBar/index.tsx +7 -0
  49. package/dist/registry/package/components/HoverCard/index.tsx +14 -37
  50. package/dist/registry/package/components/IconButton/index.tsx +8 -0
  51. package/dist/registry/package/components/Icons/index.tsx +15 -0
  52. package/dist/registry/package/components/ImageUpload/index.tsx +7 -0
  53. package/dist/registry/package/components/Input/index.tsx +7 -0
  54. package/dist/registry/package/components/JsonEditor/index.tsx +7 -0
  55. package/dist/registry/package/components/KpiGrid/index.tsx +7 -0
  56. package/dist/registry/package/components/Label/index.tsx +7 -0
  57. package/dist/registry/package/components/MarkdownEditor/index.tsx +7 -0
  58. package/dist/registry/package/components/MiniCalendar/index.tsx +7 -0
  59. package/dist/registry/package/components/Modal/index.tsx +8 -0
  60. package/dist/registry/package/components/MonthPicker/index.tsx +7 -0
  61. package/dist/registry/package/components/MultiSelect/index.tsx +7 -0
  62. package/dist/registry/package/components/OtpInput/index.tsx +7 -0
  63. package/dist/registry/package/components/Overlay/index.tsx +34 -24
  64. package/dist/registry/package/components/PageHeader/index.tsx +7 -0
  65. package/dist/registry/package/components/PermissionGate/index.tsx +6 -0
  66. package/dist/registry/package/components/Popover/index.tsx +7 -0
  67. package/dist/registry/package/components/QrCode/index.tsx +7 -0
  68. package/dist/registry/package/components/RadioGroup/index.tsx +7 -0
  69. package/dist/registry/package/components/Rating/index.tsx +6 -0
  70. package/dist/registry/package/components/ReferenceSelect/index.tsx +7 -0
  71. package/dist/registry/package/components/RelativeTime/index.tsx +6 -0
  72. package/dist/registry/package/components/ResizablePanel/index.tsx +7 -0
  73. package/dist/registry/package/components/Resource/index.tsx +514 -0
  74. package/dist/registry/package/components/RichTextEditor/index.tsx +7 -0
  75. package/dist/registry/package/components/SearchInput/index.tsx +8 -0
  76. package/dist/registry/package/components/Select/index.tsx +11 -0
  77. package/dist/registry/package/components/Sidebar/index.tsx +489 -113
  78. package/dist/registry/package/components/Sidebar/styles.css +96 -4
  79. package/dist/registry/package/components/Skeleton/index.tsx +7 -0
  80. package/dist/registry/package/components/Spinner/index.tsx +7 -0
  81. package/dist/registry/package/components/StatCard/index.tsx +7 -0
  82. package/dist/registry/package/components/Statistic/index.tsx +7 -0
  83. package/dist/registry/package/components/Switch/index.tsx +7 -0
  84. package/dist/registry/package/components/Tabs/index.tsx +7 -0
  85. package/dist/registry/package/components/TagsInput/index.tsx +7 -0
  86. package/dist/registry/package/components/Textarea/index.tsx +7 -0
  87. package/dist/registry/package/components/TimePicker/index.tsx +7 -0
  88. package/dist/registry/package/components/TimeRangePicker/index.tsx +6 -0
  89. package/dist/registry/package/components/Timeline/index.tsx +7 -0
  90. package/dist/registry/package/components/Timer/index.tsx +7 -0
  91. package/dist/registry/package/components/Tour/index.tsx +7 -0
  92. package/dist/registry/package/components/Transfer/index.tsx +7 -0
  93. package/dist/registry/package/components/Tree/index.tsx +7 -0
  94. package/dist/registry/package/components/TreeSelect/index.tsx +7 -0
  95. package/dist/registry/package/components/Typography/index.tsx +8 -0
  96. package/dist/registry/package/components/UnsavedChangesGuard/index.tsx +8 -0
  97. package/dist/registry/package/components/WeekPicker/index.tsx +7 -0
  98. package/dist/registry/package/components/YearPicker/index.tsx +7 -0
  99. package/dist/registry/registry.json +69 -14
  100. package/package.json +14 -14
@@ -1,23 +1,344 @@
1
- import { useState, type ImgHTMLAttributes } from "react";
1
+ import {
2
+ createContext,
3
+ useContext,
4
+ useMemo,
5
+ useState,
6
+ type HTMLAttributes,
7
+ type ImgHTMLAttributes,
8
+ type ReactNode,
9
+ } from "react";
2
10
  import { cn } from "@sia-ui/utils";
3
11
  import "./styles.css";
4
12
 
5
- export interface AvatarProps extends Omit<ImgHTMLAttributes<HTMLImageElement>, "children"> {
6
- name: string;
7
- size?: "sm" | "md" | "lg";
8
- fallback?: string;
13
+ export type AvatarSize = "xs" | "sm" | "md" | "lg" | "xl";
14
+ export type AvatarShape = "circle" | "rounded" | "square";
15
+ export type AvatarVariant = "soft" | "solid" | "outline";
16
+
17
+ /**
18
+ * La présence d'une personne.
19
+ *
20
+ * Quatre états et pas davantage : au-delà, la pastille devient un code que
21
+ * personne ne retient, et il faut une légende pour la lire.
22
+ */
23
+ export type AvatarStatus = "online" | "offline" | "busy" | "away";
24
+
25
+ const LIBELLES_STATUT: Record<AvatarStatus, string> = {
26
+ online: "en ligne",
27
+ offline: "hors ligne",
28
+ busy: "occupé",
29
+ away: "absent",
30
+ };
31
+
32
+ /**
33
+ * Les teintes dérivées du nom.
34
+ *
35
+ * Deux personnes aux mêmes initiales doivent se distinguer : dans une liste
36
+ * de participants, six pastilles identiques ne valent pas mieux qu'aucune.
37
+ * La couleur est donc déterministe — la même personne garde la sienne d'un
38
+ * écran à l'autre, d'une session à l'autre.
39
+ */
40
+ const TEINTES = [
41
+ "primary",
42
+ "info",
43
+ "success",
44
+ "warning",
45
+ "danger",
46
+ "secondary",
47
+ ] as const;
48
+
49
+ export type AvatarTint = (typeof TEINTES)[number];
50
+
51
+ /**
52
+ * Une empreinte stable, calculée sur le nom.
53
+ *
54
+ * Volontairement minuscule : cette fonction tourne pour chaque avatar d'une
55
+ * liste qui peut en compter des centaines. Un hachage cryptographique y
56
+ * coûterait mille fois plus pour un résultat visuellement identique.
57
+ */
58
+ function empreinte(texte: string): number {
59
+ let h = 0;
60
+ for (let i = 0; i < texte.length; i += 1) {
61
+ h = (h * 31 + texte.charCodeAt(i)) | 0;
62
+ }
63
+ return Math.abs(h);
64
+ }
65
+
66
+ export function tintForName(name: string): AvatarTint {
67
+ return TEINTES[empreinte(name) % TEINTES.length]!;
68
+ }
69
+
70
+ /**
71
+ * Les initiales d'un nom.
72
+ *
73
+ * Première et dernière partie plutôt que les deux premières : « Jean-Baptiste
74
+ * Kouassi Mbella » se lit « JM », pas « JK ». Les particules sont écartées,
75
+ * sinon « Marie de la Tour » donnerait « MD ».
76
+ */
77
+ const PARTICULES = new Set([
78
+ "de", "du", "des", "la", "le", "les", "van", "von", "da", "di", "el", "al",
79
+ ]);
80
+
81
+ export function initialsOf(name: string, count = 2): string {
82
+ const parts = name
83
+ .trim()
84
+ .split(/[\s'-]+/)
85
+ .filter((p) => p.length > 0 && !PARTICULES.has(p.toLowerCase()));
86
+
87
+ if (parts.length === 0) return "";
88
+ if (parts.length === 1 || count === 1) {
89
+ return (parts[0]!.slice(0, count) || "").toUpperCase();
90
+ }
91
+
92
+ return (
93
+ (parts[0]![0] ?? "") + (parts[parts.length - 1]![0] ?? "")
94
+ ).toUpperCase();
9
95
  }
10
96
 
11
- function getInitials(name: string) {
12
- return name.trim().split(/\s+/).slice(0, 2).map((part) => part[0]?.toUpperCase() ?? "").join("");
97
+ interface AvatarGroupContextValue {
98
+ size: AvatarSize;
99
+ shape: AvatarShape;
100
+ variant: AvatarVariant;
13
101
  }
14
102
 
15
- export function Avatar({ name, size = "md", fallback, className, src, alt, onError, ...props }: AvatarProps) {
16
- const [failed, setFailed] = useState(false);
103
+ const AvatarGroupContext = createContext<AvatarGroupContextValue | null>(null);
104
+
105
+ export interface AvatarProps
106
+ extends Omit<ImgHTMLAttributes<HTMLImageElement>, "children" | "width" | "height"> {
107
+ /** Le nom de la personne : il sert au repli, à la couleur et à l'annonce. */
108
+ name: string;
109
+
110
+ size?: AvatarSize;
111
+ shape?: AvatarShape;
112
+ variant?: AvatarVariant;
113
+
114
+ /** Force la teinte au lieu de la dériver du nom. */
115
+ tint?: AvatarTint;
116
+
117
+ /** Remplace les initiales — une icône, un emoji, deux lettres à soi. */
118
+ fallback?: ReactNode;
119
+
120
+ /** Nombre d'initiales quand le repli est automatique. */
121
+ initials?: 1 | 2;
122
+
123
+ status?: AvatarStatus;
124
+ /** Le libellé du statut, pour les lecteurs d'écran. */
125
+ statusLabel?: string;
126
+
127
+ /** Un liseré qui détache l'avatar de son fond. */
128
+ ring?: boolean;
129
+
130
+ /** Rend l'avatar actionnable — un menu de compte, par exemple. */
131
+ onPress?: () => void;
132
+ }
133
+
134
+ /**
135
+ * Le portrait d'une personne, ou ce qui le remplace.
136
+ *
137
+ * Une image met du temps à arriver, échoue parfois, et manque souvent. Les
138
+ * trois cas sont traités sans que la mise en page bouge : l'espace est réservé
139
+ * d'emblée, le repli est peint dessous, et l'image se fond par-dessus quand
140
+ * elle arrive.
141
+ */
142
+ export function Avatar({
143
+ name,
144
+ size,
145
+ shape,
146
+ variant,
147
+ tint,
148
+ fallback,
149
+ initials = 2,
150
+ status,
151
+ statusLabel,
152
+ ring = false,
153
+ onPress,
154
+ className,
155
+ src,
156
+ alt,
157
+ onError,
158
+ onLoad,
159
+ style,
160
+ ...props
161
+ }: AvatarProps) {
162
+ const groupe = useContext(AvatarGroupContext);
163
+ const [etatImage, setEtatImage] = useState<"attente" | "prete" | "echouee">(
164
+ src ? "attente" : "echouee",
165
+ );
166
+
167
+ const resolvedSize = size ?? groupe?.size ?? "md";
168
+ const resolvedShape = shape ?? groupe?.shape ?? "circle";
169
+ const resolvedVariant = variant ?? groupe?.variant ?? "soft";
170
+
171
+ // Le nom ne change pas à chaque rendu, le hachage non plus : dans une liste
172
+ // de cent participants, c'est cent calculs évités par frappe au clavier.
173
+ const teinte = useMemo(() => tint ?? tintForName(name), [name, tint]);
174
+ const lettres = useMemo(
175
+ () => (fallback === undefined ? initialsOf(name, initials) : null),
176
+ [fallback, initials, name],
177
+ );
178
+
179
+ const classe = cn(
180
+ "sia-avatar",
181
+ `sia-avatar--${resolvedSize}`,
182
+ `sia-avatar--${resolvedShape}`,
183
+ `sia-avatar--${resolvedVariant}`,
184
+ `sia-avatar--tint-${teinte}`,
185
+ ring && "sia-avatar--ring",
186
+ etatImage === "attente" && "sia-avatar--loading",
187
+ onPress && "sia-avatar--pressable",
188
+ className,
189
+ );
190
+
191
+ const contenu = (
192
+ <>
193
+ {/* Le repli est toujours peint, sous l'image. C'est lui qui tient la
194
+ place pendant le chargement, et qui reste si l'image échoue —
195
+ aucune bascule, donc aucun saut. */}
196
+ <span className="sia-avatar__fallback" aria-hidden="true">
197
+ {fallback ?? lettres}
198
+ </span>
199
+
200
+ {src && etatImage !== "echouee" && (
201
+ <img
202
+ {...props}
203
+ className="sia-avatar__image"
204
+ src={src}
205
+ alt={alt ?? ""}
206
+ loading="lazy"
207
+ decoding="async"
208
+ onLoad={(event) => {
209
+ setEtatImage("prete");
210
+ onLoad?.(event);
211
+ }}
212
+ onError={(event) => {
213
+ setEtatImage("echouee");
214
+ onError?.(event);
215
+ }}
216
+ />
217
+ )}
218
+
219
+ {status && (
220
+ <span
221
+ className={cn("sia-avatar__status", `sia-avatar__status--${status}`)}
222
+ aria-hidden="true"
223
+ />
224
+ )}
225
+
226
+ {/* Le nom n'est jamais dans l'image : un `alt` vide et un texte masqué
227
+ annoncent la personne une seule fois, statut compris. */}
228
+ <span className="sia-visually-hidden">
229
+ {name}
230
+ {status ? ` — ${statusLabel ?? LIBELLES_STATUT[status]}` : ""}
231
+ </span>
232
+ </>
233
+ );
234
+
235
+ if (onPress) {
236
+ return (
237
+ <button
238
+ type="button"
239
+ className={classe}
240
+ title={name}
241
+ onClick={onPress}
242
+ {...(style ? { style } : {})}
243
+ >
244
+ {contenu}
245
+ </button>
246
+ );
247
+ }
248
+
17
249
  return (
18
- <span className={cn("sia-avatar", `sia-avatar--${size}`, className)} title={name}>
19
- {src && !failed ? <img src={src} alt={alt ?? name} onError={(event) => { setFailed(true); onError?.(event); }} {...props} /> : <span aria-hidden="true">{fallback ?? getInitials(name)}</span>}
20
- {!src || failed ? <span className="sia-visually-hidden">{name}</span> : null}
250
+ <span className={classe} title={name} {...(style ? { style } : {})}>
251
+ {contenu}
21
252
  </span>
22
253
  );
23
254
  }
255
+
256
+ export interface AvatarGroupProps extends HTMLAttributes<HTMLDivElement> {
257
+ children: ReactNode;
258
+
259
+ /**
260
+ * Au-delà, les suivants sont résumés par un compteur.
261
+ *
262
+ * Sans limite, une réunion de quarante personnes déborde de sa ligne et
263
+ * n'apprend plus rien : au-delà de cinq visages, on ne lit plus des gens,
264
+ * on lit une foule.
265
+ */
266
+ max?: number;
267
+
268
+ size?: AvatarSize;
269
+ shape?: AvatarShape;
270
+ variant?: AvatarVariant;
271
+
272
+ /** Le chevauchement, de 0 (côte à côte) à 1 (fortement empilés). */
273
+ overlap?: number;
274
+
275
+ /** Écarte les avatars au survol, pour les distinguer. */
276
+ spreadOnHover?: boolean;
277
+
278
+ /** Ce qu'affiche le compteur. Par défaut `+N`. */
279
+ renderOverflow?: (count: number) => ReactNode;
280
+ }
281
+
282
+ /**
283
+ * Plusieurs personnes, sur une ligne.
284
+ *
285
+ * Les avatars se chevauchent — c'est ce qui dit « un groupe » plutôt qu'« une
286
+ * liste ». Chacun porte un liseré de la couleur du fond, sans quoi la pile
287
+ * devient une bouillie dès que deux teintes se ressemblent.
288
+ */
289
+ export function AvatarGroup({
290
+ children,
291
+ max,
292
+ size = "md",
293
+ shape = "circle",
294
+ variant = "soft",
295
+ overlap = 0.25,
296
+ spreadOnHover = false,
297
+ renderOverflow,
298
+ className,
299
+ style,
300
+ ...props
301
+ }: AvatarGroupProps) {
302
+ const tous = Array.isArray(children) ? children : [children];
303
+ const visibles = max === undefined ? tous : tous.slice(0, max);
304
+ const reste = tous.length - visibles.length;
305
+
306
+ const contexte = useMemo(
307
+ () => ({ size, shape, variant }),
308
+ [shape, size, variant],
309
+ );
310
+
311
+ return (
312
+ <AvatarGroupContext.Provider value={contexte}>
313
+ <div
314
+ className={cn(
315
+ "sia-avatar-group",
316
+ `sia-avatar-group--${size}`,
317
+ spreadOnHover && "sia-avatar-group--spread",
318
+ className,
319
+ )}
320
+ style={{
321
+ ...style,
322
+ "--sia-avatar-overlap": String(Math.min(Math.max(overlap, 0), 1)),
323
+ } as Record<string, string>}
324
+ {...props}
325
+ >
326
+ {visibles}
327
+ {reste > 0 && (
328
+ <span
329
+ className={cn(
330
+ "sia-avatar",
331
+ `sia-avatar--${size}`,
332
+ `sia-avatar--${shape}`,
333
+ "sia-avatar--overflow",
334
+ )}
335
+ >
336
+ <span className="sia-avatar__fallback">
337
+ {renderOverflow ? renderOverflow(reste) : `+${reste}`}
338
+ </span>
339
+ </span>
340
+ )}
341
+ </div>
342
+ </AvatarGroupContext.Provider>
343
+ );
344
+ }
@@ -1,5 +1,236 @@
1
- .sia-avatar { display:inline-grid; place-items:center; overflow:hidden; border-radius:50%; color:var(--sia-primary-foreground); background:var(--sia-primary); font-weight:750; line-height:1; }
2
- .sia-avatar img { width:100%; height:100%; object-fit:cover; }
3
- .sia-avatar--sm { width:2rem; height:2rem; font-size:.72rem; }
4
- .sia-avatar--md { width:2.75rem; height:2.75rem; font-size:.85rem; }
5
- .sia-avatar--lg { width:4rem; height:4rem; font-size:1.1rem; }
1
+ /**
2
+ * L'avatar.
3
+ *
4
+ * Tout repose sur une teinte, `--sia-avatar-tint`, posée par une classe et
5
+ * consommée par les trois variantes. Changer de variante ne redéfinit donc
6
+ * jamais la couleur : c'est ce qui permet à une teinte dérivée du nom de
7
+ * survivre à `soft`, `solid` et `outline` sans triplement des règles.
8
+ */
9
+
10
+ .sia-avatar {
11
+ --sia-avatar-tint: var(--sia-primary);
12
+ --sia-avatar-size: 2.5rem;
13
+ --sia-avatar-radius: 50%;
14
+
15
+ position: relative;
16
+ display: inline-grid;
17
+ place-items: center;
18
+ flex: none;
19
+ width: var(--sia-avatar-size);
20
+ height: var(--sia-avatar-size);
21
+ border: 0;
22
+ border-radius: var(--sia-avatar-radius);
23
+ padding: 0;
24
+ overflow: hidden;
25
+ font-family: inherit;
26
+ font-weight: 600;
27
+ /* Les initiales ne doivent jamais être sélectionnées par un double-clic
28
+ destiné à ouvrir une fiche. */
29
+ user-select: none;
30
+ line-height: 1;
31
+ }
32
+
33
+ /* ── Le repli ───────────────────────────────────────────────────────────── */
34
+
35
+ .sia-avatar__fallback {
36
+ display: grid;
37
+ place-items: center;
38
+ width: 100%;
39
+ height: 100%;
40
+ /* La même courbure que le parent. Sans elle, l'ombre interne de la
41
+ variante `outline` dessine un cadre carré que le masque circulaire
42
+ rogne : il n'en restait que deux fragments verticaux. */
43
+ border-radius: inherit;
44
+ /* Une lettre ne se centre pas d'elle-même : la casse haute laisse un vide
45
+ sous la ligne de base, et l'initiale paraît trop haute. */
46
+ padding-top: .04em;
47
+ font-size: calc(var(--sia-avatar-size) * .38);
48
+ letter-spacing: .01em;
49
+ }
50
+
51
+ /* ── L'image ────────────────────────────────────────────────────────────── */
52
+
53
+ .sia-avatar__image {
54
+ position: absolute;
55
+ inset: 0;
56
+ width: 100%;
57
+ height: 100%;
58
+ object-fit: cover;
59
+ /* Elle arrive par-dessus le repli, qui tenait la place : rien ne saute,
60
+ rien ne clignote. */
61
+ opacity: 0;
62
+ animation: sia-avatar-image var(--sia-duration-base) var(--sia-easing-standard) forwards;
63
+ }
64
+ @keyframes sia-avatar-image {
65
+ from { opacity: 0; transform: scale(1.04); }
66
+ to { opacity: 1; transform: none; }
67
+ }
68
+
69
+ /* Pendant l'attente, un souffle très léger sur le repli — assez pour dire
70
+ « ça arrive », pas assez pour attirer l'œil dans une liste de cinquante. */
71
+ .sia-avatar--loading .sia-avatar__fallback {
72
+ animation: sia-avatar-attente 1.6s ease-in-out infinite;
73
+ }
74
+ @keyframes sia-avatar-attente {
75
+ 50% { opacity: .55; }
76
+ }
77
+
78
+ /* ── Les variantes ──────────────────────────────────────────────────────── */
79
+
80
+ /* `soft` : la teinte à faible saturation, posée en dégradé très court. C'est
81
+ ce qui donne la matière sans crier — un aplat pur paraît plat, un dégradé
82
+ marqué paraît daté. */
83
+ .sia-avatar--soft .sia-avatar__fallback {
84
+ background: linear-gradient(
85
+ 145deg,
86
+ color-mix(in srgb, var(--sia-avatar-tint) 20%, var(--sia-background)),
87
+ color-mix(in srgb, var(--sia-avatar-tint) 11%, var(--sia-background))
88
+ );
89
+ color: color-mix(in srgb, var(--sia-avatar-tint) 82%, var(--sia-foreground));
90
+ }
91
+
92
+ .sia-avatar--solid .sia-avatar__fallback {
93
+ background: linear-gradient(
94
+ 145deg,
95
+ color-mix(in srgb, var(--sia-avatar-tint) 88%, white),
96
+ var(--sia-avatar-tint)
97
+ );
98
+ color: var(--sia-primary-foreground, #fff);
99
+ }
100
+
101
+ .sia-avatar--outline .sia-avatar__fallback {
102
+ box-shadow: inset 0 0 0 1.5px color-mix(in srgb, var(--sia-avatar-tint) 45%, transparent);
103
+ background: var(--sia-background);
104
+ color: color-mix(in srgb, var(--sia-avatar-tint) 85%, var(--sia-foreground));
105
+ }
106
+
107
+ /* Le liseré qui détache du fond — indispensable dans une pile. */
108
+ .sia-avatar--ring {
109
+ box-shadow:
110
+ 0 0 0 2px var(--sia-background),
111
+ 0 0 0 3px color-mix(in srgb, var(--sia-avatar-tint) 30%, transparent);
112
+ }
113
+
114
+ /* ── Les teintes ────────────────────────────────────────────────────────── */
115
+
116
+ .sia-avatar--tint-primary { --sia-avatar-tint: var(--sia-primary); }
117
+ .sia-avatar--tint-secondary { --sia-avatar-tint: var(--sia-secondary); }
118
+ .sia-avatar--tint-info { --sia-avatar-tint: var(--sia-info); }
119
+ .sia-avatar--tint-success { --sia-avatar-tint: var(--sia-success); }
120
+ .sia-avatar--tint-warning { --sia-avatar-tint: var(--sia-warning); }
121
+ .sia-avatar--tint-danger { --sia-avatar-tint: var(--sia-danger); }
122
+
123
+ /* ── Les formes ─────────────────────────────────────────────────────────── */
124
+
125
+ .sia-avatar--circle { --sia-avatar-radius: 50%; }
126
+ /* Un rayon proportionnel à la taille : un carré arrondi de 2 rem et un de
127
+ 5 rem n'ont pas la même courbure apparente à rayon fixe. */
128
+ .sia-avatar--rounded { --sia-avatar-radius: calc(var(--sia-avatar-size) * .28); }
129
+ .sia-avatar--square { --sia-avatar-radius: calc(var(--sia-avatar-size) * .08); }
130
+
131
+ /* ── Les tailles ────────────────────────────────────────────────────────── */
132
+
133
+ .sia-avatar--xs { --sia-avatar-size: 1.5rem; }
134
+ .sia-avatar--sm { --sia-avatar-size: 2rem; }
135
+ .sia-avatar--md { --sia-avatar-size: 2.5rem; }
136
+ .sia-avatar--lg { --sia-avatar-size: 3.25rem; }
137
+ .sia-avatar--xl { --sia-avatar-size: 4.5rem; }
138
+
139
+ /* ── Le statut ──────────────────────────────────────────────────────────── */
140
+
141
+ .sia-avatar__status {
142
+ position: absolute;
143
+ right: 0;
144
+ bottom: 0;
145
+ width: calc(var(--sia-avatar-size) * .26);
146
+ height: calc(var(--sia-avatar-size) * .26);
147
+ /* Un minimum absolu : sous six pixels, la pastille n'est plus lisible,
148
+ quelle que soit la taille de l'avatar. */
149
+ min-width: .4rem;
150
+ min-height: .4rem;
151
+ border-radius: 50%;
152
+ box-shadow: 0 0 0 2px var(--sia-background);
153
+ }
154
+
155
+ /* Décalée vers l'intérieur sur un cercle : posée au coin d'une boîte carrée,
156
+ elle sortirait du disque. */
157
+ .sia-avatar--circle .sia-avatar__status {
158
+ right: calc(var(--sia-avatar-size) * .03);
159
+ bottom: calc(var(--sia-avatar-size) * .03);
160
+ }
161
+
162
+ .sia-avatar__status--online { background: var(--sia-success); }
163
+ .sia-avatar__status--busy { background: var(--sia-danger); }
164
+ .sia-avatar__status--away { background: var(--sia-warning); }
165
+ .sia-avatar__status--offline {
166
+ background: var(--sia-background);
167
+ box-shadow: 0 0 0 2px var(--sia-background), inset 0 0 0 2px var(--sia-muted);
168
+ }
169
+
170
+ /* ── Actionnable ────────────────────────────────────────────────────────── */
171
+
172
+ .sia-avatar--pressable {
173
+ cursor: pointer;
174
+ transition:
175
+ transform var(--sia-duration-fast) var(--sia-easing-standard),
176
+ box-shadow var(--sia-duration-fast) var(--sia-easing-standard);
177
+ }
178
+ .sia-avatar--pressable:hover { transform: translateY(-1px); }
179
+ .sia-avatar--pressable:active { transform: translateY(0) scale(.96); }
180
+ .sia-avatar--pressable:focus-visible {
181
+ outline: 2px solid var(--sia-primary);
182
+ outline-offset: 2px;
183
+ }
184
+
185
+ /* ── Le groupe ──────────────────────────────────────────────────────────── */
186
+
187
+ .sia-avatar-group {
188
+ --sia-avatar-overlap: .25;
189
+ display: inline-flex;
190
+ align-items: center;
191
+ /* Une marge négative dont l'amplitude suit la taille : un chevauchement
192
+ fixe disparaît sur les grands avatars et les écrase sur les petits. */
193
+ padding-left: calc(var(--sia-avatar-size, 2.5rem) * var(--sia-avatar-overlap));
194
+ }
195
+
196
+ .sia-avatar-group > .sia-avatar {
197
+ margin-left: calc(var(--sia-avatar-size) * var(--sia-avatar-overlap) * -1);
198
+ /* Le liseré de la couleur du fond : sans lui, deux teintes voisines se
199
+ fondent l'une dans l'autre et la pile devient une tache. Son épaisseur
200
+ suit la taille — deux pixels disparaissent sur un avatar de 4,5 rem. */
201
+ box-shadow: 0 0 0 calc(var(--sia-avatar-size) * .055) var(--sia-background);
202
+ transition: transform var(--sia-duration-base) var(--sia-easing-spring);
203
+ }
204
+
205
+ /* Chaque avatar recouvre le précédent — l'ordre de peinture naturel, sans
206
+ `z-index`.
207
+
208
+ L'inverse semblait plus logique, et c'est un piège : en mettant le premier
209
+ devant, chaque suivant est masqué **à gauche**, c'est-à-dire précisément là
210
+ où commence la lecture. « AK » devenait « K ». Recouvert à droite, chacun
211
+ garde au moins sa première initiale, et le compteur final reste entier. */
212
+
213
+ /* Au survol, la pile s'ouvre pour qu'on puisse viser quelqu'un. */
214
+ .sia-avatar-group--spread:hover > .sia-avatar {
215
+ margin-left: calc(var(--sia-avatar-size) * var(--sia-avatar-overlap) * -.25);
216
+ }
217
+ .sia-avatar-group--spread > .sia-avatar {
218
+ transition:
219
+ margin-left var(--sia-duration-base) var(--sia-easing-spring),
220
+ transform var(--sia-duration-base) var(--sia-easing-spring);
221
+ }
222
+ .sia-avatar-group > .sia-avatar:hover { transform: translateY(-2px); z-index: 10; }
223
+
224
+ .sia-avatar--overflow .sia-avatar__fallback {
225
+ background: var(--sia-surface);
226
+ color: var(--sia-muted);
227
+ font-size: calc(var(--sia-avatar-size) * .32);
228
+ font-variant-numeric: tabular-nums;
229
+ }
230
+
231
+ @media (prefers-reduced-motion: reduce) {
232
+ .sia-avatar__image,
233
+ .sia-avatar--loading .sia-avatar__fallback { animation: none; opacity: 1; }
234
+ .sia-avatar--pressable:hover,
235
+ .sia-avatar-group > .sia-avatar:hover { transform: none; }
236
+ }
@@ -15,6 +15,13 @@ export interface BadgeProps extends HTMLAttributes<HTMLSpanElement> {
15
15
  spinnerProps?: Omit<SpinnerProps, "label">;
16
16
  }
17
17
 
18
+ /**
19
+ * Une étiquette d'état, courte et dense.
20
+ *
21
+ * Elle porte un ton, jamais une action : un badge cliquable se confond avec
22
+ * un bouton petit, et l'on découvre qu'il ne l'était pas en cliquant. Ce
23
+ * qui agit est un bouton.
24
+ */
18
25
  export function Badge({
19
26
  tone: toneProp,
20
27
  variant: variantProp,
@@ -26,6 +26,13 @@ export interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
26
26
  rightIcon?: ReactNode;
27
27
  }
28
28
 
29
+ /**
30
+ * L'action, sous toutes ses formes.
31
+ *
32
+ * Le chargement y est un état et non un remplacement : le bouton garde sa
33
+ * taille et son libellé pendant l'appel. Un bouton qui devient un rond
34
+ * tournant fait bouger la mise en page, et le clic suivant tombe à côté.
35
+ */
29
36
  export const Button = forwardRef<HTMLButtonElement, ButtonProps>(
30
37
  (
31
38
  {
@@ -59,6 +59,17 @@ function weekNumber(value: string) {
59
59
  return Math.ceil((((target.getTime() - yearStart.getTime()) / 86400000) + 1) / 7);
60
60
  }
61
61
 
62
+ /**
63
+ * Un mois de dates, sélectionnables une à une ou par plage.
64
+ *
65
+ * Trois panneaux — jours, mois, années — plutôt qu'un défilement infini :
66
+ * choisir une date de naissance demande de remonter trente ans, et trente
67
+ * ans de défilement ne sont pas une navigation.
68
+ *
69
+ * Les dates circulent en chaînes `AAAA-MM-JJ`, jamais en objets `Date` : un
70
+ * `Date` porte une heure et un fuseau dont une date de facture n'a que
71
+ * faire, et qui la décalent d'un jour une fois sur deux.
72
+ */
62
73
  export function Calendar({
63
74
  value,
64
75
  defaultValue,
@@ -2,6 +2,14 @@ import { type HTMLAttributes } from "react";
2
2
  import { cn } from "@sia-ui/utils";
3
3
  import "./styles.css";
4
4
 
5
+ /**
6
+ * Une surface qui regroupe ce qui va ensemble.
7
+ *
8
+ * Quatre parties séparées — `Card`, `CardHeader`, `CardBody`, `CardFooter` —
9
+ * plutôt que des props `title` et `footer` : dès qu'un en-tête doit porter
10
+ * deux boutons et une pastille, une prop ne suffit plus, et l'on se retrouve
11
+ * à passer du JSX à travers une chaîne.
12
+ */
5
13
  export function Card({ className, ...props }: HTMLAttributes<HTMLDivElement>) { return <div className={cn("sia-card", className)} {...props} />; }
6
14
  export function CardHeader({ className, ...props }: HTMLAttributes<HTMLDivElement>) { return <div className={cn("sia-card__header", className)} {...props} />; }
7
15
  export function CardBody({ className, ...props }: HTMLAttributes<HTMLDivElement>) { return <div className={cn("sia-card__body", className)} {...props} />; }
@@ -38,6 +38,14 @@ export interface CheckboxGroupProps {
38
38
  children: ReactNode;
39
39
  }
40
40
 
41
+ /**
42
+ * Une case, ou un groupe de cases.
43
+ *
44
+ * `CheckboxGroup` gère la liste des valeurs cochées; la case seule gère la
45
+ * sienne. L'état indéterminé est une troisième valeur, pas un entre-deux
46
+ * visuel : il dit « certains enfants sont cochés », ce qu'aucun booléen ne
47
+ * sait exprimer.
48
+ */
41
49
  export function CheckboxGroup({
42
50
  value,
43
51
  defaultValue = [],
@@ -2,5 +2,12 @@ import { forwardRef, useState, type InputHTMLAttributes } from "react";
2
2
  import { cn } from "@sia-ui/utils";
3
3
  import "./styles.css";
4
4
  export interface ColorPickerProps extends Omit<InputHTMLAttributes<HTMLInputElement>, "type" | "onChange"> { onValueChange?: (value: string) => void; showValue?: boolean; }
5
+ /**
6
+ * Le choix d'une couleur.
7
+ *
8
+ * La valeur hexadécimale est affichée à côté du nuancier : deux bleus
9
+ * voisins ne se distinguent pas à l'œil sur un écran mal calibré, alors que
10
+ * leurs codes, si.
11
+ */
5
12
  export const ColorPicker = forwardRef<HTMLInputElement, ColorPickerProps>(({ className, onValueChange, showValue = true, value, defaultValue = "#2563eb", ...props }, ref) => { const [internal, setInternal] = useState(String(defaultValue)); const current = value === undefined ? internal : String(value); return <span className={cn("sia-color-picker", className)}><input {...props} ref={ref} type="color" value={current} onChange={(event) => { if (value === undefined) setInternal(event.target.value); onValueChange?.(event.target.value); }} />{showValue && <code>{current}</code>}</span>; });
6
13
  ColorPicker.displayName = "ColorPicker";