@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
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
.sia-error-state { display:grid; place-items:center; gap:.75rem; min-height:14rem; padding:2rem; text-align:center; border:1px solid color-mix(in srgb,var(--sia-danger) 35%,var(--sia-border)); border-radius:calc(var(--sia-radius) * 1.25); background:color-mix(in srgb,var(--sia-danger) 5%,var(--sia-background)); }
|
|
2
2
|
.sia-error-state--compact { min-height:8rem; padding:1.25rem; }
|
|
3
|
-
.sia-error-state__icon { display:grid; place-items:center; width:3rem; height:3rem; border-radius:50%; color
|
|
3
|
+
.sia-error-state__icon { display:grid; place-items:center; width:3rem; height:3rem; border-radius:50%; color:var(--sia-danger-foreground); background:var(--sia-danger); font-weight:800; }
|
|
4
4
|
.sia-error-state__title { color:var(--sia-foreground); }
|
|
5
5
|
.sia-error-state__description { max-width:32rem; margin:0; color:var(--sia-muted); line-height:1.5; }
|
|
6
6
|
.sia-error-state__actions { display:flex; gap:.75rem; flex-wrap:wrap; justify-content:center; }
|
|
@@ -1,15 +1,663 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
useEffect,
|
|
3
|
+
useMemo,
|
|
4
|
+
useRef,
|
|
5
|
+
useState,
|
|
6
|
+
type KeyboardEvent,
|
|
7
|
+
type MouseEvent,
|
|
8
|
+
type ReactNode,
|
|
9
|
+
} from "react";
|
|
10
|
+
import { cn } from "@sia-ui/utils";
|
|
11
|
+
import {
|
|
12
|
+
addDaysIso,
|
|
13
|
+
addMonthsIso,
|
|
14
|
+
dayToDate,
|
|
15
|
+
eventDays,
|
|
16
|
+
eventsByDay,
|
|
17
|
+
isoDay,
|
|
18
|
+
layoutDay,
|
|
19
|
+
monthGrid,
|
|
20
|
+
plural,
|
|
21
|
+
useSiaLocale,
|
|
22
|
+
visibleRange,
|
|
23
|
+
weekDays,
|
|
24
|
+
type CalendarEventInput,
|
|
25
|
+
type CalendarView,
|
|
26
|
+
type DayEvent,
|
|
27
|
+
type EventCalendarMessages,
|
|
28
|
+
} from "@sia-ui/headless";
|
|
2
29
|
import type { ComponentTone } from "@sia-ui/tokens";
|
|
30
|
+
import { Button, type ButtonProps } from "../Button";
|
|
31
|
+
import { EmptyState, type EmptyStateProps } from "../EmptyState";
|
|
32
|
+
import { IconButton, type IconButtonProps } from "../IconButton";
|
|
33
|
+
import { Popover, type PopoverProps } from "../Popover";
|
|
34
|
+
import { RadioButton, RadioGroup, type RadioButtonProps, type RadioGroupProps } from "../RadioGroup";
|
|
35
|
+
import { Spinner, type SpinnerProps } from "../Spinner";
|
|
36
|
+
import { ChevronLeftIcon, ChevronRightIcon, PlusIcon } from "../Icons";
|
|
3
37
|
import "./styles.css";
|
|
4
38
|
|
|
5
|
-
export
|
|
6
|
-
|
|
39
|
+
export type { CalendarView } from "@sia-ui/headless";
|
|
40
|
+
|
|
41
|
+
/** Un événement du calendrier. Les champs métier en plus passent tels quels. */
|
|
42
|
+
export interface CalendarEvent extends Omit<CalendarEventInput, "start"> {
|
|
43
|
+
/** `2026-09-30` pour la journée, `2026-09-30T14:30` pour une heure. */
|
|
44
|
+
start?: string | undefined;
|
|
45
|
+
/** @deprecated Ancien nom de `start`, gardé pour les projets existants. */
|
|
46
|
+
date?: string | undefined;
|
|
47
|
+
// `| undefined` partout : le brouillon rendu par `EventManager` s'étale
|
|
48
|
+
// dans un événement (`{ ...brouillon, id }`) sans transtypage.
|
|
49
|
+
tone?: ComponentTone | undefined;
|
|
50
|
+
description?: ReactNode | undefined;
|
|
51
|
+
location?: ReactNode | undefined;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Les libellés du calendrier : le groupe `eventCalendar` de la locale SIA.
|
|
56
|
+
* « +N autres » se décline en `more_one` / `more_other`, avec `{count}`.
|
|
57
|
+
*/
|
|
58
|
+
export type EventCalendarLabels = EventCalendarMessages;
|
|
59
|
+
|
|
60
|
+
export interface EventCalendarProps<T extends CalendarEvent = CalendarEvent> {
|
|
61
|
+
events: T[];
|
|
62
|
+
|
|
63
|
+
/** La vue affichée — mois, semaine, agenda. */
|
|
64
|
+
view?: CalendarView;
|
|
65
|
+
defaultView?: CalendarView;
|
|
66
|
+
onViewChange?: (view: CalendarView) => void;
|
|
67
|
+
/** Les vues proposées. Une seule masque le sélecteur. */
|
|
68
|
+
views?: CalendarView[];
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* L'habillage de la grille. `bordered` : des cases tracées, comme un
|
|
72
|
+
* agenda papier. `minimal` : aucune bordure intérieure, les jours se
|
|
73
|
+
* lisent à leur numéro. `soft` : des cases sur fond teinté, séparées par
|
|
74
|
+
* des blancs.
|
|
75
|
+
*/
|
|
76
|
+
variant?: "bordered" | "minimal" | "soft";
|
|
77
|
+
/** Le rendu des événements : fond léger, plein, ou contour. */
|
|
78
|
+
eventVariant?: "soft" | "solid" | "outline";
|
|
79
|
+
/** `compact` resserre les cases et la grille horaire. */
|
|
80
|
+
density?: "comfortable" | "compact";
|
|
81
|
+
/** L'heure affichée en haut de la grille horaire à l'ouverture. 8 h par défaut. */
|
|
82
|
+
scrollToHour?: number;
|
|
83
|
+
|
|
84
|
+
/** Un jour de la période affichée (`2026-09-30`). Aujourd'hui par défaut. */
|
|
85
|
+
date?: string;
|
|
86
|
+
defaultDate?: string;
|
|
87
|
+
onDateChange?: (date: string) => void;
|
|
88
|
+
/** @deprecated Ancien nom de `defaultDate`. */
|
|
89
|
+
defaultValue?: string;
|
|
90
|
+
/** @deprecated Ancien nom de `defaultDate`, au mois près. */
|
|
91
|
+
defaultMonth?: string;
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* La période visible, bornes incluses, à chaque changement de vue ou de
|
|
95
|
+
* période — et au premier affichage. C'est elle qu'on demande au serveur.
|
|
96
|
+
*/
|
|
97
|
+
onRangeChange?: (range: { start: string; end: string }, view: CalendarView) => void;
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Un clic sur un jour, hors événement : le moment de créer. Dans la
|
|
101
|
+
* grille horaire, `time` donne la demi-heure visée (`14:30`).
|
|
102
|
+
*/
|
|
103
|
+
onDateClick?: (date: string, time?: string) => void;
|
|
104
|
+
onEventClick?: (event: T) => void;
|
|
105
|
+
|
|
106
|
+
/** Événements visibles par case en vue mois ; le reste passe dans « +N ». */
|
|
107
|
+
maxEventsPerDay?: number;
|
|
108
|
+
firstDayOfWeek?: 0 | 1;
|
|
109
|
+
/** La langue des dates. Par défaut, celle de la locale SIA. */
|
|
110
|
+
locale?: string;
|
|
111
|
+
/** Remplace le contenu d'un événement. Le bouton qui l'entoure reste. */
|
|
112
|
+
renderEvent?: (event: T, placement: DayEvent<T & CalendarEventInput>) => ReactNode;
|
|
113
|
+
/** Actions à droite de la barre : « Nouvel événement », un filtre. */
|
|
114
|
+
toolbarActions?: ReactNode;
|
|
115
|
+
loading?: boolean;
|
|
116
|
+
/** Remplace, clé par clé, les textes du groupe `eventCalendar` de la locale. */
|
|
117
|
+
labels?: Partial<EventCalendarLabels>;
|
|
118
|
+
|
|
119
|
+
/** Le bouton « Aujourd'hui ». */
|
|
120
|
+
toolbarButtonProps?: Partial<Omit<ButtonProps, "onClick" | "children">>;
|
|
121
|
+
/** Le sélecteur de vue — un `RadioGroup` en boutons. */
|
|
122
|
+
viewSwitchProps?: Partial<Omit<RadioGroupProps, "value" | "defaultValue" | "onValueChange" | "children">>;
|
|
123
|
+
/** Chaque bouton de vue. */
|
|
124
|
+
viewButtonProps?: Partial<Omit<RadioButtonProps, "value" | "children">>;
|
|
125
|
+
/** Le bouton « +N autres » d'une case chargée. */
|
|
126
|
+
moreButtonProps?: Partial<Omit<ButtonProps, "onClick" | "children">>;
|
|
127
|
+
/** L'agenda vide. */
|
|
128
|
+
emptyStateProps?: Partial<EmptyStateProps>;
|
|
129
|
+
/** Le témoin de chargement de la barre. */
|
|
130
|
+
spinnerProps?: Partial<Omit<SpinnerProps, "label">>;
|
|
131
|
+
/** Les flèches de période. */
|
|
132
|
+
navButtonProps?: Partial<Omit<IconButtonProps, "onClick" | "label" | "icon">>;
|
|
133
|
+
/** La liste complète d'un jour chargé (« +N autres »). */
|
|
134
|
+
popoverProps?: Partial<Omit<PopoverProps, "content" | "children">>;
|
|
135
|
+
className?: string;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/** Les événements d'hier, écrits `date` au lieu de `start`. */
|
|
139
|
+
function normaliser<T extends CalendarEvent>(events: T[]): Array<T & CalendarEventInput> {
|
|
140
|
+
return events
|
|
141
|
+
.filter((e) => e.start ?? e.date)
|
|
142
|
+
.map((e) => (e.start ? e : { ...e, start: e.date! })) as Array<T & CalendarEventInput>;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/** La largeur sous laquelle la grille passe en points et liste le jour. */
|
|
146
|
+
const SEUIL_COMPACT = 640;
|
|
147
|
+
|
|
7
148
|
/**
|
|
8
|
-
* Un calendrier
|
|
149
|
+
* Un calendrier d'événements : mois, semaine, agenda.
|
|
9
150
|
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
151
|
+
* Il affiche et signale ; il ne gère pas. Un clic sur un jour appelle
|
|
152
|
+
* `onDateClick`, un clic sur un événement `onEventClick` — `EventManager`
|
|
153
|
+
* s'y branche pour créer, consulter, modifier et supprimer. Les données
|
|
154
|
+
* viennent du serveur : `onRangeChange` donne la période à charger.
|
|
155
|
+
*
|
|
156
|
+
* Étroit, la grille du mois n'affiche plus que des points de couleur, et les
|
|
157
|
+
* événements du jour choisi s'affichent dessous : une case de 45 px ne se lit
|
|
158
|
+
* pas, une liste si.
|
|
13
159
|
*/
|
|
14
|
-
export function EventCalendar
|
|
15
|
-
|
|
160
|
+
export function EventCalendar<T extends CalendarEvent = CalendarEvent>({
|
|
161
|
+
events,
|
|
162
|
+
view: viewProp,
|
|
163
|
+
defaultView = "month",
|
|
164
|
+
onViewChange,
|
|
165
|
+
views = ["month", "week", "day", "agenda"],
|
|
166
|
+
variant = "bordered",
|
|
167
|
+
eventVariant = "soft",
|
|
168
|
+
density = "comfortable",
|
|
169
|
+
scrollToHour = 8,
|
|
170
|
+
date: dateProp,
|
|
171
|
+
defaultDate,
|
|
172
|
+
defaultValue,
|
|
173
|
+
defaultMonth,
|
|
174
|
+
onDateChange,
|
|
175
|
+
onRangeChange,
|
|
176
|
+
onDateClick,
|
|
177
|
+
onEventClick,
|
|
178
|
+
maxEventsPerDay = 3,
|
|
179
|
+
firstDayOfWeek = 1,
|
|
180
|
+
locale: localeProp,
|
|
181
|
+
renderEvent,
|
|
182
|
+
toolbarActions,
|
|
183
|
+
loading = false,
|
|
184
|
+
labels: labelsProp,
|
|
185
|
+
toolbarButtonProps,
|
|
186
|
+
viewSwitchProps,
|
|
187
|
+
viewButtonProps,
|
|
188
|
+
moreButtonProps,
|
|
189
|
+
emptyStateProps,
|
|
190
|
+
spinnerProps,
|
|
191
|
+
navButtonProps,
|
|
192
|
+
popoverProps,
|
|
193
|
+
className,
|
|
194
|
+
}: EventCalendarProps<T>) {
|
|
195
|
+
const messages = useSiaLocale();
|
|
196
|
+
const locale = localeProp ?? messages.language;
|
|
197
|
+
const labels = { ...messages.eventCalendar, ...labelsProp };
|
|
198
|
+
const aujourdhui = isoDay(new Date());
|
|
199
|
+
|
|
200
|
+
const [vueInterne, setVueInterne] = useState<CalendarView>(defaultView);
|
|
201
|
+
const vue = viewProp ?? vueInterne;
|
|
202
|
+
const [ancreInterne, setAncreInterne] = useState(
|
|
203
|
+
() =>
|
|
204
|
+
defaultDate ??
|
|
205
|
+
defaultValue ??
|
|
206
|
+
(defaultMonth ? `${defaultMonth.slice(0, 7)}-01` : aujourdhui),
|
|
207
|
+
);
|
|
208
|
+
const ancre = dateProp ?? ancreInterne;
|
|
209
|
+
const [jourChoisi, setJourChoisi] = useState(ancre);
|
|
210
|
+
|
|
211
|
+
const racine = useRef<HTMLDivElement>(null);
|
|
212
|
+
const [compact, setCompact] = useState(false);
|
|
213
|
+
useEffect(() => {
|
|
214
|
+
const noeud = racine.current;
|
|
215
|
+
if (!noeud || typeof ResizeObserver === "undefined") return undefined;
|
|
216
|
+
const observateur = new ResizeObserver(([entree]) =>
|
|
217
|
+
setCompact((entree?.contentRect.width ?? SEUIL_COMPACT) < SEUIL_COMPACT),
|
|
218
|
+
);
|
|
219
|
+
observateur.observe(noeud);
|
|
220
|
+
return () => observateur.disconnect();
|
|
221
|
+
}, []);
|
|
222
|
+
|
|
223
|
+
const changerVue = (suivante: CalendarView) => {
|
|
224
|
+
if (viewProp === undefined) setVueInterne(suivante);
|
|
225
|
+
onViewChange?.(suivante);
|
|
226
|
+
};
|
|
227
|
+
const changerAncre = (suivante: string) => {
|
|
228
|
+
if (dateProp === undefined) setAncreInterne(suivante);
|
|
229
|
+
setJourChoisi(suivante);
|
|
230
|
+
onDateChange?.(suivante);
|
|
231
|
+
};
|
|
232
|
+
|
|
233
|
+
const periode = useMemo(
|
|
234
|
+
() => visibleRange(vue, ancre, firstDayOfWeek),
|
|
235
|
+
[ancre, firstDayOfWeek, vue],
|
|
236
|
+
);
|
|
237
|
+
|
|
238
|
+
// La période à charger, à chaque changement et au premier affichage.
|
|
239
|
+
const annoncer = useRef(onRangeChange);
|
|
240
|
+
annoncer.current = onRangeChange;
|
|
241
|
+
useEffect(() => {
|
|
242
|
+
annoncer.current?.(periode, vue);
|
|
243
|
+
}, [periode.start, periode.end, vue]); // eslint-disable-line react-hooks/exhaustive-deps
|
|
244
|
+
|
|
245
|
+
const normalises = useMemo(() => normaliser(events), [events]);
|
|
246
|
+
|
|
247
|
+
const jours = useMemo(() => {
|
|
248
|
+
if (vue === "week") return weekDays(ancre, firstDayOfWeek);
|
|
249
|
+
if (vue === "day") return [ancre];
|
|
250
|
+
if (vue === "month") return monthGrid(ancre, firstDayOfWeek);
|
|
251
|
+
const liste: string[] = [];
|
|
252
|
+
for (let j = periode.start; j <= periode.end; j = addDaysIso(j, 1)) liste.push(j);
|
|
253
|
+
return liste;
|
|
254
|
+
}, [ancre, firstDayOfWeek, periode.end, periode.start, vue]);
|
|
255
|
+
|
|
256
|
+
const parJour = useMemo(() => eventsByDay(normalises, jours), [jours, normalises]);
|
|
257
|
+
|
|
258
|
+
const fmt = (options: Intl.DateTimeFormatOptions) => new Intl.DateTimeFormat(locale, options);
|
|
259
|
+
const titre =
|
|
260
|
+
vue === "week"
|
|
261
|
+
? formatSemaine(jours[0]!, jours[6]!, locale)
|
|
262
|
+
: vue === "day"
|
|
263
|
+
? capitaliser(fmt({ weekday: "long", day: "numeric", month: "long", year: "numeric" }).format(dayToDate(ancre)))
|
|
264
|
+
: capitaliser(fmt({ month: "long", year: "numeric" }).format(dayToDate(ancre)));
|
|
265
|
+
|
|
266
|
+
const deplacer = (sens: 1 | -1) =>
|
|
267
|
+
changerAncre(
|
|
268
|
+
vue === "week"
|
|
269
|
+
? addDaysIso(ancre, 7 * sens)
|
|
270
|
+
: vue === "day"
|
|
271
|
+
? addDaysIso(ancre, sens)
|
|
272
|
+
: addMonthsIso(ancre, sens),
|
|
273
|
+
);
|
|
274
|
+
|
|
275
|
+
const nomsDesJours = weekDays("2026-09-28", firstDayOfWeek).map((j) =>
|
|
276
|
+
capitaliser(fmt({ weekday: "short" }).format(dayToDate(j)).replace(".", "")),
|
|
277
|
+
);
|
|
278
|
+
|
|
279
|
+
const cliquerJour = (jour: string, evenement: MouseEvent) => {
|
|
280
|
+
// Seul le fond de la case compte : un clic sur un événement ou sur « +N »
|
|
281
|
+
// a son propre sens.
|
|
282
|
+
if ((evenement.target as HTMLElement).closest("button")) return;
|
|
283
|
+
if (compact) setJourChoisi(jour);
|
|
284
|
+
else onDateClick?.(jour);
|
|
285
|
+
};
|
|
286
|
+
|
|
287
|
+
const puce = (placement: DayEvent<T & CalendarEventInput>) => {
|
|
288
|
+
const { event } = placement;
|
|
289
|
+
const barre = event.allDay || placement.continuesBefore || placement.continuesAfter || !placement.time;
|
|
290
|
+
return (
|
|
291
|
+
<button
|
|
292
|
+
type="button"
|
|
293
|
+
key={event.id}
|
|
294
|
+
className={cn(
|
|
295
|
+
"sia-ec__event",
|
|
296
|
+
`sia-ec__event--${event.tone ?? "primary"}`,
|
|
297
|
+
barre ? "sia-ec__event--bar" : "sia-ec__event--timed",
|
|
298
|
+
placement.continuesBefore && "sia-ec__event--continues-before",
|
|
299
|
+
placement.continuesAfter && "sia-ec__event--continues-after",
|
|
300
|
+
)}
|
|
301
|
+
// Le titre entier au survol : dans une case, il est souvent coupé.
|
|
302
|
+
title={placement.time ? `${placement.time} · ${event.title}` : event.title}
|
|
303
|
+
onClick={() => onEventClick?.(event)}
|
|
304
|
+
>
|
|
305
|
+
{renderEvent ? (
|
|
306
|
+
renderEvent(event, placement)
|
|
307
|
+
) : (
|
|
308
|
+
<>
|
|
309
|
+
{!barre && <span className="sia-ec__dot" aria-hidden="true" />}
|
|
310
|
+
{placement.time && <span className="sia-ec__time">{placement.time}</span>}
|
|
311
|
+
<span className="sia-ec__title">{event.title}</span>
|
|
312
|
+
</>
|
|
313
|
+
)}
|
|
314
|
+
</button>
|
|
315
|
+
);
|
|
316
|
+
};
|
|
317
|
+
|
|
318
|
+
const libelleJour = (jour: string) =>
|
|
319
|
+
capitaliser(fmt({ weekday: "long", day: "numeric", month: "long" }).format(dayToDate(jour)));
|
|
320
|
+
|
|
321
|
+
const listeDuJour = (jour: string) => {
|
|
322
|
+
const liste = parJour.get(jour) ?? [];
|
|
323
|
+
return liste.length === 0 ? (
|
|
324
|
+
<p className="sia-ec__empty-day">{labels.emptyDay}</p>
|
|
325
|
+
) : (
|
|
326
|
+
<div className="sia-ec__list">{liste.map(puce)}</div>
|
|
327
|
+
);
|
|
328
|
+
};
|
|
329
|
+
|
|
330
|
+
const moisCourant = ancre.slice(0, 7);
|
|
331
|
+
|
|
332
|
+
const toucheCase = (jour: string) => (event: KeyboardEvent) => {
|
|
333
|
+
if (event.target !== event.currentTarget) return;
|
|
334
|
+
if (event.key === "Enter" || event.key === " ") {
|
|
335
|
+
event.preventDefault();
|
|
336
|
+
if (compact) setJourChoisi(jour);
|
|
337
|
+
else onDateClick?.(jour);
|
|
338
|
+
}
|
|
339
|
+
};
|
|
340
|
+
|
|
341
|
+
const grilleMois = (
|
|
342
|
+
<div className="sia-ec__month" role="grid" aria-label={titre}>
|
|
343
|
+
<div className="sia-ec__weekdays" role="row">
|
|
344
|
+
{nomsDesJours.map((nom) => (
|
|
345
|
+
<span key={nom} role="columnheader" className="sia-ec__weekday">
|
|
346
|
+
{nom}
|
|
347
|
+
</span>
|
|
348
|
+
))}
|
|
349
|
+
</div>
|
|
350
|
+
{Array.from({ length: 6 }, (_, semaine) => (
|
|
351
|
+
<div key={semaine} className="sia-ec__week" role="row">
|
|
352
|
+
{jours.slice(semaine * 7, semaine * 7 + 7).map((jour) => {
|
|
353
|
+
const liste = parJour.get(jour) ?? [];
|
|
354
|
+
const visibles = liste.slice(0, maxEventsPerDay);
|
|
355
|
+
const caches = liste.length - visibles.length;
|
|
356
|
+
const jourSemaine = dayToDate(jour).getDay();
|
|
357
|
+
return (
|
|
358
|
+
<div
|
|
359
|
+
key={jour}
|
|
360
|
+
role="gridcell"
|
|
361
|
+
tabIndex={0}
|
|
362
|
+
aria-label={`${libelleJour(jour)}${liste.length ? `, ${plural(labels, "dayEvents", liste.length, locale)}` : ""}`}
|
|
363
|
+
aria-selected={compact ? jour === jourChoisi : undefined}
|
|
364
|
+
className={cn(
|
|
365
|
+
"sia-ec__day",
|
|
366
|
+
jour.slice(0, 7) !== moisCourant && "sia-ec__day--outside",
|
|
367
|
+
jour === aujourdhui && "sia-ec__day--today",
|
|
368
|
+
(jourSemaine === 0 || jourSemaine === 6) && "sia-ec__day--weekend",
|
|
369
|
+
compact && jour === jourChoisi && "sia-ec__day--selected",
|
|
370
|
+
)}
|
|
371
|
+
onClick={(event) => cliquerJour(jour, event)}
|
|
372
|
+
onKeyDown={toucheCase(jour)}
|
|
373
|
+
>
|
|
374
|
+
<span className="sia-ec__day-number">{Number(jour.slice(8))}</span>
|
|
375
|
+
{compact ? (
|
|
376
|
+
<span className="sia-ec__dots" aria-hidden="true">
|
|
377
|
+
{liste.slice(0, 3).map(({ event }) => (
|
|
378
|
+
<span key={event.id} className={cn("sia-ec__dot", `sia-ec__dot--${event.tone ?? "primary"}`)} />
|
|
379
|
+
))}
|
|
380
|
+
</span>
|
|
381
|
+
) : (
|
|
382
|
+
<div className="sia-ec__events">
|
|
383
|
+
{visibles.map(puce)}
|
|
384
|
+
{caches > 0 && (
|
|
385
|
+
<Popover
|
|
386
|
+
placement="bottom-start"
|
|
387
|
+
title={libelleJour(jour)}
|
|
388
|
+
{...popoverProps}
|
|
389
|
+
content={<div className="sia-ec__list">{liste.map(puce)}</div>}
|
|
390
|
+
>
|
|
391
|
+
<Button
|
|
392
|
+
variant="ghost"
|
|
393
|
+
size="sm"
|
|
394
|
+
{...moreButtonProps}
|
|
395
|
+
className={cn("sia-ec__more", moreButtonProps?.className)}
|
|
396
|
+
>
|
|
397
|
+
{plural(labels, "more", caches, locale)}
|
|
398
|
+
</Button>
|
|
399
|
+
</Popover>
|
|
400
|
+
)}
|
|
401
|
+
</div>
|
|
402
|
+
)}
|
|
403
|
+
</div>
|
|
404
|
+
);
|
|
405
|
+
})}
|
|
406
|
+
</div>
|
|
407
|
+
))}
|
|
408
|
+
</div>
|
|
409
|
+
);
|
|
410
|
+
|
|
411
|
+
// L'heure qui passe, pour la ligne rouge d'aujourd'hui.
|
|
412
|
+
const [maintenant, setMaintenant] = useState(() => {
|
|
413
|
+
const d = new Date();
|
|
414
|
+
return d.getHours() * 60 + d.getMinutes();
|
|
415
|
+
});
|
|
416
|
+
const grilleVisible = vue === "week" || vue === "day";
|
|
417
|
+
useEffect(() => {
|
|
418
|
+
if (!grilleVisible) return undefined;
|
|
419
|
+
const minuteur = window.setInterval(() => {
|
|
420
|
+
const d = new Date();
|
|
421
|
+
setMaintenant(d.getHours() * 60 + d.getMinutes());
|
|
422
|
+
}, 60_000);
|
|
423
|
+
return () => window.clearInterval(minuteur);
|
|
424
|
+
}, [grilleVisible]);
|
|
425
|
+
|
|
426
|
+
// À l'ouverture, la grille se place sur le début de journée plutôt que sur
|
|
427
|
+
// minuit : personne ne travaille à 1 h du matin.
|
|
428
|
+
const corps = useRef<HTMLDivElement>(null);
|
|
429
|
+
useEffect(() => {
|
|
430
|
+
const noeud = corps.current;
|
|
431
|
+
if (!noeud) return;
|
|
432
|
+
const heure = noeud.querySelector<HTMLElement>(".sia-ec__hour");
|
|
433
|
+
const hauteur = heure?.offsetHeight ?? 48;
|
|
434
|
+
noeud.scrollTop = scrollToHour * hauteur;
|
|
435
|
+
}, [grilleVisible, scrollToHour]);
|
|
436
|
+
|
|
437
|
+
/** La demi-heure visée par un clic dans une colonne. */
|
|
438
|
+
const cliquerCreneau = (jour: string, evenement: MouseEvent<HTMLDivElement>) => {
|
|
439
|
+
if ((evenement.target as HTMLElement).closest("button")) return;
|
|
440
|
+
const cadre = evenement.currentTarget.getBoundingClientRect();
|
|
441
|
+
const minutes = ((evenement.clientY - cadre.top) / cadre.height) * 24 * 60;
|
|
442
|
+
const creneau = Math.max(0, Math.min(23 * 60 + 30, Math.floor(minutes / 30) * 30));
|
|
443
|
+
onDateClick?.(jour, `${String(Math.floor(creneau / 60)).padStart(2, "0")}:${String(creneau % 60).padStart(2, "0")}`);
|
|
444
|
+
};
|
|
445
|
+
|
|
446
|
+
const heures = Array.from({ length: 24 }, (_, h) => h);
|
|
447
|
+
const grilleHoraire = (
|
|
448
|
+
<div
|
|
449
|
+
className={cn("sia-ec__timegrid", vue === "day" && "sia-ec__timegrid--day")}
|
|
450
|
+
style={{ ["--sia-ec-days" as string]: jours.length }}
|
|
451
|
+
>
|
|
452
|
+
<div className="sia-ec__tg-head">
|
|
453
|
+
<span className="sia-ec__tg-gutter" aria-hidden="true" />
|
|
454
|
+
{jours.map((jour) => (
|
|
455
|
+
<button
|
|
456
|
+
type="button"
|
|
457
|
+
key={jour}
|
|
458
|
+
className={cn("sia-ec__tg-dayhead", jour === aujourdhui && "sia-ec__tg-dayhead--today")}
|
|
459
|
+
onClick={() => {
|
|
460
|
+
changerAncre(jour);
|
|
461
|
+
if (vue === "week" && views.includes("day")) changerVue("day");
|
|
462
|
+
}}
|
|
463
|
+
aria-label={libelleJour(jour)}
|
|
464
|
+
>
|
|
465
|
+
<span className="sia-ec__tg-weekday">
|
|
466
|
+
{capitaliser(fmt({ weekday: "short" }).format(dayToDate(jour)).replace(".", ""))}
|
|
467
|
+
</span>
|
|
468
|
+
<span className="sia-ec__tg-daynum">{Number(jour.slice(8))}</span>
|
|
469
|
+
</button>
|
|
470
|
+
))}
|
|
471
|
+
</div>
|
|
472
|
+
|
|
473
|
+
<div className="sia-ec__tg-allday">
|
|
474
|
+
<span className="sia-ec__tg-gutter">{labels.allDay}</span>
|
|
475
|
+
{jours.map((jour) => (
|
|
476
|
+
<div key={jour} className="sia-ec__tg-allday-cell">
|
|
477
|
+
{(parJour.get(jour) ?? [])
|
|
478
|
+
.filter((p) => p.event.allDay || p.continuesBefore || p.continuesAfter || !p.time)
|
|
479
|
+
.map(puce)}
|
|
480
|
+
</div>
|
|
481
|
+
))}
|
|
482
|
+
</div>
|
|
483
|
+
|
|
484
|
+
<div className="sia-ec__tg-body" ref={corps}>
|
|
485
|
+
<div className="sia-ec__tg-hours" aria-hidden="true">
|
|
486
|
+
{heures.map((h) => (
|
|
487
|
+
<span key={h} className="sia-ec__hour">
|
|
488
|
+
{h === 0 ? "" : `${String(h).padStart(2, "0")}:00`}
|
|
489
|
+
</span>
|
|
490
|
+
))}
|
|
491
|
+
</div>
|
|
492
|
+
{jours.map((jour) => (
|
|
493
|
+
<div
|
|
494
|
+
key={jour}
|
|
495
|
+
className={cn("sia-ec__tg-col", jour === aujourdhui && "sia-ec__tg-col--today")}
|
|
496
|
+
onClick={(evenement) => cliquerCreneau(jour, evenement)}
|
|
497
|
+
aria-label={libelleJour(jour)}
|
|
498
|
+
role="group"
|
|
499
|
+
>
|
|
500
|
+
{layoutDay(parJour.get(jour) ?? []).map((p) => (
|
|
501
|
+
<button
|
|
502
|
+
type="button"
|
|
503
|
+
key={p.event.id}
|
|
504
|
+
className={cn("sia-ec__slot", `sia-ec__event--${p.event.tone ?? "primary"}`)}
|
|
505
|
+
style={{
|
|
506
|
+
top: `${(p.start / 1440) * 100}%`,
|
|
507
|
+
height: `${((p.end - p.start) / 1440) * 100}%`,
|
|
508
|
+
left: `calc(${(p.column / p.columns) * 100}% + 2px)`,
|
|
509
|
+
width: `calc(${100 / p.columns}% - 4px)`,
|
|
510
|
+
}}
|
|
511
|
+
onClick={() => onEventClick?.(p.event)}
|
|
512
|
+
>
|
|
513
|
+
{renderEvent ? (
|
|
514
|
+
renderEvent(p.event, { event: p.event, continuesBefore: false, continuesAfter: false, time: formatMinutes(p.start) })
|
|
515
|
+
) : (
|
|
516
|
+
<>
|
|
517
|
+
<span className="sia-ec__slot-title">{p.event.title}</span>
|
|
518
|
+
<span className="sia-ec__slot-time">
|
|
519
|
+
{formatMinutes(p.start)} – {formatMinutes(p.end)}
|
|
520
|
+
</span>
|
|
521
|
+
</>
|
|
522
|
+
)}
|
|
523
|
+
</button>
|
|
524
|
+
))}
|
|
525
|
+
{jour === aujourdhui && (
|
|
526
|
+
<span
|
|
527
|
+
className="sia-ec__now"
|
|
528
|
+
style={{ top: `${(maintenant / 1440) * 100}%` }}
|
|
529
|
+
aria-hidden="true"
|
|
530
|
+
/>
|
|
531
|
+
)}
|
|
532
|
+
</div>
|
|
533
|
+
))}
|
|
534
|
+
</div>
|
|
535
|
+
</div>
|
|
536
|
+
);
|
|
537
|
+
|
|
538
|
+
const joursAvecEvenements = jours.filter((jour) => (parJour.get(jour) ?? []).length > 0);
|
|
539
|
+
const agenda = (
|
|
540
|
+
<div className="sia-ec__agenda">
|
|
541
|
+
{joursAvecEvenements.length === 0 ? (
|
|
542
|
+
<EmptyState compact title={labels.empty} {...emptyStateProps} />
|
|
543
|
+
) : (
|
|
544
|
+
joursAvecEvenements.map((jour) => (
|
|
545
|
+
<section key={jour} className="sia-ec__agenda-day">
|
|
546
|
+
<h3 className={cn("sia-ec__agenda-date", jour === aujourdhui && "sia-ec__agenda-date--today")}>
|
|
547
|
+
{libelleJour(jour)}
|
|
548
|
+
</h3>
|
|
549
|
+
<div className="sia-ec__list">
|
|
550
|
+
{(parJour.get(jour) ?? []).map((placement) => (
|
|
551
|
+
<div key={placement.event.id} className="sia-ec__agenda-row">
|
|
552
|
+
{puce(placement)}
|
|
553
|
+
{placement.event.location && (
|
|
554
|
+
<span className="sia-ec__location">{placement.event.location}</span>
|
|
555
|
+
)}
|
|
556
|
+
</div>
|
|
557
|
+
))}
|
|
558
|
+
</div>
|
|
559
|
+
</section>
|
|
560
|
+
))
|
|
561
|
+
)}
|
|
562
|
+
</div>
|
|
563
|
+
);
|
|
564
|
+
|
|
565
|
+
return (
|
|
566
|
+
<div
|
|
567
|
+
ref={racine}
|
|
568
|
+
className={cn(
|
|
569
|
+
"sia-ec",
|
|
570
|
+
`sia-ec--${variant}`,
|
|
571
|
+
`sia-ec--events-${eventVariant}`,
|
|
572
|
+
density === "compact" && "sia-ec--dense",
|
|
573
|
+
compact && "sia-ec--compact",
|
|
574
|
+
loading && "sia-ec--loading",
|
|
575
|
+
className,
|
|
576
|
+
)}
|
|
577
|
+
aria-busy={loading || undefined}
|
|
578
|
+
>
|
|
579
|
+
<div className="sia-ec__toolbar">
|
|
580
|
+
<div className="sia-ec__nav">
|
|
581
|
+
<Button
|
|
582
|
+
variant="outline"
|
|
583
|
+
size="sm"
|
|
584
|
+
{...toolbarButtonProps}
|
|
585
|
+
onClick={() => changerAncre(aujourdhui)}
|
|
586
|
+
>
|
|
587
|
+
{labels.today}
|
|
588
|
+
</Button>
|
|
589
|
+
<IconButton variant="ghost" size="sm" {...navButtonProps} label={labels.previous} icon={<ChevronLeftIcon />} onClick={() => deplacer(-1)} />
|
|
590
|
+
<IconButton variant="ghost" size="sm" {...navButtonProps} label={labels.next} icon={<ChevronRightIcon />} onClick={() => deplacer(1)} />
|
|
591
|
+
<h2 className="sia-ec__period" aria-live="polite">
|
|
592
|
+
{titre}
|
|
593
|
+
</h2>
|
|
594
|
+
{loading && <Spinner size="sm" {...spinnerProps} label={labels.loading} />}
|
|
595
|
+
</div>
|
|
596
|
+
<div className="sia-ec__tools">
|
|
597
|
+
{toolbarActions}
|
|
598
|
+
{views.length > 1 && (
|
|
599
|
+
<RadioGroup
|
|
600
|
+
orientation="horizontal"
|
|
601
|
+
size="sm"
|
|
602
|
+
label={<span className="sia-visually-hidden">{labels.viewSwitch}</span>}
|
|
603
|
+
{...viewSwitchProps}
|
|
604
|
+
className={cn("sia-ec__views", viewSwitchProps?.className)}
|
|
605
|
+
value={vue}
|
|
606
|
+
onValueChange={(v) => changerVue(v as CalendarView)}
|
|
607
|
+
>
|
|
608
|
+
{views.map((v) => (
|
|
609
|
+
<RadioButton key={v} {...viewButtonProps} value={v}>
|
|
610
|
+
{labels[v]}
|
|
611
|
+
</RadioButton>
|
|
612
|
+
))}
|
|
613
|
+
</RadioGroup>
|
|
614
|
+
)}
|
|
615
|
+
</div>
|
|
616
|
+
</div>
|
|
617
|
+
|
|
618
|
+
{vue === "month" ? grilleMois : grilleVisible ? grilleHoraire : agenda}
|
|
619
|
+
|
|
620
|
+
{compact && vue === "month" && (
|
|
621
|
+
<section className="sia-ec__selected" aria-live="polite">
|
|
622
|
+
<header className="sia-ec__selected-head">
|
|
623
|
+
<h3>{libelleJour(jourChoisi)}</h3>
|
|
624
|
+
{onDateClick && (
|
|
625
|
+
<IconButton
|
|
626
|
+
variant="outline"
|
|
627
|
+
size="sm"
|
|
628
|
+
{...navButtonProps}
|
|
629
|
+
label={labels.add}
|
|
630
|
+
icon={<PlusIcon />}
|
|
631
|
+
onClick={() => onDateClick(jourChoisi)}
|
|
632
|
+
/>
|
|
633
|
+
)}
|
|
634
|
+
</header>
|
|
635
|
+
{listeDuJour(jourChoisi)}
|
|
636
|
+
</section>
|
|
637
|
+
)}
|
|
638
|
+
</div>
|
|
639
|
+
);
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
/** 870 → « 14:30 ». */
|
|
643
|
+
function formatMinutes(minutes: number) {
|
|
644
|
+
const bornees = Math.min(minutes, 24 * 60 - 1);
|
|
645
|
+
return `${String(Math.floor(bornees / 60)).padStart(2, "0")}:${String(bornees % 60).padStart(2, "0")}`;
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
function capitaliser(texte: string) {
|
|
649
|
+
return texte.charAt(0).toUpperCase() + texte.slice(1);
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
/** « 28 sept. – 4 oct. 2026 », sans répéter le mois ni l'année s'ils ne changent pas. */
|
|
653
|
+
function formatSemaine(debut: string, fin: string, locale: string) {
|
|
654
|
+
const d = dayToDate(debut);
|
|
655
|
+
const f = dayToDate(fin);
|
|
656
|
+
const memeMois = debut.slice(0, 7) === fin.slice(0, 7);
|
|
657
|
+
const gauche = new Intl.DateTimeFormat(locale, memeMois ? { day: "numeric" } : { day: "numeric", month: "short" }).format(d);
|
|
658
|
+
const droite = new Intl.DateTimeFormat(locale, { day: "numeric", month: "short", year: "numeric" }).format(f);
|
|
659
|
+
return `${gauche} – ${droite}`;
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
/** Les jours qu'un événement occupe — exporté pour qui compose sa propre vue. */
|
|
663
|
+
export { eventDays };
|