@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
@@ -0,0 +1,456 @@
1
+ import { useState, type ReactNode } from "react";
2
+ import { cn } from "@sia-ui/utils";
3
+ import {
4
+ dayToDate,
5
+ eventDays,
6
+ hasFormErrors,
7
+ useSiaLocale,
8
+ useLocalForm,
9
+ type FormErrors,
10
+ type EventManagerMessages,
11
+ type FormSubmitResult,
12
+ } from "@sia-ui/headless";
13
+ import type { ComponentTone } from "@sia-ui/tokens";
14
+ import { Badge, type BadgeProps } from "../Badge";
15
+ import { Button, type ButtonProps } from "../Button";
16
+ import { Descriptions, type DescriptionsProps } from "../Descriptions";
17
+ import { ConfirmDialog, type ConfirmDialogProps } from "../ConfirmDialog";
18
+ import {
19
+ EventCalendar,
20
+ type CalendarEvent,
21
+ type EventCalendarProps,
22
+ } from "../EventCalendar";
23
+ import { Form, type FormEntry, type FormProps, type FormShape } from "../Form";
24
+ import { Modal, type ModalProps } from "../Modal";
25
+ import { PlusIcon } from "../Icons";
26
+ import "./styles.css";
27
+
28
+ /** Ce que le formulaire rend : un événement prêt à envoyer, champs métier compris. */
29
+ export interface EventDraft {
30
+ title: string;
31
+ start: string;
32
+ end?: string | undefined;
33
+ allDay: boolean;
34
+ tone?: ComponentTone | undefined;
35
+ location?: string | undefined;
36
+ description?: string | undefined;
37
+ /** Les champs déclarés dans `extraFields`, sous leur nom. */
38
+ [champ: string]: unknown;
39
+ }
40
+
41
+ type Resultat = FormSubmitResult<FormShape> | Promise<FormSubmitResult<FormShape>>;
42
+
43
+ /** Les libellés du gestionnaire : le groupe `eventManager` de la locale SIA. */
44
+ export type EventManagerLabels = EventManagerMessages;
45
+
46
+ /** Les couleurs proposées par défaut, nommées par la locale. */
47
+ function tonsDe(labels: EventManagerLabels): Array<{ value: ComponentTone; label: string }> {
48
+ return [
49
+ { value: "primary", label: labels.tonePrimary },
50
+ { value: "success", label: labels.toneSuccess },
51
+ { value: "warning", label: labels.toneWarning },
52
+ { value: "danger", label: labels.toneDanger },
53
+ { value: "info", label: labels.toneInfo },
54
+ { value: "neutral", label: labels.toneNeutral },
55
+ ];
56
+ }
57
+
58
+ export interface EventManagerProps<T extends CalendarEvent = CalendarEvent> {
59
+ events: T[];
60
+ /** Absent : pas de création. Rendre `{ champ: message }` garde le formulaire ouvert. */
61
+ onCreate?: (draft: EventDraft) => Resultat;
62
+ /** Absent : les événements se consultent sans se modifier. */
63
+ onUpdate?: (event: T, draft: EventDraft) => Resultat;
64
+ /** Absent : pas de suppression. Confirmée d'office. */
65
+ onDelete?: (event: T) => void | Promise<void>;
66
+
67
+ /** Des champs métier en plus : participants, salle, rappel. Leurs valeurs arrivent dans le brouillon. */
68
+ extraFields?: FormEntry[];
69
+ /** Les valeurs de départ de ces champs, et comment les relire sur un événement. */
70
+ extraDefaults?: FormShape;
71
+ /** Les couleurs proposées. Par défaut, les tons du système, nommés par la locale. */
72
+ toneOptions?: Array<{ value: ComponentTone; label: string }>;
73
+ /** Remplace le contenu de la fiche d'un événement. */
74
+ renderDetail?: (event: T) => ReactNode;
75
+ /** Remplace, clé par clé, les textes du groupe `eventManager` de la locale. */
76
+ labels?: Partial<EventManagerLabels>;
77
+ /** La langue des dates de la fiche. Par défaut, celle de la locale SIA. */
78
+ locale?: string;
79
+
80
+ /** Le calendrier lui-même : vues, variantes, période, rendu des événements. */
81
+ calendarProps?: Partial<
82
+ Omit<EventCalendarProps<T>, "events" | "onDateClick" | "onEventClick">
83
+ >;
84
+ /** Les boîtes de fiche et de formulaire. */
85
+ modalProps?: Partial<Omit<ModalProps, "open" | "onOpenChange" | "children" | "title" | "footer">>;
86
+ /** Le formulaire, hors champs, valeurs et envoi qu'il reçoit d'ici. */
87
+ formProps?: Partial<Omit<FormProps, "fields" | "form" | "defaultValues" | "validate" | "onSubmit">>;
88
+ /** La confirmation de suppression. */
89
+ confirmDialogProps?: Partial<Omit<ConfirmDialogProps, "open" | "onOpenChange" | "onConfirm">>;
90
+ /** Le bouton « Nouvel événement » de la barre. */
91
+ createButtonProps?: Partial<Omit<ButtonProps, "onClick">>;
92
+ /** La fiche d'un événement, hors rubriques qu'elle tire de l'événement. */
93
+ descriptionsProps?: Partial<Omit<DescriptionsProps, "items">>;
94
+ /** La pastille d'une couleur, dans la fiche et dans le choix des couleurs. */
95
+ toneBadgeProps?: Partial<Omit<BadgeProps, "tone" | "children">>;
96
+ className?: string;
97
+ }
98
+
99
+ type Etat<T> =
100
+ | { mode: "create"; date: string; time?: string | undefined }
101
+ | { mode: "view"; event: T }
102
+ | { mode: "edit"; event: T }
103
+ | null;
104
+
105
+ const aujourdhui = () => {
106
+ const d = new Date();
107
+ return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`;
108
+ };
109
+
110
+ const heureDe = (valeur: string | undefined) => (valeur ? /T(\d{2}:\d{2})/.exec(valeur)?.[1] : undefined);
111
+
112
+ /** Une heure plus tard, sans passer minuit. */
113
+ function uneHeureApres(heure: string) {
114
+ const [h, m] = heure.split(":").map(Number) as [number, number];
115
+ return `${String(Math.min(23, h + 1)).padStart(2, "0")}:${String(m).padStart(2, "0")}`;
116
+ }
117
+
118
+ /** Les valeurs du formulaire d'un événement existant, ou d'un nouveau. */
119
+ function valeursDe<T extends CalendarEvent>(etat: Exclude<Etat<T>, null>, extraDefaults: FormShape): FormShape {
120
+ if (etat.mode === "create") {
121
+ const debut = etat.time ?? "09:00";
122
+ return {
123
+ title: "",
124
+ date: etat.date,
125
+ endDate: "",
126
+ allDay: false,
127
+ startTime: debut,
128
+ endTime: uneHeureApres(debut),
129
+ tone: "primary",
130
+ location: "",
131
+ description: "",
132
+ ...extraDefaults,
133
+ };
134
+ }
135
+
136
+ const { event } = etat;
137
+ const start = event.start ?? event.date ?? aujourdhui();
138
+ const jours = eventDays({ id: event.id, title: event.title, start, end: event.end });
139
+ const allDay = Boolean(event.allDay) || !heureDe(start);
140
+ const extras: FormShape = {};
141
+ for (const cle of Object.keys(extraDefaults)) {
142
+ const valeur = (event as Record<string, unknown>)[cle];
143
+ extras[cle] = (valeur ?? extraDefaults[cle]) as FormShape[string];
144
+ }
145
+ return {
146
+ title: event.title,
147
+ date: jours.start,
148
+ endDate: jours.end !== jours.start ? jours.end : "",
149
+ allDay,
150
+ startTime: heureDe(start) ?? "09:00",
151
+ endTime: heureDe(event.end) ?? uneHeureApres(heureDe(start) ?? "09:00"),
152
+ tone: event.tone ?? "primary",
153
+ location: typeof event.location === "string" ? event.location : "",
154
+ description: typeof event.description === "string" ? event.description : "",
155
+ ...extras,
156
+ };
157
+ }
158
+
159
+ /** Le brouillon d'événement que rend un formulaire rempli. */
160
+ function brouillonDe(valeurs: FormShape, extras: string[]): EventDraft {
161
+ const allDay = Boolean(valeurs.allDay);
162
+ const date = String(valeurs.date);
163
+ const fin = String(valeurs.endDate || "") || date;
164
+ const brouillon: EventDraft = {
165
+ title: String(valeurs.title).trim(),
166
+ allDay,
167
+ start: allDay ? date : `${date}T${valeurs.startTime}`,
168
+ end: allDay
169
+ ? fin !== date ? fin : undefined
170
+ : valeurs.endTime
171
+ ? `${fin}T${valeurs.endTime}`
172
+ : undefined,
173
+ tone: (valeurs.tone || undefined) as ComponentTone | undefined,
174
+ location: String(valeurs.location || "") || undefined,
175
+ description: String(valeurs.description || "") || undefined,
176
+ };
177
+ for (const cle of extras) brouillon[cle] = valeurs[cle];
178
+ return brouillon;
179
+ }
180
+
181
+ /**
182
+ * Un calendrier où l'on gère ses événements.
183
+ *
184
+ * Il assemble ce qui existe — \`EventCalendar\` pour voir, \`Modal\` et
185
+ * \`Form\` pour saisir, \`ConfirmDialog\` pour supprimer — et tient le reste :
186
+ *
187
+ * - un clic sur un jour ouvre la création à cette date, et à la demi-heure
188
+ * visée dans la grille horaire ;
189
+ * - un clic sur un événement ouvre sa fiche, d'où l'on modifie ou supprime ;
190
+ * - un refus du serveur garde le formulaire ouvert, erreurs sous les champs.
191
+ *
192
+ * Les données restent à l'appelant : `onCreate`, `onUpdate` et `onDelete`
193
+ * reçoivent un brouillon prêt à envoyer, et le calendrier affiche ce qu'on
194
+ * lui repasse dans `events`.
195
+ */
196
+ export function EventManager<T extends CalendarEvent = CalendarEvent>({
197
+ events,
198
+ onCreate,
199
+ onUpdate,
200
+ onDelete,
201
+ extraFields = [],
202
+ extraDefaults = {},
203
+ toneOptions: toneOptionsProp,
204
+ renderDetail,
205
+ labels: labelsProp,
206
+ locale: localeProp,
207
+ calendarProps,
208
+ modalProps,
209
+ formProps,
210
+ confirmDialogProps,
211
+ createButtonProps,
212
+ descriptionsProps,
213
+ toneBadgeProps,
214
+ className,
215
+ }: EventManagerProps<T>) {
216
+ const messages = useSiaLocale();
217
+ const locale = localeProp ?? messages.language;
218
+ const labels = { ...messages.eventManager, ...labelsProp };
219
+ const toneOptions = toneOptionsProp ?? tonsDe(labels);
220
+ const [etat, setEtat] = useState<Etat<T>>(null);
221
+ const [aSupprimer, setASupprimer] = useState<T | null>(null);
222
+
223
+ const fermer = () => setEtat(null);
224
+
225
+ const creer = onCreate
226
+ ? (date: string, time?: string) => setEtat({ mode: "create", date, time })
227
+ : undefined;
228
+
229
+ return (
230
+ <div className={cn("sia-event-manager", className)}>
231
+ <EventCalendar<T>
232
+ {...calendarProps}
233
+ events={events}
234
+ {...(creer ? { onDateClick: creer } : {})}
235
+ onEventClick={(event) => setEtat({ mode: "view", event })}
236
+ toolbarActions={
237
+ <>
238
+ {calendarProps?.toolbarActions}
239
+ {creer && (
240
+ <Button size="sm" leftIcon={<PlusIcon />} {...createButtonProps} onClick={() => creer(aujourdhui())}>
241
+ {createButtonProps?.children ?? labels.create}
242
+ </Button>
243
+ )}
244
+ </>
245
+ }
246
+ />
247
+
248
+ {etat?.mode === "view" && (
249
+ <Modal
250
+ {...modalProps}
251
+ open
252
+ onOpenChange={(ouvert) => !ouvert && fermer()}
253
+ title={etat.event.title}
254
+ className={cn("sia-event-manager__detail", modalProps?.className)}
255
+ footer={
256
+ (onUpdate || onDelete) && (
257
+ <div className="sia-event-manager__actions">
258
+ {onDelete && (
259
+ <Button variant="ghost" tone="danger" onClick={() => setASupprimer(etat.event)}>
260
+ {labels.delete}
261
+ </Button>
262
+ )}
263
+ {onUpdate && (
264
+ <Button onClick={() => setEtat({ mode: "edit", event: etat.event })}>{labels.edit}</Button>
265
+ )}
266
+ </div>
267
+ )
268
+ }
269
+ >
270
+ {renderDetail ? (
271
+ renderDetail(etat.event)
272
+ ) : (
273
+ <Fiche
274
+ event={etat.event}
275
+ locale={locale}
276
+ labels={labels}
277
+ toneOptions={toneOptions}
278
+ descriptionsProps={descriptionsProps}
279
+ toneBadgeProps={toneBadgeProps}
280
+ />
281
+ )}
282
+ </Modal>
283
+ )}
284
+
285
+ {(etat?.mode === "create" || etat?.mode === "edit") && (
286
+ <Modal
287
+ {...modalProps}
288
+ open
289
+ onOpenChange={(ouvert) => !ouvert && fermer()}
290
+ title={etat.mode === "create" ? labels.newEvent : labels.editEvent}
291
+ className={cn("sia-event-manager__form", modalProps?.className)}
292
+ >
293
+ <FormulaireEvenement
294
+ // Remonté à chaque ouverture : les valeurs de départ sont figées
295
+ // à la première passe de \`useLocalForm\`.
296
+ key={etat.mode === "edit" ? `edit-${etat.event.id}` : `create-${etat.date}-${etat.time ?? ""}`}
297
+ depart={valeursDe(etat, extraDefaults)}
298
+ extraFields={extraFields}
299
+ toneOptions={toneOptions}
300
+ toneBadgeProps={toneBadgeProps}
301
+ labels={labels}
302
+ formProps={formProps}
303
+ envoyer={async (brouillon) => {
304
+ const resultat =
305
+ etat.mode === "create"
306
+ ? await onCreate?.(brouillon)
307
+ : await onUpdate?.(etat.event, brouillon);
308
+ if (hasFormErrors(resultat)) return resultat;
309
+ fermer();
310
+ return undefined;
311
+ }}
312
+ />
313
+ </Modal>
314
+ )}
315
+
316
+ {aSupprimer && onDelete && (
317
+ <ConfirmDialog
318
+ title={labels.deleteTitle}
319
+ description={labels.deleteDescription}
320
+ confirmLabel={labels.delete}
321
+ tone="danger"
322
+ {...confirmDialogProps}
323
+ open
324
+ onOpenChange={(ouvert) => !ouvert && setASupprimer(null)}
325
+ onConfirm={async () => {
326
+ await onDelete(aSupprimer);
327
+ setASupprimer(null);
328
+ fermer();
329
+ }}
330
+ />
331
+ )}
332
+ </div>
333
+ );
334
+ }
335
+
336
+ /** La fiche d'un événement : quand, où, quoi. */
337
+ /** La fiche d'un événement : quand, où, quoi — des `Descriptions` en une colonne. */
338
+ function Fiche<T extends CalendarEvent>({
339
+ event,
340
+ locale,
341
+ labels,
342
+ toneOptions,
343
+ descriptionsProps,
344
+ toneBadgeProps,
345
+ }: {
346
+ event: T;
347
+ locale: string;
348
+ labels: EventManagerLabels;
349
+ toneOptions: Array<{ value: ComponentTone; label: string }>;
350
+ descriptionsProps: EventManagerProps["descriptionsProps"];
351
+ toneBadgeProps: EventManagerProps["toneBadgeProps"];
352
+ }) {
353
+ const start = event.start ?? event.date ?? "";
354
+ const { start: debut, end: fin } = eventDays({ id: event.id, title: event.title, start, end: event.end });
355
+ const jour = (d: string) =>
356
+ new Intl.DateTimeFormat(locale, { weekday: "long", day: "numeric", month: "long", year: "numeric" }).format(dayToDate(d));
357
+ const h1 = heureDe(start);
358
+ const h2 = heureDe(event.end);
359
+ const quand =
360
+ debut === fin
361
+ ? `${jour(debut)}${event.allDay || !h1 ? ` · ${labels.allDay.toLowerCase()}` : ` · ${h1}${h2 ? ` – ${h2}` : ""}`}`
362
+ : `${jour(debut)}${h1 && !event.allDay ? ` ${h1}` : ""} → ${jour(fin)}${h2 && !event.allDay ? ` ${h2}` : ""}`;
363
+
364
+ const ton = event.tone ?? "primary";
365
+ const couleur = toneOptions.find((t) => t.value === ton)?.label;
366
+
367
+ return (
368
+ <Descriptions
369
+ columns={1}
370
+ {...descriptionsProps}
371
+ className={cn("sia-event-manager__fiche", `sia-ec__event--${ton}`, descriptionsProps?.className)}
372
+ items={[
373
+ {
374
+ key: "quand",
375
+ label: labels.date,
376
+ value: <span className="sia-event-manager__when">{quand.charAt(0).toUpperCase() + quand.slice(1)}</span>,
377
+ },
378
+ ...(couleur ? [{ key: "ton", label: labels.tone, value: <Badge {...toneBadgeProps} tone={ton}>{couleur}</Badge> }] : []),
379
+ ...(event.location ? [{ key: "lieu", label: labels.location, value: event.location }] : []),
380
+ ...(event.description
381
+ ? [{ key: "description", label: labels.description, value: <span className="sia-event-manager__description">{event.description}</span> }]
382
+ : []),
383
+ ]}
384
+ />
385
+ );
386
+ }
387
+
388
+ interface FormulaireProps {
389
+ depart: FormShape;
390
+ extraFields: FormEntry[];
391
+ toneOptions: Array<{ value: ComponentTone; label: string }>;
392
+ toneBadgeProps: EventManagerProps["toneBadgeProps"];
393
+ labels: EventManagerLabels;
394
+ formProps: EventManagerProps["formProps"];
395
+ envoyer: (brouillon: EventDraft) => Promise<FormSubmitResult<FormShape>>;
396
+ }
397
+
398
+ function FormulaireEvenement({ depart, extraFields, toneOptions, toneBadgeProps, labels, formProps, envoyer }: FormulaireProps) {
399
+ const extras = extraFields.flatMap((e) => ("group" in e ? e.fields : [e])).map((f) => f.name);
400
+
401
+ const form = useLocalForm<FormShape>({
402
+ defaultValues: depart,
403
+ validate: (v) => {
404
+ const erreurs: FormErrors<FormShape> = {};
405
+ if (!String(v.title ?? "").trim()) erreurs.title = labels.titleRequired;
406
+ const debut = v.allDay ? String(v.date) : `${v.date}T${v.startTime}`;
407
+ const fin = v.allDay
408
+ ? String(v.endDate || v.date)
409
+ : `${v.endDate || v.date}T${v.endTime || v.startTime}`;
410
+ if (fin < debut) erreurs[v.allDay ? "endDate" : "endTime"] = labels.endBeforeStart;
411
+ return erreurs;
412
+ },
413
+ onSubmit: (valeurs) => envoyer(brouillonDe(valeurs, extras)),
414
+ });
415
+
416
+ const journee = Boolean(form.state.values.allDay);
417
+
418
+ // Les champs suivent la saisie : pas d'heures pour une journée entière.
419
+ const champs: FormEntry[] = [
420
+ { name: "title", label: labels.title, required: true, colSpan: 2 },
421
+ { name: "date", label: labels.date, type: "date", required: true },
422
+ { name: "endDate", label: labels.endDate, type: "date" },
423
+ { name: "allDay", label: labels.allDay, type: "switch", colSpan: 2 },
424
+ ...(journee
425
+ ? []
426
+ : ([
427
+ { name: "startTime", label: labels.startTime, type: "time" },
428
+ { name: "endTime", label: labels.endTime, type: "time" },
429
+ ] as FormEntry[])),
430
+ {
431
+ name: "tone",
432
+ label: labels.tone,
433
+ type: "select",
434
+ colSpan: 2,
435
+ options: toneOptions.map((t) => ({
436
+ value: t.value,
437
+ label: <Badge {...toneBadgeProps} tone={t.value}>{t.label}</Badge>,
438
+ keywords: [t.label],
439
+ })),
440
+ },
441
+ { name: "location", label: labels.location, colSpan: 2 },
442
+ { name: "description", label: labels.description, type: "textarea", colSpan: 2 },
443
+ ...extraFields,
444
+ ];
445
+
446
+ return (
447
+ <Form
448
+ columns={2}
449
+ submitText={formProps?.submitText ?? labels.submit}
450
+ requireDirty={false}
451
+ {...formProps}
452
+ form={form}
453
+ fields={champs}
454
+ />
455
+ );
456
+ }
@@ -0,0 +1,12 @@
1
+ .sia-event-manager { min-width: 0; }
2
+
3
+ /* La fiche : les `Descriptions` du système, et la barre du ton à gauche —
4
+ elle rappelle l'événement cliqué dans la grille. */
5
+ .sia-event-manager__fiche {
6
+ padding-left: .85rem;
7
+ border-left: 3px solid var(--ec-tone, var(--sia-primary));
8
+ }
9
+ .sia-event-manager__when { font-weight: 600; }
10
+ .sia-event-manager__description { white-space: pre-line; }
11
+
12
+ .sia-event-manager__actions { display: flex; justify-content: flex-end; gap: .5rem; width: 100%; }
@@ -6,6 +6,7 @@ import {
6
6
  type ReactNode,
7
7
  } from "react";
8
8
  import { cn } from "@sia-ui/utils";
9
+ import { useSiaLocale } from "@sia-ui/headless";
9
10
  import { Input, type InputProps } from "../Input";
10
11
  import { Textarea, type TextareaProps } from "../Textarea";
11
12
  import { CurrencyInput, type CurrencyInputProps } from "../CurrencyInput";
@@ -91,31 +92,38 @@ export type FieldValue =
91
92
  | File
92
93
  | null
93
94
  | undefined;
94
- type ControlProps = Partial<
95
- InputProps &
96
- TextareaProps &
97
- CurrencyInputProps &
98
- DatePickerProps &
99
- TimePickerProps &
100
- DateTimePickerProps &
101
- SelectProps &
102
- MultiSelectProps &
103
- RadioGroupProps &
104
- CheckboxProps &
105
- SwitchProps &
106
- FileUploadProps &
107
- ImageUploadProps &
108
- SliderProps &
109
- RatingProps &
110
- ColorPickerProps &
111
- RichTextEditorProps &
112
- MarkdownEditorProps &
113
- JsonEditorProps &
114
- OtpInputProps &
115
- TagsInputProps &
116
- AutocompleteProps &
117
- ReferenceSelectProps
118
- >;
95
+ /**
96
+ * Ce que le champ décide seul pour son contrôle, quel qu'il soit.
97
+ *
98
+ * La valeur et son changement viennent de `value`/`onValueChange` ou du
99
+ * branchement `field` ; le nom, l'identifiant, les options, le texte
100
+ * indicatif et la désactivation ont chacun leur prop sur `Field`. Les
101
+ * laisser passer aussi par `controlProps` donnerait deux sources pour la
102
+ * même chose, dont l'une serait silencieusement écrasée.
103
+ */
104
+ type FieldPilotedKey =
105
+ | "value"
106
+ | "defaultValue"
107
+ | "onChange"
108
+ | "onValueChange"
109
+ | "onCheckedChange"
110
+ | "onFilesChange"
111
+ | "checked"
112
+ | "defaultChecked"
113
+ | "name"
114
+ | "id"
115
+ | "options"
116
+ | "placeholder"
117
+ | "disabled"
118
+ | "children";
119
+
120
+ /**
121
+ * Distributif : un `Omit` posé sur l'union entière n'en garderait que les
122
+ * clés communes, et `searchable` ou `maxLength` disparaîtraient.
123
+ */
124
+ type FieldControlSlot<P> = P extends unknown
125
+ ? Partial<Omit<P, FieldPilotedKey>>
126
+ : never;
119
127
 
120
128
  /**
121
129
  * Un branchement de formulaire, quel que soit le type exact de sa valeur.
@@ -163,6 +171,7 @@ export interface FieldProps {
163
171
  success?: ReactNode;
164
172
  required?: boolean;
165
173
  optional?: boolean;
174
+ /** La mention des champs facultatifs. Par défaut, `optional` de la locale. */
166
175
  optionalLabel?: ReactNode;
167
176
  htmlFor?: string;
168
177
  orientation?: "vertical" | "horizontal";
@@ -176,10 +185,46 @@ export interface FieldProps {
176
185
  controlLabel?: ReactNode;
177
186
  placeholder?: string;
178
187
  disabled?: boolean;
179
- controlProps?: ControlProps;
188
+ /**
189
+ * Réglages du contrôle que `type` choisit — `searchable` d'un `Select`,
190
+ * `maxLength` d'un `Input`, `accept` d'un `FileUpload`. Pour `radio`, ils
191
+ * vont au `RadioGroup`.
192
+ *
193
+ * Appliqués sous ce que le champ pilote : valeur, changement, nom,
194
+ * identifiant, options, texte indicatif et désactivation gardent leurs
195
+ * props dédiées. Ignorés quand le contrôle est fourni en `children`.
196
+ */
197
+ controlProps?: FieldControlSlot<
198
+ | InputProps
199
+ | TextareaProps
200
+ | CurrencyInputProps
201
+ | DatePickerProps
202
+ | TimePickerProps
203
+ | DateTimePickerProps
204
+ | SelectProps
205
+ | MultiSelectProps
206
+ | RadioGroupProps
207
+ | CheckboxProps
208
+ | SwitchProps
209
+ | FileUploadProps
210
+ | ImageUploadProps
211
+ | SliderProps
212
+ | RatingProps
213
+ | ColorPickerProps
214
+ | RichTextEditorProps
215
+ | MarkdownEditorProps
216
+ | JsonEditorProps
217
+ | OtpInputProps
218
+ | TagsInputProps
219
+ | AutocompleteProps
220
+ | ReferenceSelectProps
221
+ >;
180
222
  children?: ReactElement<FieldControlProps>;
181
223
  }
182
224
 
225
+ /** Les réglages qu'accepte `controlProps`, pour les composer hors du JSX. */
226
+ export type FieldControlOverrides = NonNullable<FieldProps["controlProps"]>;
227
+
183
228
  function joinIds(...values: Array<string | undefined>) {
184
229
  return values.filter(Boolean).join(" ") || undefined;
185
230
  }
@@ -586,12 +631,14 @@ export function Field(rawProps: FieldProps) {
586
631
  success,
587
632
  required = false,
588
633
  optional = false,
589
- optionalLabel = "Optionnel",
634
+ optionalLabel: optionalLabelProp,
590
635
  htmlFor,
591
636
  orientation = "vertical",
592
637
  className,
593
638
  children,
594
639
  } = props;
640
+ const locale = useSiaLocale();
641
+ const optionalLabel = optionalLabelProp ?? locale.optional;
595
642
  const generatedControlId = useId();
596
643
  const labelId = useId();
597
644
  const helpTextId = useId();
@@ -5,14 +5,19 @@ import {
5
5
  type InputHTMLAttributes,
6
6
  } from "react";
7
7
  import { cn } from "@sia-ui/utils";
8
+ import { useSiaLocale } from "@sia-ui/headless";
8
9
  import "./styles.css";
9
10
 
10
11
  export interface FileUploadProps extends Omit<
11
12
  InputHTMLAttributes<HTMLInputElement>,
12
13
  "type" | "onChange"
13
14
  > {
15
+ /** Le titre de la zone. Par défaut, celui de la locale (`fileUpload.title`). */
14
16
  label?: string;
17
+ /** La phrase d'aide. Par défaut, celle de la locale (`fileUpload.description`). */
15
18
  description?: string;
19
+ /** Le bouton qui ouvre le sélecteur. Par défaut, celui de la locale (`fileUpload.browse`). */
20
+ browseLabel?: string;
16
21
  onFilesChange?: (files: File[]) => void;
17
22
  }
18
23
  /**
@@ -23,8 +28,9 @@ export interface FileUploadProps extends Omit<
23
28
  * champ natif.
24
29
  */
25
30
  export function FileUpload({
26
- label = "Ajouter des fichiers",
27
- description = "Glissez les fichiers ici ou parcourez votre appareil.",
31
+ label: labelProp,
32
+ description: descriptionProp,
33
+ browseLabel: browseLabelProp,
28
34
  onFilesChange,
29
35
  className,
30
36
  multiple = true,
@@ -32,6 +38,10 @@ export function FileUpload({
32
38
  disabled,
33
39
  ...props
34
40
  }: FileUploadProps) {
41
+ const messages = useSiaLocale().fileUpload;
42
+ const label = labelProp ?? messages.title;
43
+ const description = descriptionProp ?? messages.description;
44
+ const browseLabel = browseLabelProp ?? messages.browse;
35
45
  const inputRef = useRef<HTMLInputElement>(null);
36
46
  const emit = (files: FileList | null) =>
37
47
  onFilesChange?.(Array.from(files ?? []));
@@ -68,7 +78,7 @@ export function FileUpload({
68
78
  disabled={disabled}
69
79
  onClick={() => inputRef.current?.click()}
70
80
  >
71
- Parcourir
81
+ {browseLabel}
72
82
  </button>
73
83
  </div>
74
84
  );