@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.
- package/CHANGELOG.md +6 -0
- package/README.md +6 -0
- package/dist/registry/package/components/AccountMenu/index.tsx +107 -0
- package/dist/registry/package/components/AccountMenu/styles.css +30 -0
- package/dist/registry/package/components/ActivityLog/index.tsx +96 -25
- package/dist/registry/package/components/AmountDisplay/index.tsx +5 -2
- package/dist/registry/package/components/AppShell/index.tsx +44 -8
- package/dist/registry/package/components/AppShell/tab-bar.tsx +6 -3
- package/dist/registry/package/components/AsyncState/index.tsx +29 -7
- package/dist/registry/package/components/AuditMeta/index.tsx +12 -2
- package/dist/registry/package/components/Avatar/index.tsx +4 -9
- package/dist/registry/package/components/Badge/index.tsx +3 -2
- package/dist/registry/package/components/Badge/styles.css +4 -4
- package/dist/registry/package/components/BrandIcon/index.tsx +100 -0
- package/dist/registry/package/components/BrandIcon/styles.css +31 -0
- package/dist/registry/package/components/Breadcrumb/index.tsx +14 -2
- package/dist/registry/package/components/Button/index.tsx +3 -2
- package/dist/registry/package/components/Button/styles.css +4 -4
- package/dist/registry/package/components/Calendar/index.tsx +7 -3
- package/dist/registry/package/components/Card/index.tsx +14 -4
- package/dist/registry/package/components/ColorModeToggle/index.tsx +44 -0
- package/dist/registry/package/components/CommandPalette/index.tsx +3 -2
- package/dist/registry/package/components/ConfirmDialog/index.tsx +24 -4
- package/dist/registry/package/components/CrudPage/dialogs.tsx +53 -8
- package/dist/registry/package/components/CrudPage/index.tsx +99 -25
- package/dist/registry/package/components/CurrencyInput/index.tsx +5 -2
- package/dist/registry/package/components/DataTable/index.tsx +53 -18
- package/dist/registry/package/components/DataTable/row-actions.tsx +20 -8
- package/dist/registry/package/components/DataTable/types.ts +100 -0
- package/dist/registry/package/components/DatePicker/index.tsx +14 -6
- package/dist/registry/package/components/DateRangeFilter/index.tsx +12 -3
- package/dist/registry/package/components/DateRangePicker/index.tsx +13 -4
- package/dist/registry/package/components/DateTimePicker/index.tsx +3 -1
- package/dist/registry/package/components/Drawer/index.tsx +16 -6
- package/dist/registry/package/components/DropdownMenu/index.tsx +37 -4
- package/dist/registry/package/components/DropdownMenu/styles.css +10 -0
- package/dist/registry/package/components/DurationDisplay/index.tsx +20 -2
- package/dist/registry/package/components/EntityMeta/index.tsx +14 -9
- package/dist/registry/package/components/ErrorState/index.tsx +9 -3
- package/dist/registry/package/components/ErrorState/styles.css +1 -1
- package/dist/registry/package/components/EventCalendar/index.tsx +657 -9
- package/dist/registry/package/components/EventCalendar/styles.css +360 -8
- package/dist/registry/package/components/EventManager/index.tsx +456 -0
- package/dist/registry/package/components/EventManager/styles.css +12 -0
- package/dist/registry/package/components/Field/index.tsx +74 -27
- package/dist/registry/package/components/FileUpload/index.tsx +13 -3
- package/dist/registry/package/components/FiltersBar/index.tsx +8 -3
- package/dist/registry/package/components/FiltersBar/styles.css +7 -0
- package/dist/registry/package/components/Form/index.tsx +118 -14
- package/dist/registry/package/components/Form/styles.css +3 -0
- package/dist/registry/package/components/HoverCard/index.tsx +8 -4
- package/dist/registry/package/components/Icons/index.tsx +18 -0
- package/dist/registry/package/components/ImageUpload/index.tsx +17 -2
- package/dist/registry/package/components/Input/index.tsx +3 -2
- package/dist/registry/package/components/JobProgress/index.tsx +289 -0
- package/dist/registry/package/components/JobProgress/styles.css +58 -0
- package/dist/registry/package/components/JsonEditor/index.tsx +18 -3
- package/dist/registry/package/components/JsonView/index.tsx +282 -0
- package/dist/registry/package/components/JsonView/styles.css +99 -0
- package/dist/registry/package/components/LiveIndicator/index.tsx +43 -0
- package/dist/registry/package/components/LiveIndicator/styles.css +41 -0
- package/dist/registry/package/components/LogStream/index.tsx +545 -0
- package/dist/registry/package/components/LogStream/styles.css +152 -0
- package/dist/registry/package/components/MarkdownEditor/index.tsx +14 -3
- package/dist/registry/package/components/Modal/index.tsx +7 -1
- package/dist/registry/package/components/MultiSelect/index.tsx +14 -3
- package/dist/registry/package/components/OtpInput/index.tsx +2 -1
- package/dist/registry/package/components/Pagination/index.tsx +13 -2
- package/dist/registry/package/components/PasswordInput/index.tsx +23 -9
- package/dist/registry/package/components/PhoneInput/index.tsx +66 -30
- package/dist/registry/package/components/Popover/index.tsx +19 -4
- package/dist/registry/package/components/QrCode/index.tsx +3 -2
- package/dist/registry/package/components/RadioGroup/styles.css +3 -0
- package/dist/registry/package/components/Rating/index.tsx +6 -2
- package/dist/registry/package/components/ReferenceSelect/index.tsx +8 -1
- package/dist/registry/package/components/RelativeTime/index.tsx +5 -1
- package/dist/registry/package/components/RequireSession/index.tsx +10 -4
- package/dist/registry/package/components/Resource/index.tsx +70 -17
- package/dist/registry/package/components/RichTextEditor/index.tsx +2 -1
- package/dist/registry/package/components/SearchInput/index.tsx +4 -1
- package/dist/registry/package/components/SecretFields/index.tsx +70 -17
- package/dist/registry/package/components/Select/index.tsx +34 -12
- package/dist/registry/package/components/Sidebar/index.tsx +22 -6
- package/dist/registry/package/components/Slider/index.tsx +26 -5
- package/dist/registry/package/components/Spinner/index.tsx +3 -2
- package/dist/registry/package/components/StatCard/index.tsx +11 -3
- package/dist/registry/package/components/Statistic/index.tsx +5 -1
- package/dist/registry/package/components/Steps/styles.css +2 -2
- package/dist/registry/package/components/TagsInput/index.tsx +2 -1
- package/dist/registry/package/components/TimePicker/index.tsx +10 -4
- package/dist/registry/package/components/Toast/index.tsx +20 -5
- package/dist/registry/package/components/Tour/index.tsx +2 -1
- package/dist/registry/package/components/Transfer/index.tsx +2 -1
- package/dist/registry/package/components/Tree/index.tsx +3 -2
- package/dist/registry/package/components/TreeSelect/index.tsx +17 -4
- package/dist/registry/package/components/UnsavedChangesGuard/index.tsx +34 -3
- package/dist/registry/registry.json +257 -75
- package/dist/registry/styles/sia-ui.css +23 -4
- 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
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
27
|
-
description
|
|
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
|
-
|
|
81
|
+
{browseLabel}
|
|
72
82
|
</button>
|
|
73
83
|
</div>
|
|
74
84
|
);
|