@sia-ui/cli 0.7.1 → 0.8.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 (99) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/README.md +6 -0
  3. package/dist/registry/package/components/AccountMenu/index.tsx +107 -0
  4. package/dist/registry/package/components/AccountMenu/styles.css +30 -0
  5. package/dist/registry/package/components/ActivityLog/index.tsx +96 -25
  6. package/dist/registry/package/components/AmountDisplay/index.tsx +5 -2
  7. package/dist/registry/package/components/AppShell/index.tsx +44 -8
  8. package/dist/registry/package/components/AppShell/tab-bar.tsx +6 -3
  9. package/dist/registry/package/components/AsyncState/index.tsx +29 -7
  10. package/dist/registry/package/components/AuditMeta/index.tsx +12 -2
  11. package/dist/registry/package/components/Avatar/index.tsx +4 -9
  12. package/dist/registry/package/components/Badge/index.tsx +3 -2
  13. package/dist/registry/package/components/Badge/styles.css +4 -4
  14. package/dist/registry/package/components/BrandIcon/index.tsx +100 -0
  15. package/dist/registry/package/components/BrandIcon/styles.css +31 -0
  16. package/dist/registry/package/components/Breadcrumb/index.tsx +14 -2
  17. package/dist/registry/package/components/Button/index.tsx +3 -2
  18. package/dist/registry/package/components/Button/styles.css +4 -4
  19. package/dist/registry/package/components/Calendar/index.tsx +7 -3
  20. package/dist/registry/package/components/Card/index.tsx +14 -4
  21. package/dist/registry/package/components/ColorModeToggle/index.tsx +44 -0
  22. package/dist/registry/package/components/CommandPalette/index.tsx +3 -2
  23. package/dist/registry/package/components/ConfirmDialog/index.tsx +24 -4
  24. package/dist/registry/package/components/CrudPage/dialogs.tsx +53 -8
  25. package/dist/registry/package/components/CrudPage/index.tsx +99 -25
  26. package/dist/registry/package/components/CurrencyInput/index.tsx +5 -2
  27. package/dist/registry/package/components/DataTable/index.tsx +53 -18
  28. package/dist/registry/package/components/DataTable/row-actions.tsx +20 -8
  29. package/dist/registry/package/components/DataTable/types.ts +100 -0
  30. package/dist/registry/package/components/DatePicker/index.tsx +14 -6
  31. package/dist/registry/package/components/DateRangeFilter/index.tsx +12 -3
  32. package/dist/registry/package/components/DateRangePicker/index.tsx +13 -4
  33. package/dist/registry/package/components/DateTimePicker/index.tsx +3 -1
  34. package/dist/registry/package/components/Drawer/index.tsx +16 -6
  35. package/dist/registry/package/components/DropdownMenu/index.tsx +37 -4
  36. package/dist/registry/package/components/DropdownMenu/styles.css +10 -0
  37. package/dist/registry/package/components/DurationDisplay/index.tsx +20 -2
  38. package/dist/registry/package/components/EntityMeta/index.tsx +14 -9
  39. package/dist/registry/package/components/ErrorState/index.tsx +9 -3
  40. package/dist/registry/package/components/ErrorState/styles.css +1 -1
  41. package/dist/registry/package/components/EventCalendar/index.tsx +657 -9
  42. package/dist/registry/package/components/EventCalendar/styles.css +360 -8
  43. package/dist/registry/package/components/EventManager/index.tsx +456 -0
  44. package/dist/registry/package/components/EventManager/styles.css +12 -0
  45. package/dist/registry/package/components/Field/index.tsx +74 -27
  46. package/dist/registry/package/components/FileUpload/index.tsx +13 -3
  47. package/dist/registry/package/components/FiltersBar/index.tsx +8 -3
  48. package/dist/registry/package/components/FiltersBar/styles.css +7 -0
  49. package/dist/registry/package/components/Form/index.tsx +118 -14
  50. package/dist/registry/package/components/Form/styles.css +3 -0
  51. package/dist/registry/package/components/HoverCard/index.tsx +8 -4
  52. package/dist/registry/package/components/Icons/index.tsx +18 -0
  53. package/dist/registry/package/components/ImageUpload/index.tsx +17 -2
  54. package/dist/registry/package/components/Input/index.tsx +3 -2
  55. package/dist/registry/package/components/JobProgress/index.tsx +289 -0
  56. package/dist/registry/package/components/JobProgress/styles.css +58 -0
  57. package/dist/registry/package/components/JsonEditor/index.tsx +18 -3
  58. package/dist/registry/package/components/JsonView/index.tsx +282 -0
  59. package/dist/registry/package/components/JsonView/styles.css +99 -0
  60. package/dist/registry/package/components/LiveIndicator/index.tsx +43 -0
  61. package/dist/registry/package/components/LiveIndicator/styles.css +41 -0
  62. package/dist/registry/package/components/LogStream/index.tsx +545 -0
  63. package/dist/registry/package/components/LogStream/styles.css +152 -0
  64. package/dist/registry/package/components/MarkdownEditor/index.tsx +14 -3
  65. package/dist/registry/package/components/Modal/index.tsx +7 -1
  66. package/dist/registry/package/components/MultiSelect/index.tsx +14 -3
  67. package/dist/registry/package/components/OtpInput/index.tsx +2 -1
  68. package/dist/registry/package/components/Pagination/index.tsx +13 -2
  69. package/dist/registry/package/components/PasswordInput/index.tsx +23 -9
  70. package/dist/registry/package/components/PhoneInput/index.tsx +66 -30
  71. package/dist/registry/package/components/Popover/index.tsx +19 -4
  72. package/dist/registry/package/components/QrCode/index.tsx +3 -2
  73. package/dist/registry/package/components/RadioGroup/styles.css +3 -0
  74. package/dist/registry/package/components/Rating/index.tsx +6 -2
  75. package/dist/registry/package/components/ReferenceSelect/index.tsx +8 -1
  76. package/dist/registry/package/components/RelativeTime/index.tsx +5 -1
  77. package/dist/registry/package/components/RequireSession/index.tsx +10 -4
  78. package/dist/registry/package/components/Resource/index.tsx +70 -17
  79. package/dist/registry/package/components/RichTextEditor/index.tsx +2 -1
  80. package/dist/registry/package/components/SearchInput/index.tsx +4 -1
  81. package/dist/registry/package/components/SecretFields/index.tsx +70 -17
  82. package/dist/registry/package/components/Select/index.tsx +34 -12
  83. package/dist/registry/package/components/Sidebar/index.tsx +22 -6
  84. package/dist/registry/package/components/Slider/index.tsx +26 -5
  85. package/dist/registry/package/components/Spinner/index.tsx +3 -2
  86. package/dist/registry/package/components/StatCard/index.tsx +11 -3
  87. package/dist/registry/package/components/Statistic/index.tsx +5 -1
  88. package/dist/registry/package/components/Steps/styles.css +2 -2
  89. package/dist/registry/package/components/TagsInput/index.tsx +2 -1
  90. package/dist/registry/package/components/TimePicker/index.tsx +10 -4
  91. package/dist/registry/package/components/Toast/index.tsx +20 -5
  92. package/dist/registry/package/components/Tour/index.tsx +2 -1
  93. package/dist/registry/package/components/Transfer/index.tsx +2 -1
  94. package/dist/registry/package/components/Tree/index.tsx +3 -2
  95. package/dist/registry/package/components/TreeSelect/index.tsx +17 -4
  96. package/dist/registry/package/components/UnsavedChangesGuard/index.tsx +34 -3
  97. package/dist/registry/registry.json +257 -75
  98. package/dist/registry/styles/sia-ui.css +23 -4
  99. package/package.json +5 -5
@@ -1,6 +1,6 @@
1
1
  import { type ReactNode } from "react";
2
2
  import { cn } from "@sia-ui/utils";
3
- import { useSiaLocale } from "@sia-ui/headless";
3
+ import { plural, useSiaLocale } from "@sia-ui/headless";
4
4
  import "./styles.css";
5
5
 
6
6
  export interface FiltersBarProps {
@@ -10,6 +10,11 @@ export interface FiltersBarProps {
10
10
  activeCount?: number;
11
11
  onReset?: () => void;
12
12
  resetLabel?: string;
13
+ /**
14
+ * La largeur de chaque filtre — `12rem` par défaut. Les filtres se placent
15
+ * côte à côte à cette largeur, et passent à la ligne quand elle manque.
16
+ */
17
+ filterWidth?: string;
13
18
  className?: string;
14
19
  }
15
20
 
@@ -20,8 +25,8 @@ export interface FiltersBarProps {
20
25
  * une liste filtrée ressemble à une liste vide, et l'on cherche la panne
21
26
  * avant de penser au filtre posé la veille.
22
27
  */
23
- export function FiltersBar({ search, filters, actions, activeCount = 0, onReset, resetLabel: resetLabelProp, className }: FiltersBarProps) {
28
+ export function FiltersBar({ search, filters, actions, activeCount = 0, onReset, resetLabel: resetLabelProp, filterWidth, className }: FiltersBarProps) {
24
29
  const locale = useSiaLocale();
25
30
  const resetLabel = resetLabelProp ?? locale.reset;
26
- return <section className={cn("sia-filters-bar", className)} aria-label="Filtres"><div className="sia-filters-bar__main">{search && <div className="sia-filters-bar__search">{search}</div>}{filters && <div className="sia-filters-bar__filters">{filters}</div>}</div><div className="sia-filters-bar__actions">{activeCount > 0 && <span className="sia-filters-bar__count">{activeCount} actif{activeCount > 1 ? "s" : ""}</span>}{onReset && activeCount > 0 && <button type="button" className="sia-filters-bar__reset" onClick={onReset}>{resetLabel}</button>}{actions}</div></section>;
31
+ return <section className={cn("sia-filters-bar", className)} aria-label={locale.filtersBar.label} {...(filterWidth ? { style: { ["--sia-filter-width" as string]: filterWidth } } : {})}><div className="sia-filters-bar__main">{search && <div className="sia-filters-bar__search">{search}</div>}{filters && <div className="sia-filters-bar__filters">{filters}</div>}</div><div className="sia-filters-bar__actions">{activeCount > 0 && <span className="sia-filters-bar__count">{plural(locale.filtersBar, "active", activeCount, locale.language)}</span>}{onReset && activeCount > 0 && <button type="button" className="sia-filters-bar__reset" onClick={onReset}>{resetLabel}</button>}{actions}</div></section>;
27
32
  }
@@ -2,5 +2,12 @@
2
2
  .sia-filters-bar__main,.sia-filters-bar__filters,.sia-filters-bar__actions { display:flex; align-items:center; gap:.75rem; flex-wrap:wrap; }
3
3
  .sia-filters-bar__main { flex:1; min-width:min(100%,18rem); }
4
4
  .sia-filters-bar__search { flex:1; min-width:min(100%,16rem); }
5
+ /* Les filtres occupent la place restante et s'y rangent en ligne : sans
6
+ cela le conteneur gardait la largeur de son contenu, et deux `Select` en
7
+ largeur pleine s'empilaient au lieu de s'aligner. Chaque filtre garde sa
8
+ largeur de base — une liste de 60rem pour trois options serait absurde —
9
+ et passe à la ligne quand la place manque. */
10
+ .sia-filters-bar__filters { flex:1 1 auto; min-width:0; }
11
+ .sia-filters-bar__filters > * { flex:0 1 var(--sia-filter-width,12rem); min-width:0; max-width:100%; }
5
12
  .sia-filters-bar__count { color:var(--sia-muted); font-size:.8rem; font-weight:650; }
6
13
  .sia-filters-bar__reset { border:0; color:var(--sia-primary); background:transparent; font:inherit; font-weight:650; cursor:pointer; }
@@ -2,16 +2,19 @@ import { useState, type CSSProperties, type ReactNode } from "react";
2
2
  import { cn } from "@sia-ui/utils";
3
3
  import {
4
4
  readSubmitError,
5
+ useAsyncValidation,
5
6
  useLocalForm,
6
7
  useSiaLocale,
8
+ type AsyncFieldValidator,
7
9
  type FormAdapter,
8
10
  type FormErrors,
9
11
  type FormSubmitResult,
10
12
  type FormValidator,
11
13
  } from "@sia-ui/headless";
12
- import { Alert } from "../Alert";
14
+ import { Alert, type AlertProps } from "../Alert";
13
15
  import { Button, type ButtonProps } from "../Button";
14
16
  import { Field, type FieldProps, type FieldValue } from "../Field";
17
+ import { Spinner, type SpinnerProps } from "../Spinner";
15
18
  import "./styles.css";
16
19
 
17
20
  /** Un champ du formulaire : les props de `Field`, plus sa place. */
@@ -21,6 +24,17 @@ export interface FormFieldConfig extends Omit<FieldProps, "field" | "name"> {
21
24
  /** Nombre de colonnes occupées. Ignoré sur une seule colonne. */
22
25
  colSpan?: number;
23
26
  rowSpan?: number;
27
+ /**
28
+ * Une vérification qui demande le serveur — un identifiant libre, un nom
29
+ * de domaine disponible. Rend le message d'erreur, ou rien.
30
+ *
31
+ * Relancée après la frappe (`asyncValidationDelay`), annulée à la frappe
32
+ * suivante par son `signal`. Pendant la vérification le champ l'annonce,
33
+ * et l'envoi attend ; une valeur refusée bloque l'envoi.
34
+ */
35
+ validateAsync?: AsyncFieldValidator<FieldValue>;
36
+ /** Affiché sous le champ quand `validateAsync` accepte la valeur. */
37
+ validText?: ReactNode;
24
38
  }
25
39
 
26
40
  /** Un groupe de champs, avec son titre et sa propre grille. */
@@ -37,14 +51,14 @@ function isGroup(entry: FormEntry): entry is FormFieldGroup {
37
51
  return "group" in entry;
38
52
  }
39
53
 
54
+ /** Les champs, groupes aplatis. */
55
+ function champsAplatis(fields: FormEntry[]): FormFieldConfig[] {
56
+ return fields.flatMap((entree) => (isGroup(entree) ? entree.fields : [entree]));
57
+ }
58
+
40
59
  /** Les noms de champs, groupes aplatis. */
41
60
  export function formFieldNames(fields: FormEntry[]): string[] {
42
- const noms: string[] = [];
43
- for (const entree of fields) {
44
- if (isGroup(entree)) noms.push(...entree.fields.map((f) => f.name));
45
- else noms.push(entree.name);
46
- }
47
- return noms;
61
+ return champsAplatis(fields).map((champ) => champ.name);
48
62
  }
49
63
 
50
64
  /**
@@ -80,10 +94,18 @@ export interface FormProps<TValues extends FormShape = FormShape> {
80
94
  values: TValues,
81
95
  ) => FormSubmitResult<TValues> | Promise<FormSubmitResult<TValues>>;
82
96
  columns?: number;
97
+ /** Le texte du bouton d'envoi. Par défaut, `save` de la locale. */
83
98
  submitText?: ReactNode;
99
+ /** Le texte du bouton de remise à zéro. Par défaut, `reset` de la locale. */
84
100
  clearText?: ReactNode;
85
101
  showClear?: boolean;
102
+ /**
103
+ * Les props du bouton d'envoi. `disabled` s'ajoute aux conditions du
104
+ * formulaire au lieu d'être remplacé par elles.
105
+ */
86
106
  submitProps?: ButtonProps;
107
+ /** Millisecondes sans frappe avant une vérification `validateAsync`. 400 par défaut. */
108
+ asyncValidationDelay?: number;
87
109
  /** Remplace entièrement la barre de boutons. */
88
110
  actions?: ReactNode;
89
111
  loading?: boolean;
@@ -92,6 +114,39 @@ export interface FormProps<TValues extends FormShape = FormShape> {
92
114
  /** Désactive l'envoi tant qu'une erreur est en vigueur. */
93
115
  requireValid?: boolean;
94
116
  className?: string;
117
+ /**
118
+ * Les props communes à tous les champs — orientation, libellé
119
+ * « facultatif », `controlProps`. Chaque champ déclaré dans `fields`
120
+ * l'emporte sur elles.
121
+ *
122
+ * Sans ce que le formulaire branche lui-même : le nom, la valeur, le
123
+ * changement et la sortie de champ (`field`), ni les messages d'erreur et
124
+ * de validation, qui sont propres à chaque champ.
125
+ */
126
+ fieldProps?: Partial<
127
+ Omit<
128
+ FieldProps,
129
+ | "field"
130
+ | "name"
131
+ | "value"
132
+ | "defaultValue"
133
+ | "onValueChange"
134
+ | "onBlur"
135
+ | "error"
136
+ | "success"
137
+ | "message"
138
+ >
139
+ >;
140
+ /**
141
+ * Les props de l'`Alert` qui annonce un échec d'envoi. Sans `children`, le
142
+ * message d'échec, ni `onDismiss`, qui l'efface.
143
+ */
144
+ alertProps?: Partial<Omit<AlertProps, "children" | "onDismiss">>;
145
+ /**
146
+ * Les props du `Spinner` d'une vérification `validateAsync` en cours.
147
+ * Sans `label`, repris de la locale comme le texte qui l'accompagne.
148
+ */
149
+ spinnerProps?: Partial<Omit<SpinnerProps, "label">>;
95
150
  }
96
151
 
97
152
  const VIDE = {} as FormShape;
@@ -123,15 +178,19 @@ export function Form<TValues extends FormShape = FormShape>({
123
178
  validate,
124
179
  onSubmit,
125
180
  columns = 1,
126
- submitText = "Enregistrer",
127
- clearText = "Réinitialiser",
181
+ submitText,
182
+ clearText,
128
183
  showClear = false,
129
184
  submitProps,
130
185
  actions,
131
186
  loading = false,
132
187
  requireDirty = true,
133
188
  requireValid = true,
189
+ asyncValidationDelay = 400,
134
190
  className,
191
+ fieldProps,
192
+ alertProps,
193
+ spinnerProps,
135
194
  }: FormProps<TValues>) {
136
195
  // Le hook est appelé sans condition, comme il se doit; son résultat n'est
137
196
  // utilisé que si aucun adaptateur n'a été fourni. Un `useLocalForm` inerte
@@ -152,7 +211,21 @@ export function Form<TValues extends FormShape = FormShape>({
152
211
  */
153
212
  const [echec, setEchec] = useState<string | null>(null);
154
213
 
214
+ // Les vérifications serveur déclarées sur les champs.
215
+ const verifications: Record<string, AsyncFieldValidator> = {};
216
+ for (const champ of champsAplatis(fields)) {
217
+ if (champ.validateAsync) {
218
+ verifications[champ.name] = champ.validateAsync as AsyncFieldValidator;
219
+ }
220
+ }
221
+ const asynchrone = useAsyncValidation(verifications, form.state.values, {
222
+ delay: asyncValidationDelay,
223
+ });
224
+
155
225
  const envoyer = () => {
226
+ // Entrée dans un champ envoie le formulaire même bouton désactivé : la
227
+ // garde est ici, pas seulement sur le bouton.
228
+ if (asynchrone.pending || asynchrone.invalid || submitProps?.disabled) return;
156
229
  setEchec(null);
157
230
  form.submit().catch((error: unknown) => {
158
231
  const { fields: erreurs, message } = readSubmitError(error);
@@ -178,6 +251,9 @@ export function Form<TValues extends FormShape = FormShape>({
178
251
  const busy = loading || form.state.isSubmitting;
179
252
  const submitDisabled =
180
253
  busy ||
254
+ Boolean(submitProps?.disabled) ||
255
+ asynchrone.pending ||
256
+ asynchrone.invalid ||
181
257
  (requireDirty && !form.state.isDirty) ||
182
258
  (requireValid && !form.state.isValid);
183
259
 
@@ -186,7 +262,29 @@ export function Form<TValues extends FormShape = FormShape>({
186
262
  });
187
263
 
188
264
  const rendre = (config: FormFieldConfig) => {
189
- const { name, colSpan, rowSpan, ...props } = config;
265
+ const { name, colSpan, rowSpan, validateAsync, validText, ...props } = config;
266
+ const binding = form.bind(name as keyof TValues & string);
267
+ const verification = validateAsync ? asynchrone.states[name] : undefined;
268
+
269
+ // L'erreur locale passe devant : une valeur mal formée n'a pas à être
270
+ // vérifiée auprès du serveur pour être refusée.
271
+ const etat =
272
+ binding.error || !verification
273
+ ? {}
274
+ : verification.status === "checking"
275
+ ? {
276
+ message: (
277
+ <span className="sia-form-checking">
278
+ <Spinner size="xs" {...spinnerProps} label={locale.checking} />
279
+ {locale.checking}
280
+ </span>
281
+ ),
282
+ }
283
+ : verification.status === "invalid"
284
+ ? { error: verification.error }
285
+ : verification.status === "valid" && validText
286
+ ? { success: validText }
287
+ : {};
190
288
 
191
289
  return (
192
290
  <div
@@ -198,8 +296,13 @@ export function Form<TValues extends FormShape = FormShape>({
198
296
  }}
199
297
  >
200
298
  <Field
299
+ {...fieldProps}
201
300
  {...props}
202
- field={form.bind(name as keyof TValues & string)}
301
+ {...(fieldProps?.className || props.className
302
+ ? { className: cn(fieldProps?.className, props.className) }
303
+ : {})}
304
+ {...etat}
305
+ field={binding}
203
306
  {...(busy ? { disabled: true } : {})}
204
307
  />
205
308
  </div>
@@ -243,7 +346,8 @@ export function Form<TValues extends FormShape = FormShape>({
243
346
  {echec && (
244
347
  <Alert
245
348
  tone="danger"
246
- className="sia-form-error"
349
+ {...alertProps}
350
+ className={cn("sia-form-error", alertProps?.className)}
247
351
  onDismiss={() => setEchec(null)}
248
352
  >
249
353
  {echec}
@@ -259,7 +363,7 @@ export function Form<TValues extends FormShape = FormShape>({
259
363
  disabled={busy}
260
364
  onClick={() => form.reset()}
261
365
  >
262
- {clearText}
366
+ {clearText ?? locale.reset}
263
367
  </Button>
264
368
  )}
265
369
  <Button
@@ -268,7 +372,7 @@ export function Form<TValues extends FormShape = FormShape>({
268
372
  loading={busy}
269
373
  disabled={submitDisabled}
270
374
  >
271
- {submitText}
375
+ {submitText ?? locale.save}
272
376
  </Button>
273
377
  </div>
274
378
  )}
@@ -78,3 +78,6 @@
78
78
  .sia-form-actions { flex-direction: column-reverse; }
79
79
  .sia-form-actions > * { width: 100%; }
80
80
  }
81
+
82
+ /* « Vérification… » sous un champ : le témoin aligné sur le texte. */
83
+ .sia-form-checking { display: inline-flex; align-items: center; gap: .4rem; color: var(--sia-muted); }
@@ -7,7 +7,7 @@ import {
7
7
  } from "react";
8
8
  import { cn } from "@sia-ui/utils";
9
9
  import { useHoverIntent } from "@sia-ui/headless";
10
- import { Overlay, type OverlayPlacement } from "../Overlay";
10
+ import { Overlay, type OverlayPlacement, type OverlayProps } from "../Overlay";
11
11
  import "./styles.css";
12
12
 
13
13
  export interface HoverCardProps {
@@ -21,6 +21,8 @@ export interface HoverCardProps {
21
21
  /** Délai avant fermeture, le temps d'atteindre le panneau à la souris. */
22
22
  closeDelay?: number;
23
23
  placement?: OverlayPlacement;
24
+ /** Réglages du panneau flottant — décalage, marge au bord, rôle, classe. */
25
+ overlayProps?: Partial<Omit<OverlayProps, "open" | "anchorRef" | "onOpenChange" | "children">>;
24
26
  open?: boolean;
25
27
  onOpenChange?: (open: boolean) => void;
26
28
  width?: number | string;
@@ -40,6 +42,7 @@ export function HoverCard({
40
42
  openDelay = 300,
41
43
  closeDelay = 150,
42
44
  placement = "bottom-start",
45
+ overlayProps,
43
46
  open: openProp,
44
47
  onOpenChange,
45
48
  width = 20 * 16,
@@ -76,12 +79,13 @@ export function HoverCard({
76
79
  <span className="sia-hover-card-anchor">
77
80
  {trigger}
78
81
  <Overlay
82
+ placement={placement}
83
+ role="dialog"
84
+ {...overlayProps}
85
+ className={cn("sia-hover-card", className, overlayProps?.className)}
79
86
  open={open}
80
87
  anchorRef={anchorRef}
81
88
  onOpenChange={setOpen}
82
- placement={placement}
83
- className={cn("sia-hover-card", className)}
84
- role="dialog"
85
89
  >
86
90
  <div
87
91
  style={{ width }}
@@ -291,3 +291,21 @@ export function PencilIcon(props: IconProps) {
291
291
  </IconBase>
292
292
  );
293
293
  }
294
+
295
+ /* ── Mode de couleur ───────────────────────────────────────────────────── */
296
+
297
+ export function SunIcon(props: IconProps) {
298
+ return (
299
+ <IconBase {...props}>
300
+ <circle cx="12" cy="12" r="4" />
301
+ <path d="M12 2.5v2M12 19.5v2M4.6 4.6 6 6M18 18l1.4 1.4M2.5 12h2M19.5 12h2M4.6 19.4 6 18M18 6l1.4-1.4" />
302
+ </IconBase>
303
+ );
304
+ }
305
+ export function MoonIcon(props: IconProps) {
306
+ return (
307
+ <IconBase {...props}>
308
+ <path d="M20 14.5A8 8 0 1 1 9.5 4a6.5 6.5 0 0 0 10.5 10.5Z" />
309
+ </IconBase>
310
+ );
311
+ }
@@ -1,8 +1,23 @@
1
1
  import { useEffect, useState } from "react";
2
2
  import { FileUpload, type FileUploadProps } from "../FileUpload";
3
3
  import { cn } from "@sia-ui/utils";
4
+ import { useSiaLocale } from "@sia-ui/headless";
4
5
  import "./styles.css";
5
- export interface ImageUploadProps extends Omit<FileUploadProps, "accept" | "onFilesChange"> { accept?: string; value?: string; onValueChange?: (file: File | null, previewUrl?: string) => void; previewAlt?: string; }
6
+ /**
7
+ * Les props de `FileUpload`, transmises telles quelles au dépôt : libellé,
8
+ * description, désactivation. `onFilesChange` cède la place à
9
+ * `onValueChange`, qui porte l'aperçu, et `multiple` est forcé à `false` —
10
+ * un aperçu ne montre qu'une image.
11
+ */
12
+ export interface ImageUploadProps extends Omit<FileUploadProps, "accept" | "onFilesChange"> {
13
+ accept?: string;
14
+ value?: string;
15
+ onValueChange?: (file: File | null, previewUrl?: string) => void;
16
+ /** Le texte alternatif de l'aperçu. Par défaut, celui de la locale (`imageUpload.previewAlt`). */
17
+ previewAlt?: string;
18
+ /** Le bouton qui retire l'image. Par défaut, `delete` de la locale. */
19
+ removeLabel?: string;
20
+ }
6
21
  /**
7
22
  * Un dépôt de fichier qui montre ce qu'on vient de choisir.
8
23
  *
@@ -10,4 +25,4 @@ export interface ImageUploadProps extends Omit<FileUploadProps, "accept" | "onFi
10
25
  * serveur pour afficher l'image laisse un carré vide au moment précis où
11
26
  * l'on veut vérifier qu'on n'a pas pris la mauvaise.
12
27
  */
13
- export function ImageUpload({ accept = "image/*", value, onValueChange, previewAlt = "Aperçu de l'image", className, ...props }: ImageUploadProps) { const [preview, setPreview] = useState(value); useEffect(() => setPreview(value), [value]); useEffect(() => () => { if (preview?.startsWith("blob:")) URL.revokeObjectURL(preview); }, [preview]); const select = (files: File[]) => { const file = files[0]; if (!file) return; const next = URL.createObjectURL(file); setPreview(next); onValueChange?.(file, next); }; return <div className={cn("sia-image-upload", className)}>{preview && <div className="sia-image-upload__preview"><img src={preview} alt={previewAlt} /><button type="button" onClick={() => { setPreview(undefined); onValueChange?.(null); }}>Supprimer</button></div>}<FileUpload {...props} accept={accept} multiple={false} onFilesChange={select} /></div>; }
28
+ export function ImageUpload({ accept = "image/*", value, onValueChange, previewAlt: previewAltProp, removeLabel: removeLabelProp, className, ...props }: ImageUploadProps) { const locale = useSiaLocale(); const previewAlt = previewAltProp ?? locale.imageUpload.previewAlt; const removeLabel = removeLabelProp ?? locale.delete; const [preview, setPreview] = useState(value); useEffect(() => setPreview(value), [value]); useEffect(() => () => { if (preview?.startsWith("blob:")) URL.revokeObjectURL(preview); }, [preview]); const select = (files: File[]) => { const file = files[0]; if (!file) return; const next = URL.createObjectURL(file); setPreview(next); onValueChange?.(file, next); }; return <div className={cn("sia-image-upload", className)}>{preview && <div className="sia-image-upload__preview"><img src={preview} alt={previewAlt} /><button type="button" onClick={() => { setPreview(undefined); onValueChange?.(null); }}>{removeLabel}</button></div>}<FileUpload {...props} accept={accept} multiple={false} onFilesChange={select} /></div>; }
@@ -7,7 +7,7 @@ import {
7
7
  import { cn, toneClass } from "@sia-ui/utils";
8
8
  import { Spinner, type SpinnerProps } from "../Spinner";
9
9
  import type { ComponentTone } from "@sia-ui/tokens";
10
- import { useComponentDefaults } from "@sia-ui/headless";
10
+ import { useComponentDefaults, useSiaLocale } from "@sia-ui/headless";
11
11
  import "./styles.css";
12
12
 
13
13
  export interface InputProps extends InputHTMLAttributes<HTMLInputElement> {
@@ -50,7 +50,8 @@ export const Input = forwardRef<HTMLInputElement, InputProps>(
50
50
  ref,
51
51
  ) => {
52
52
  const defaults = useComponentDefaults<InputProps>("input");
53
- const loadingLabel = loadingLabelProp ?? defaults.loadingLabel ?? "Chargement";
53
+ const locale = useSiaLocale();
54
+ const loadingLabel = loadingLabelProp ?? defaults.loadingLabel ?? locale.loading;
54
55
  const generatedId = useId();
55
56
  const id = providedId ?? generatedId;
56
57
  const decorated = Boolean(left || right || loading);