@sia-ui/cli 0.7.0 → 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 +12 -0
- package/README.md +12 -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 +44 -12
- package/dist/registry/package/components/Select/styles.css +6 -3
- 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,15 @@
|
|
|
1
1
|
import type { Key, ReactNode } from "react";
|
|
2
2
|
import type { PermissionRule } from "@sia-ui/headless";
|
|
3
3
|
import type { ComponentTone } from "@sia-ui/tokens";
|
|
4
|
+
import type { SearchInputProps } from "../SearchInput";
|
|
5
|
+
import type { EmptyStateProps } from "../EmptyState";
|
|
6
|
+
import type { ErrorStateProps } from "../ErrorState";
|
|
7
|
+
import type { SkeletonProps } from "../Skeleton";
|
|
8
|
+
import type { CheckboxProps } from "../Checkbox";
|
|
9
|
+
import type { ButtonProps } from "../Button";
|
|
10
|
+
import type { IconButtonProps } from "../IconButton";
|
|
11
|
+
import type { DropdownMenuProps } from "../DropdownMenu";
|
|
12
|
+
import type { ConfirmDialogProps } from "../ConfirmDialog";
|
|
4
13
|
|
|
5
14
|
/** Liste dense, ou grille de cartes. */
|
|
6
15
|
export type DataTableView = "table" | "cards";
|
|
@@ -115,6 +124,18 @@ export interface DataTableProps<T> {
|
|
|
115
124
|
* lettre ; Entrée et l'effacement déclenchent sans attendre.
|
|
116
125
|
*/
|
|
117
126
|
searchDelay?: number;
|
|
127
|
+
/**
|
|
128
|
+
* Le champ de recherche : placeholder, libellés, `clearable`, classe.
|
|
129
|
+
*
|
|
130
|
+
* La valeur et les rappels restent au tableau — c'est lui qui filtre et
|
|
131
|
+
* qui temporise ; les déléguer ici ferait deux recherches concurrentes.
|
|
132
|
+
*/
|
|
133
|
+
searchInputProps?: Partial<
|
|
134
|
+
Omit<
|
|
135
|
+
SearchInputProps,
|
|
136
|
+
"value" | "defaultValue" | "onValueChange" | "onSearch" | "searchDelay"
|
|
137
|
+
>
|
|
138
|
+
>;
|
|
118
139
|
|
|
119
140
|
/* ─────────────────────────────────────────────────────── mise en page */
|
|
120
141
|
|
|
@@ -135,6 +156,40 @@ export interface DataTableProps<T> {
|
|
|
135
156
|
actionsHeader?: ReactNode;
|
|
136
157
|
/** `icon` garde la colonne étroite sur un tableau déjà large. */
|
|
137
158
|
actionsDisplay?: "icon" | "label";
|
|
159
|
+
/**
|
|
160
|
+
* Les boutons d'action à libellé : variante, taille, classe.
|
|
161
|
+
*
|
|
162
|
+
* Le clic, le libellé et l'état désactivé viennent de chaque `RowAction`.
|
|
163
|
+
*/
|
|
164
|
+
rowActionButtonProps?: Partial<
|
|
165
|
+
Omit<ButtonProps, "onClick" | "children" | "disabled">
|
|
166
|
+
>;
|
|
167
|
+
/**
|
|
168
|
+
* Les boutons d'action réduits à l'icône, déclencheur du menu « autres
|
|
169
|
+
* actions » compris. Une `tooltip` passée ici se fusionne avec celle de
|
|
170
|
+
* l'action, qui garde son libellé.
|
|
171
|
+
*/
|
|
172
|
+
rowActionIconButtonProps?: Partial<
|
|
173
|
+
Omit<IconButtonProps, "onClick" | "label" | "icon" | "disabled">
|
|
174
|
+
>;
|
|
175
|
+
/**
|
|
176
|
+
* Le menu des actions qui ne tiennent pas en ligne : placement, décalage.
|
|
177
|
+
*
|
|
178
|
+
* Ni `open` ni `defaultOpen` : partagés par toutes les lignes, ils
|
|
179
|
+
* ouvriraient tous les menus à la fois.
|
|
180
|
+
*/
|
|
181
|
+
rowActionsMenuProps?: Partial<
|
|
182
|
+
Omit<DropdownMenuProps, "items" | "children" | "onSelect" | "open" | "defaultOpen">
|
|
183
|
+
>;
|
|
184
|
+
/**
|
|
185
|
+
* La confirmation d'une action : libellés par défaut, `loading`.
|
|
186
|
+
*
|
|
187
|
+
* Ce que déclare `confirm` sur l'action l'emporte ; le ton suit
|
|
188
|
+
* `confirm.destructive`, l'ouverture et la validation restent au tableau.
|
|
189
|
+
*/
|
|
190
|
+
confirmDialogProps?: Partial<
|
|
191
|
+
Omit<ConfirmDialogProps, "open" | "onOpenChange" | "onConfirm" | "title" | "tone">
|
|
192
|
+
>;
|
|
138
193
|
|
|
139
194
|
/** Évalue les règles de permission des actions. */
|
|
140
195
|
can?: (rule: PermissionRule) => boolean;
|
|
@@ -161,13 +216,28 @@ export interface DataTableProps<T> {
|
|
|
161
216
|
loading?: boolean;
|
|
162
217
|
/** Lignes fantômes pendant le premier chargement. */
|
|
163
218
|
skeletonRows?: number;
|
|
219
|
+
/** Les blocs fantômes : rayon, classe, style. Leurs tailles ne sont que des défauts. */
|
|
220
|
+
skeletonProps?: Partial<SkeletonProps>;
|
|
164
221
|
error?: ReactNode;
|
|
165
222
|
onRetry?: () => void;
|
|
223
|
+
/**
|
|
224
|
+
* L'état d'erreur : titre, libellé du bouton, action, `compact`.
|
|
225
|
+
*
|
|
226
|
+
* Le message vient de `error` ; `onRetry` du tableau l'emporte s'il est
|
|
227
|
+
* fourni.
|
|
228
|
+
*/
|
|
229
|
+
errorStateProps?: Partial<Omit<ErrorStateProps, "description" | "error">>;
|
|
166
230
|
empty?: {
|
|
167
231
|
title?: ReactNode;
|
|
168
232
|
description?: ReactNode;
|
|
169
233
|
action?: ReactNode;
|
|
234
|
+
icon?: ReactNode;
|
|
170
235
|
};
|
|
236
|
+
/**
|
|
237
|
+
* L'état vide au complet : icône, `compact`, classe. Ce que déclare `empty`
|
|
238
|
+
* l'emporte sur les mêmes clés.
|
|
239
|
+
*/
|
|
240
|
+
emptyStateProps?: Partial<EmptyStateProps>;
|
|
171
241
|
|
|
172
242
|
/* ───────────────────────────────────────────────────────── sélection */
|
|
173
243
|
|
|
@@ -183,6 +253,25 @@ export interface DataTableProps<T> {
|
|
|
183
253
|
defaultSelectedKeys?: Key[];
|
|
184
254
|
onSelectionChange?: (keys: Key[], rows: T[]) => void;
|
|
185
255
|
isRowSelectable?: (row: T, index: number) => boolean;
|
|
256
|
+
/**
|
|
257
|
+
* Les cases de sélection, en-tête comprise : taille, ton, classe.
|
|
258
|
+
*
|
|
259
|
+
* L'état coché, l'indétermination et le rappel sont calculés par le
|
|
260
|
+
* tableau ; le clic reste retenu pour ne pas déclencher `onRowClick`.
|
|
261
|
+
*/
|
|
262
|
+
checkboxProps?: Partial<
|
|
263
|
+
Omit<
|
|
264
|
+
CheckboxProps,
|
|
265
|
+
| "checked"
|
|
266
|
+
| "defaultChecked"
|
|
267
|
+
| "indeterminate"
|
|
268
|
+
| "onValueChange"
|
|
269
|
+
| "onChange"
|
|
270
|
+
| "onClick"
|
|
271
|
+
| "disabled"
|
|
272
|
+
| "value"
|
|
273
|
+
>
|
|
274
|
+
>;
|
|
186
275
|
/** Barre affichée dès qu'une ligne est cochée. */
|
|
187
276
|
selectionActions?: (context: {
|
|
188
277
|
keys: Key[];
|
|
@@ -200,3 +289,14 @@ export interface DataTableProps<T> {
|
|
|
200
289
|
caption?: string;
|
|
201
290
|
className?: string;
|
|
202
291
|
}
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* Les réglages des sous-composants d'une action de ligne. Regroupés, ils
|
|
295
|
+
* traversent la vue cartes sans une prop par sous-composant.
|
|
296
|
+
*/
|
|
297
|
+
export interface RowActionsSlots {
|
|
298
|
+
button: DataTableProps<unknown>["rowActionButtonProps"];
|
|
299
|
+
iconButton: DataTableProps<unknown>["rowActionIconButtonProps"];
|
|
300
|
+
menu: DataTableProps<unknown>["rowActionsMenuProps"];
|
|
301
|
+
confirmDialog: DataTableProps<unknown>["confirmDialogProps"];
|
|
302
|
+
}
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { forwardRef, useId, useRef, useState, type ReactNode } from "react";
|
|
2
2
|
import { cn, toneClass } from "@sia-ui/utils";
|
|
3
|
+
import { useSiaLocale } from "@sia-ui/headless";
|
|
3
4
|
import { Calendar, type CalendarProps } from "../Calendar";
|
|
4
5
|
import type { ComponentTone } from "@sia-ui/tokens";
|
|
5
6
|
import { CalendarIcon, ChevronDownIcon, XIcon } from "../Icons";
|
|
6
|
-
import { Overlay, type OverlayPlacement } from "../Overlay";
|
|
7
|
+
import { Overlay, type OverlayPlacement, type OverlayProps } from "../Overlay";
|
|
7
8
|
|
|
8
9
|
export interface DatePickerProps
|
|
9
10
|
extends Pick<CalendarProps, "locale" | "firstDayOfWeek" | "disabledDate" | "cellRender" | "showWeek"> {
|
|
@@ -16,12 +17,15 @@ export interface DatePickerProps
|
|
|
16
17
|
onValueChange?: (value: string) => void;
|
|
17
18
|
onOpenChange?: (open: boolean) => void;
|
|
18
19
|
open?: boolean;
|
|
20
|
+
/** Par défaut, « Sélectionner une date » de la locale SIA. */
|
|
19
21
|
placeholder?: string;
|
|
20
22
|
formatValue?: (value: string, locale: string) => ReactNode;
|
|
21
23
|
allowClear?: boolean;
|
|
22
24
|
showToday?: boolean;
|
|
23
25
|
renderExtraFooter?: () => ReactNode;
|
|
24
26
|
placement?: OverlayPlacement;
|
|
27
|
+
/** Réglages du panneau flottant — décalage, marge au bord, classe. */
|
|
28
|
+
overlayProps?: Partial<Omit<OverlayProps, "open" | "anchorRef" | "onOpenChange" | "children">>;
|
|
25
29
|
tone?: ComponentTone;
|
|
26
30
|
invalid?: boolean;
|
|
27
31
|
disabled?: boolean;
|
|
@@ -57,17 +61,18 @@ export const DatePicker = forwardRef<HTMLButtonElement, DatePickerProps>(functio
|
|
|
57
61
|
onValueChange,
|
|
58
62
|
onOpenChange,
|
|
59
63
|
open: openProp,
|
|
60
|
-
placeholder
|
|
64
|
+
placeholder: placeholderProp,
|
|
61
65
|
formatValue = defaultFormat,
|
|
62
66
|
allowClear = true,
|
|
63
67
|
showToday = true,
|
|
64
68
|
renderExtraFooter,
|
|
65
69
|
placement = "bottom-start",
|
|
70
|
+
overlayProps,
|
|
66
71
|
tone = "primary",
|
|
67
72
|
invalid = false,
|
|
68
73
|
disabled = false,
|
|
69
74
|
required = false,
|
|
70
|
-
locale
|
|
75
|
+
locale: localeProp,
|
|
71
76
|
firstDayOfWeek = 1,
|
|
72
77
|
disabledDate,
|
|
73
78
|
cellRender,
|
|
@@ -77,6 +82,9 @@ export const DatePicker = forwardRef<HTMLButtonElement, DatePickerProps>(functio
|
|
|
77
82
|
"aria-required": ariaRequired,
|
|
78
83
|
...ariaProps
|
|
79
84
|
}, forwardedRef) {
|
|
85
|
+
const messages = useSiaLocale();
|
|
86
|
+
const placeholder = placeholderProp ?? messages.selectDate;
|
|
87
|
+
const locale = localeProp ?? messages.language;
|
|
80
88
|
const generatedId = useId();
|
|
81
89
|
const controlId = id ?? `sia-date-picker-${generatedId.replace(/:/g, "")}`;
|
|
82
90
|
const anchorRef = useRef<HTMLButtonElement>(null);
|
|
@@ -96,12 +104,12 @@ export const DatePicker = forwardRef<HTMLButtonElement, DatePickerProps>(functio
|
|
|
96
104
|
<CalendarIcon className="sia-picker__icon" />
|
|
97
105
|
<span className={cn("sia-picker__value", !current && "sia-picker__placeholder")}>{current ? formatValue(current, locale) : placeholder}</span>
|
|
98
106
|
<span className="sia-picker__actions">
|
|
99
|
-
{allowClear && current && <span className="sia-picker__clear" title=
|
|
107
|
+
{allowClear && current && <span className="sia-picker__clear" title={messages.clear} aria-hidden="true" onClick={(event) => { event.stopPropagation(); update(""); }}><XIcon /></span>}
|
|
100
108
|
<ChevronDownIcon />
|
|
101
109
|
</span>
|
|
102
110
|
</button>
|
|
103
|
-
<Overlay open={open} anchorRef={anchorRef} onOpenChange={setOpen}
|
|
104
|
-
<Calendar value={current} defaultMonth={current} {...(min && max ? { validRange: [min, max] as [string, string] } : {})} disabledDate={(date) => Boolean((min && date < min) || (max && date > max) || disabledDate?.(date))} locale={locale} firstDayOfWeek={firstDayOfWeek} {...(cellRender ? { cellRender } : {})} {...(showWeek !== undefined ? { showWeek } : {})} tone={tone} onValueChange={(next) => { update(next); setOpen(false); }} renderExtraFooter={() => <div className="sia-picker__footer">{showToday && <button type="button" disabled={Boolean((min && todayValue < min) || (max && todayValue > max) || disabledDate?.(todayValue))} onClick={() => { update(todayValue); setOpen(false); }}>
|
|
111
|
+
<Overlay placement={placement} {...overlayProps} className={cn("sia-picker__overlay", overlayProps?.className)} open={open} anchorRef={anchorRef} onOpenChange={setOpen}>
|
|
112
|
+
<Calendar value={current} defaultMonth={current} {...(min && max ? { validRange: [min, max] as [string, string] } : {})} disabledDate={(date) => Boolean((min && date < min) || (max && date > max) || disabledDate?.(date))} locale={locale} firstDayOfWeek={firstDayOfWeek} {...(cellRender ? { cellRender } : {})} {...(showWeek !== undefined ? { showWeek } : {})} tone={tone} onValueChange={(next) => { update(next); setOpen(false); }} renderExtraFooter={() => <div className="sia-picker__footer">{showToday && <button type="button" disabled={Boolean((min && todayValue < min) || (max && todayValue > max) || disabledDate?.(todayValue))} onClick={() => { update(todayValue); setOpen(false); }}>{messages.today}</button>}{renderExtraFooter?.()}</div>} />
|
|
105
113
|
</Overlay>
|
|
106
114
|
</div>
|
|
107
115
|
);
|
|
@@ -1,10 +1,19 @@
|
|
|
1
1
|
import { type ReactNode } from "react";
|
|
2
|
-
import { DateRangePicker, type DateRangeValue } from "../DateRangePicker";
|
|
2
|
+
import { DateRangePicker, type DateRangePickerProps, type DateRangeValue } from "../DateRangePicker";
|
|
3
3
|
import { cn } from "@sia-ui/utils";
|
|
4
|
+
import { useSiaLocale } from "@sia-ui/headless";
|
|
4
5
|
import "./styles.css";
|
|
5
6
|
|
|
6
7
|
export interface DateRangePreset { label: ReactNode; value: DateRangeValue; }
|
|
7
|
-
export interface DateRangeFilterProps {
|
|
8
|
+
export interface DateRangeFilterProps {
|
|
9
|
+
value: DateRangeValue;
|
|
10
|
+
onValueChange: (value: DateRangeValue) => void;
|
|
11
|
+
presets?: DateRangePreset[];
|
|
12
|
+
onClear?: () => void;
|
|
13
|
+
/** Réglages du sélecteur de période — bornes, libellés, jours désactivés. */
|
|
14
|
+
dateRangePickerProps?: Partial<Omit<DateRangePickerProps, "value" | "defaultValue" | "onValueChange">>;
|
|
15
|
+
className?: string;
|
|
16
|
+
}
|
|
8
17
|
/**
|
|
9
18
|
* Une période, avec ses raccourcis.
|
|
10
19
|
*
|
|
@@ -12,4 +21,4 @@ export interface DateRangeFilterProps { value: DateRangeValue; onValueChange: (v
|
|
|
12
21
|
* l'exercice — et les proposer évite de saisir deux dates pour la question
|
|
13
22
|
* que tout le monde pose en premier.
|
|
14
23
|
*/
|
|
15
|
-
export function DateRangeFilter({ value, onValueChange, presets = [], onClear, className }: DateRangeFilterProps) { return <section className={cn("sia-date-range-filter", className)}><div className="sia-date-range-filter__presets">{presets.map((preset, index) => <button type="button" key={index} onClick={() => onValueChange(preset.value)}>{preset.label}</button>)}</div><DateRangePicker value={value} onValueChange={onValueChange} />{onClear && (value.start || value.end) && <button type="button" className="sia-date-range-filter__clear" onClick={onClear}>
|
|
24
|
+
export function DateRangeFilter({ value, onValueChange, presets = [], onClear, dateRangePickerProps, className }: DateRangeFilterProps) { const locale = useSiaLocale(); return <section className={cn("sia-date-range-filter", className)}><div className="sia-date-range-filter__presets">{presets.map((preset, index) => <button type="button" key={index} onClick={() => onValueChange(preset.value)}>{preset.label}</button>)}</div><DateRangePicker {...dateRangePickerProps} value={value} onValueChange={onValueChange} />{onClear && (value.start || value.end) && <button type="button" className="sia-date-range-filter__clear" onClick={onClear}>{locale.clear}</button>}</section>; }
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { useRef, useState } from "react";
|
|
2
2
|
import { cn, toneClass } from "@sia-ui/utils";
|
|
3
|
+
import { useSiaLocale } from "@sia-ui/headless";
|
|
3
4
|
import { Calendar, type CalendarProps, type CalendarRangeValue } from "../Calendar";
|
|
4
5
|
import type { ComponentTone } from "@sia-ui/tokens";
|
|
5
6
|
import { ArrowRightIcon, CalendarIcon, ChevronDownIcon, XIcon } from "../Icons";
|
|
6
|
-
import { Overlay, type OverlayPlacement } from "../Overlay";
|
|
7
|
+
import { Overlay, type OverlayPlacement, type OverlayProps } from "../Overlay";
|
|
7
8
|
|
|
8
9
|
export type DateRangeValue = CalendarRangeValue;
|
|
9
10
|
export interface DateRangePickerProps extends Pick<CalendarProps, "locale" | "firstDayOfWeek" | "disabledDate" | "cellRender" | "showWeek"> {
|
|
@@ -12,10 +13,14 @@ export interface DateRangePickerProps extends Pick<CalendarProps, "locale" | "fi
|
|
|
12
13
|
onValueChange?: (value: DateRangeValue) => void;
|
|
13
14
|
min?: string;
|
|
14
15
|
max?: string;
|
|
16
|
+
/** Par défaut, « Date de début » de la locale SIA. */
|
|
15
17
|
startLabel?: string;
|
|
18
|
+
/** Par défaut, « Date de fin » de la locale SIA. */
|
|
16
19
|
endLabel?: string;
|
|
17
20
|
allowClear?: boolean;
|
|
18
21
|
placement?: OverlayPlacement;
|
|
22
|
+
/** Réglages du panneau flottant — décalage, marge au bord, classe. */
|
|
23
|
+
overlayProps?: Partial<Omit<OverlayProps, "open" | "anchorRef" | "onOpenChange" | "children">>;
|
|
19
24
|
tone?: ComponentTone;
|
|
20
25
|
invalid?: boolean;
|
|
21
26
|
disabled?: boolean;
|
|
@@ -28,7 +33,11 @@ export interface DateRangePickerProps extends Pick<CalendarProps, "locale" | "fi
|
|
|
28
33
|
* faut alors décider lequel des deux avait tort. Ici les deux bornes sont
|
|
29
34
|
* une seule valeur, et le calendrier les tient dans l'ordre.
|
|
30
35
|
*/
|
|
31
|
-
export function DateRangePicker({ value, defaultValue = { start: "", end: "" }, onValueChange, min, max, startLabel
|
|
36
|
+
export function DateRangePicker({ value, defaultValue = { start: "", end: "" }, onValueChange, min, max, startLabel: startLabelProp, endLabel: endLabelProp, allowClear = true, placement = "bottom-start", overlayProps, tone = "primary", invalid, disabled, locale: localeProp, firstDayOfWeek = 1, disabledDate, cellRender, showWeek, className }: DateRangePickerProps) {
|
|
37
|
+
const messages = useSiaLocale();
|
|
38
|
+
const startLabel = startLabelProp ?? messages.startDate;
|
|
39
|
+
const endLabel = endLabelProp ?? messages.endDate;
|
|
40
|
+
const locale = localeProp ?? messages.language;
|
|
32
41
|
const anchorRef = useRef<HTMLButtonElement>(null);
|
|
33
42
|
const [internal, setInternal] = useState(defaultValue);
|
|
34
43
|
const [open, setOpen] = useState(false);
|
|
@@ -37,7 +46,7 @@ export function DateRangePicker({ value, defaultValue = { start: "", end: "" },
|
|
|
37
46
|
const rangeInvalid = Boolean(invalid || (current.start && current.end && current.start > current.end));
|
|
38
47
|
const format = (date: string) => date ? new Intl.DateTimeFormat(locale, { day: "2-digit", month: "short", year: "numeric" }).format(new Date(`${date}T12:00:00`)) : null;
|
|
39
48
|
return <div className={cn("sia-picker", "sia-range-picker", "sia-date-range-picker", toneClass("sia-tone", tone), rangeInvalid && "sia-picker--invalid", disabled && "sia-picker--disabled", className)}>
|
|
40
|
-
<button ref={anchorRef} type="button" className="sia-picker__trigger sia-range-picker__trigger" disabled={disabled} aria-haspopup="dialog" aria-expanded={open} aria-invalid={rangeInvalid || undefined} onClick={() => setOpen(!open)}><CalendarIcon className="sia-picker__icon" /><span className={cn("sia-range-picker__part", !current.start && "sia-picker__placeholder")}>{format(current.start) ?? startLabel}</span><ArrowRightIcon className="sia-range-picker__separator" /><span className={cn("sia-range-picker__part", !current.end && "sia-picker__placeholder")}>{format(current.end) ?? endLabel}</span><span className="sia-picker__actions">{allowClear && (current.start || current.end) && <span className="sia-picker__clear" title=
|
|
41
|
-
<Overlay open={open} anchorRef={anchorRef} onOpenChange={setOpen}
|
|
49
|
+
<button ref={anchorRef} type="button" className="sia-picker__trigger sia-range-picker__trigger" disabled={disabled} aria-haspopup="dialog" aria-expanded={open} aria-invalid={rangeInvalid || undefined} onClick={() => setOpen(!open)}><CalendarIcon className="sia-picker__icon" /><span className={cn("sia-range-picker__part", !current.start && "sia-picker__placeholder")}>{format(current.start) ?? startLabel}</span><ArrowRightIcon className="sia-range-picker__separator" /><span className={cn("sia-range-picker__part", !current.end && "sia-picker__placeholder")}>{format(current.end) ?? endLabel}</span><span className="sia-picker__actions">{allowClear && (current.start || current.end) && <span className="sia-picker__clear" title={messages.clear} aria-hidden="true" onClick={(event) => { event.stopPropagation(); update({ start: "", end: "" }); }}><XIcon /></span>}<ChevronDownIcon /></span></button>
|
|
50
|
+
<Overlay placement={placement} {...overlayProps} className={cn("sia-picker__overlay", overlayProps?.className)} open={open} anchorRef={anchorRef} onOpenChange={setOpen}><Calendar selectionMode="range" range={current} defaultMonth={current.start || current.end} locale={locale} firstDayOfWeek={firstDayOfWeek} disabledDate={(date) => Boolean((min && date < min) || (max && date > max) || disabledDate?.(date))} {...(cellRender ? { cellRender } : {})} {...(showWeek !== undefined ? { showWeek } : {})} tone={tone} onRangeChange={(next) => { update(next); if (next.end) setOpen(false); }} /></Overlay>
|
|
42
51
|
</div>;
|
|
43
52
|
}
|
|
@@ -2,6 +2,7 @@ import { useState } from "react";
|
|
|
2
2
|
import { DatePicker, type DatePickerProps } from "../DatePicker";
|
|
3
3
|
import { TimePicker, type TimePickerProps } from "../TimePicker";
|
|
4
4
|
import { cn, toneClass } from "@sia-ui/utils";
|
|
5
|
+
import { useSiaLocale } from "@sia-ui/headless";
|
|
5
6
|
import type { ComponentTone } from "@sia-ui/tokens";
|
|
6
7
|
|
|
7
8
|
export interface DateTimeValue { date: string; time: string; }
|
|
@@ -31,9 +32,10 @@ export interface DateTimePickerProps {
|
|
|
31
32
|
* celle de la veille.
|
|
32
33
|
*/
|
|
33
34
|
export function DateTimePicker({ value, defaultValue = { date: "", time: "" }, onValueChange, minuteStep = 1, secondStep = 1, showSeconds = false, hourFormat = 24, needConfirm = false, minDate, maxDate, tone = "primary", invalid, disabled, dateProps, timeProps, className }: DateTimePickerProps) {
|
|
35
|
+
const locale = useSiaLocale();
|
|
34
36
|
const [internal, setInternal] = useState(defaultValue);
|
|
35
37
|
const current = value ?? internal;
|
|
36
38
|
const update = (next: DateTimeValue) => { if (value === undefined) setInternal(next); onValueChange?.(next); };
|
|
37
39
|
const stateProps = { ...(invalid !== undefined ? { invalid } : {}), ...(disabled !== undefined ? { disabled } : {}) };
|
|
38
|
-
return <div className={cn("sia-date-time-picker", toneClass("sia-tone", tone), className)}><DatePicker {...dateProps} value={current.date} {...(minDate ? { min: minDate } : {})} {...(maxDate ? { max: maxDate } : {})} tone={tone} {...stateProps} aria-label=
|
|
40
|
+
return <div className={cn("sia-date-time-picker", toneClass("sia-tone", tone), className)}><DatePicker {...dateProps} value={current.date} {...(minDate ? { min: minDate } : {})} {...(maxDate ? { max: maxDate } : {})} tone={tone} {...stateProps} aria-label={locale.dateTimePicker.date} onValueChange={(date) => update({ ...current, date })} /><TimePicker {...timeProps} value={current.time} minuteStep={minuteStep} secondStep={secondStep} showSeconds={showSeconds} hourFormat={hourFormat} needConfirm={needConfirm} tone={tone} {...stateProps} aria-label={locale.dateTimePicker.time} onValueChange={(time) => update({ ...current, time })} /></div>;
|
|
39
41
|
}
|
|
@@ -11,7 +11,7 @@ import { Portal } from "@sia-ui/react";
|
|
|
11
11
|
import "./styles.css";
|
|
12
12
|
import { useOutsideClick } from "@sia-ui/react";
|
|
13
13
|
import { DrawerContext } from "./context";
|
|
14
|
-
import { overlayStack, useDisclosureState } from "@sia-ui/headless";
|
|
14
|
+
import { overlayStack, useDisclosureState, useSiaLocale } from "@sia-ui/headless";
|
|
15
15
|
import { XIcon } from "../Icons";
|
|
16
16
|
/** Ce que la réf impérative expose — ouvrir un tiroir depuis l'extérieur. */
|
|
17
17
|
export type DrawerRef = {
|
|
@@ -76,10 +76,12 @@ export const Drawer = forwardRef<DrawerRef, IDrawerProps>(
|
|
|
76
76
|
// même mécanique.
|
|
77
77
|
const {
|
|
78
78
|
isOpen,
|
|
79
|
+
present,
|
|
79
80
|
phase: state,
|
|
80
81
|
open: doOpen,
|
|
81
82
|
close: doClose,
|
|
82
83
|
toggle,
|
|
84
|
+
recentlyOpened,
|
|
83
85
|
} = useDisclosureState({
|
|
84
86
|
...(open !== undefined ? { open } : {}),
|
|
85
87
|
defaultOpen,
|
|
@@ -107,9 +109,10 @@ export const Drawer = forwardRef<DrawerRef, IDrawerProps>(
|
|
|
107
109
|
[isOpen, doOpen, doClose, toggle],
|
|
108
110
|
);
|
|
109
111
|
|
|
110
|
-
/* Overlay stack
|
|
112
|
+
/* Overlay stack — tenu tant que le tiroir est monté, sortie comprise :
|
|
113
|
+
rendu trop tôt, il passait sous la page à la fin de son animation. */
|
|
111
114
|
useEffect(() => {
|
|
112
|
-
if (!
|
|
115
|
+
if (!present) return;
|
|
113
116
|
|
|
114
117
|
zIndexRef.current = overlayStack.acquire("drawer");
|
|
115
118
|
|
|
@@ -117,7 +120,7 @@ export const Drawer = forwardRef<DrawerRef, IDrawerProps>(
|
|
|
117
120
|
overlayStack.release("drawer");
|
|
118
121
|
zIndexRef.current = null;
|
|
119
122
|
};
|
|
120
|
-
}, [
|
|
123
|
+
}, [present]);
|
|
121
124
|
|
|
122
125
|
/* ESC + body lock */
|
|
123
126
|
useEffect(() => {
|
|
@@ -145,13 +148,19 @@ export const Drawer = forwardRef<DrawerRef, IDrawerProps>(
|
|
|
145
148
|
useOutsideClick([drawerRef], () => {
|
|
146
149
|
if (!closeOnOutsideClick || !isOpen) return;
|
|
147
150
|
|
|
151
|
+
// Le second clic d'un double clic sur la ligne qui ouvre le tiroir, ou
|
|
152
|
+
// le rebond d'une souris : il tombe sur le fond et refermait aussitôt.
|
|
153
|
+
if (recentlyOpened()) return;
|
|
154
|
+
|
|
148
155
|
// 🔒 Seul le drawer au top peut se fermer
|
|
149
156
|
if (!overlayStack.isTop(zIndexRef.current)) return;
|
|
150
157
|
|
|
151
158
|
doClose();
|
|
152
159
|
});
|
|
153
160
|
|
|
154
|
-
|
|
161
|
+
// Monté tant que la sortie se joue : `isOpen` est déjà faux, le parent
|
|
162
|
+
// a déjà été prévenu, mais l'animation n'est pas finie.
|
|
163
|
+
if (!present) return null;
|
|
155
164
|
|
|
156
165
|
return (
|
|
157
166
|
<Portal>
|
|
@@ -206,6 +215,7 @@ export function DrawerFooter({ children }: { children: React.ReactNode }) {
|
|
|
206
215
|
|
|
207
216
|
export function DrawerClose() {
|
|
208
217
|
const ctx = useContext(DrawerContext);
|
|
218
|
+
const locale = useSiaLocale();
|
|
209
219
|
if (!ctx) return null;
|
|
210
220
|
|
|
211
221
|
return (
|
|
@@ -213,7 +223,7 @@ export function DrawerClose() {
|
|
|
213
223
|
type="button"
|
|
214
224
|
className="sia-drawer-close"
|
|
215
225
|
onClick={ctx.close}
|
|
216
|
-
aria-label=
|
|
226
|
+
aria-label={locale.close}
|
|
217
227
|
>
|
|
218
228
|
<XIcon aria-hidden="true" />
|
|
219
229
|
</button>
|
|
@@ -11,7 +11,7 @@ import {
|
|
|
11
11
|
} from "react";
|
|
12
12
|
import { cn } from "@sia-ui/utils";
|
|
13
13
|
import { useRovingIndex } from "@sia-ui/headless";
|
|
14
|
-
import { Overlay, type OverlayPlacement } from "../Overlay";
|
|
14
|
+
import { Overlay, type OverlayPlacement, type OverlayProps } from "../Overlay";
|
|
15
15
|
import { CheckIcon } from "../Icons";
|
|
16
16
|
import "./styles.css";
|
|
17
17
|
|
|
@@ -40,7 +40,25 @@ export interface MenuLabel {
|
|
|
40
40
|
label: ReactNode;
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
-
|
|
43
|
+
/**
|
|
44
|
+
* Un en-tête à contenu libre : le profil d'un menu de compte, un résumé.
|
|
45
|
+
*
|
|
46
|
+
* `label` est un titre de section — petit, en capitales, sur une ligne — et
|
|
47
|
+
* n'a pas vocation à porter un avatar, un nom, une adresse et un rôle.
|
|
48
|
+
* L'en-tête ne se sélectionne pas et le clavier le saute.
|
|
49
|
+
*/
|
|
50
|
+
export interface MenuHeader {
|
|
51
|
+
key: string;
|
|
52
|
+
type: "header";
|
|
53
|
+
content: ReactNode;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export type MenuEntry = MenuItem | MenuSeparator | MenuLabel | MenuHeader;
|
|
57
|
+
|
|
58
|
+
/** Ce qu'on règle de l'`Overlay` d'un menu : tout sauf ce que le menu pilote. */
|
|
59
|
+
export type MenuOverlayProps = Partial<
|
|
60
|
+
Omit<OverlayProps, "open" | "anchorRef" | "onOpenChange" | "children">
|
|
61
|
+
>;
|
|
44
62
|
|
|
45
63
|
function isActionable(entry: MenuEntry): entry is MenuItem {
|
|
46
64
|
return !("type" in entry) && !entry.disabled;
|
|
@@ -78,6 +96,13 @@ export function MenuList({
|
|
|
78
96
|
if ("type" in entry && entry.type === "separator") {
|
|
79
97
|
return <div key={entry.key} className="sia-menu__separator" role="separator" />;
|
|
80
98
|
}
|
|
99
|
+
if ("type" in entry && entry.type === "header") {
|
|
100
|
+
return (
|
|
101
|
+
<div key={entry.key} className="sia-menu__header">
|
|
102
|
+
{entry.content}
|
|
103
|
+
</div>
|
|
104
|
+
);
|
|
105
|
+
}
|
|
81
106
|
if ("type" in entry && entry.type === "label") {
|
|
82
107
|
return (
|
|
83
108
|
<div key={entry.key} className="sia-menu__group-label">
|
|
@@ -203,6 +228,8 @@ export interface DropdownMenuProps {
|
|
|
203
228
|
onOpenChange?: (open: boolean) => void;
|
|
204
229
|
placement?: OverlayPlacement;
|
|
205
230
|
matchTriggerWidth?: boolean;
|
|
231
|
+
/** Le positionnement du panneau : décalage, marge au bord de l'écran, id. */
|
|
232
|
+
overlayProps?: MenuOverlayProps;
|
|
206
233
|
className?: string;
|
|
207
234
|
}
|
|
208
235
|
|
|
@@ -222,6 +249,7 @@ export function DropdownMenu({
|
|
|
222
249
|
onOpenChange,
|
|
223
250
|
placement = "bottom-start",
|
|
224
251
|
matchTriggerWidth = false,
|
|
252
|
+
overlayProps,
|
|
225
253
|
className,
|
|
226
254
|
}: DropdownMenuProps) {
|
|
227
255
|
const anchorRef = useRef<HTMLElement>(null);
|
|
@@ -265,7 +293,8 @@ export function DropdownMenu({
|
|
|
265
293
|
onOpenChange={setOpen}
|
|
266
294
|
placement={placement}
|
|
267
295
|
matchAnchorWidth={matchTriggerWidth}
|
|
268
|
-
|
|
296
|
+
{...overlayProps}
|
|
297
|
+
className={cn("sia-menu-overlay", className, overlayProps?.className)}
|
|
269
298
|
>
|
|
270
299
|
<MenuList
|
|
271
300
|
items={items}
|
|
@@ -283,6 +312,8 @@ export interface ContextMenuProps {
|
|
|
283
312
|
items: MenuEntry[];
|
|
284
313
|
children: ReactNode;
|
|
285
314
|
onSelect?: (item: MenuItem) => void;
|
|
315
|
+
/** Le positionnement du panneau, comme pour `DropdownMenu`. */
|
|
316
|
+
overlayProps?: MenuOverlayProps;
|
|
286
317
|
className?: string;
|
|
287
318
|
}
|
|
288
319
|
|
|
@@ -297,6 +328,7 @@ export function ContextMenu({
|
|
|
297
328
|
items,
|
|
298
329
|
children,
|
|
299
330
|
onSelect,
|
|
331
|
+
overlayProps,
|
|
300
332
|
className,
|
|
301
333
|
}: ContextMenuProps) {
|
|
302
334
|
const anchorRef = useRef<HTMLSpanElement>(null);
|
|
@@ -335,7 +367,8 @@ export function ContextMenu({
|
|
|
335
367
|
}}
|
|
336
368
|
placement="bottom-start"
|
|
337
369
|
offset={2}
|
|
338
|
-
|
|
370
|
+
{...overlayProps}
|
|
371
|
+
className={cn("sia-menu-overlay", className, overlayProps?.className)}
|
|
339
372
|
>
|
|
340
373
|
<MenuList
|
|
341
374
|
items={items}
|
|
@@ -90,6 +90,16 @@
|
|
|
90
90
|
}
|
|
91
91
|
|
|
92
92
|
.sia-menu__separator { height: 1px; margin: .3rem -.3rem; background: var(--sia-border); }
|
|
93
|
+
/* L'en-tête libre : aucune typographie imposée, seulement de l'air et un
|
|
94
|
+
filet qui le sépare des entrées — il porte un profil, pas un titre. */
|
|
95
|
+
.sia-menu__header {
|
|
96
|
+
padding: .55rem .6rem .6rem;
|
|
97
|
+
margin: -.3rem -.3rem .3rem;
|
|
98
|
+
border-bottom: 1px solid var(--sia-border);
|
|
99
|
+
color: var(--sia-foreground);
|
|
100
|
+
font-size: .875rem;
|
|
101
|
+
}
|
|
102
|
+
|
|
93
103
|
.sia-menu__group-label {
|
|
94
104
|
padding: .4rem .55rem .25rem;
|
|
95
105
|
color: var(--sia-muted);
|
|
@@ -1,9 +1,27 @@
|
|
|
1
1
|
import { cn } from "@sia-ui/utils";
|
|
2
|
-
|
|
2
|
+
import { formatMessage, plural, useSiaLocale } from "@sia-ui/headless";
|
|
3
|
+
export interface DurationDisplayProps { milliseconds: number; /** La langue des nombres et des unités. Par défaut, celle de la locale SIA. */ locale?: string; style?: "short" | "long" | "clock"; className?: string; }
|
|
3
4
|
/**
|
|
4
5
|
* Une durée, en mots ou en pendule.
|
|
5
6
|
*
|
|
6
7
|
* Trois styles pour trois usages : `clock` pour un chronomètre qu'on lit en
|
|
7
8
|
* continu, `short` pour une colonne de tableau, `long` pour une phrase.
|
|
8
9
|
*/
|
|
9
|
-
export function DurationDisplay({ milliseconds, locale
|
|
10
|
+
export function DurationDisplay({ milliseconds, locale: localeProp, style = "short", className }: DurationDisplayProps) {
|
|
11
|
+
const messages = useSiaLocale();
|
|
12
|
+
const locale = localeProp ?? messages.language;
|
|
13
|
+
const unites = messages.durationDisplay;
|
|
14
|
+
const totalSeconds = Math.max(0, Math.floor(milliseconds / 1000));
|
|
15
|
+
const hours = Math.floor(totalSeconds / 3600);
|
|
16
|
+
const minutes = Math.floor((totalSeconds % 3600) / 60);
|
|
17
|
+
const seconds = totalSeconds % 60;
|
|
18
|
+
if (style === "clock") return <span className={cn("sia-duration-display", className)}>{[hours, minutes, seconds].map((value) => String(value).padStart(2, "0")).join(":")}</span>;
|
|
19
|
+
const formatter = new Intl.NumberFormat(locale);
|
|
20
|
+
// Le nombre formaté remplace `{count}` ; le pluriel se choisit sur la valeur brute.
|
|
21
|
+
const dire = (value: number, cle: "hours" | "minutes" | "seconds") =>
|
|
22
|
+
style === "long"
|
|
23
|
+
? plural(unites, cle, value, locale, { count: formatter.format(value) })
|
|
24
|
+
: formatMessage(unites[`${cle}Short`], { count: formatter.format(value) });
|
|
25
|
+
const parts = [[hours, "hours"], [minutes, "minutes"], [seconds, "seconds"]] as const;
|
|
26
|
+
return <span className={cn("sia-duration-display", className)}>{parts.filter(([value]) => value > 0).map(([value, cle]) => dire(value, cle)).join(" ") || dire(0, "seconds")}</span>;
|
|
27
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { type ReactNode } from "react";
|
|
2
2
|
import { cn, formatDate, formatTimeAgo, normalizeIdentifier, truncate } from "@sia-ui/utils";
|
|
3
|
+
import { useSiaLocale } from "@sia-ui/headless";
|
|
3
4
|
import "./styles.css";
|
|
4
5
|
|
|
5
6
|
export interface EntityMetaProps {
|
|
@@ -9,6 +10,7 @@ export interface EntityMetaProps {
|
|
|
9
10
|
updatedBy?: ReactNode;
|
|
10
11
|
identifier?: string;
|
|
11
12
|
status?: ReactNode;
|
|
13
|
+
/** La langue des dates. Par défaut, celle de la locale SIA. */
|
|
12
14
|
locale?: string;
|
|
13
15
|
className?: string;
|
|
14
16
|
}
|
|
@@ -46,26 +48,29 @@ export function EntityMeta({
|
|
|
46
48
|
updatedBy,
|
|
47
49
|
identifier,
|
|
48
50
|
status,
|
|
49
|
-
locale
|
|
51
|
+
locale: localeProp,
|
|
50
52
|
className
|
|
51
53
|
}: EntityMetaProps) {
|
|
54
|
+
const messages = useSiaLocale();
|
|
55
|
+
const locale = localeProp ?? messages.language;
|
|
56
|
+
const textes = messages.entityMeta;
|
|
52
57
|
const normalizedIdentifier = identifier ? normalizeIdentifier(identifier) : undefined;
|
|
53
|
-
const createdLabel = createdAt ? formatTimeAgo(createdAt, { locale, prefix:
|
|
54
|
-
const updatedLabel = updatedAt ? formatTimeAgo(updatedAt, { locale, prefix:
|
|
58
|
+
const createdLabel = createdAt ? formatTimeAgo(createdAt, { locale, prefix: textes.createdPrefix }) : undefined;
|
|
59
|
+
const updatedLabel = updatedAt ? formatTimeAgo(updatedAt, { locale, prefix: textes.updatedPrefix }) : undefined;
|
|
55
60
|
const updatedDate = updatedAt ? formatDate(updatedAt, { locale, dateStyle: "medium", timeStyle: "short" }) : undefined;
|
|
56
61
|
|
|
57
62
|
return (
|
|
58
|
-
<section className={cn("sia-entity-meta", className)} aria-label=
|
|
63
|
+
<section className={cn("sia-entity-meta", className)} aria-label={textes.label}>
|
|
59
64
|
<div className="sia-entity-meta__header">
|
|
60
65
|
{status && <span className="sia-entity-meta__status">{status}</span>}
|
|
61
66
|
{normalizedIdentifier && <code className="sia-entity-meta__identifier">{truncate(normalizedIdentifier, 36)}</code>}
|
|
62
67
|
</div>
|
|
63
68
|
<div className="sia-entity-meta__grid">
|
|
64
|
-
<Detail label=
|
|
65
|
-
<Detail label=
|
|
66
|
-
<Detail label=
|
|
67
|
-
<Detail label=
|
|
68
|
-
<Detail label=
|
|
69
|
+
<Detail label={textes.created} value={createdLabel} emphasized />
|
|
70
|
+
<Detail label={textes.createdBy} value={createdBy} />
|
|
71
|
+
<Detail label={textes.updated} value={updatedLabel} emphasized />
|
|
72
|
+
<Detail label={textes.updatedBy} value={updatedBy} />
|
|
73
|
+
<Detail label={textes.lastDate} value={updatedDate} />
|
|
69
74
|
</div>
|
|
70
75
|
</section>
|
|
71
76
|
);
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { type ReactNode } from "react";
|
|
2
2
|
import { cn } from "@sia-ui/utils";
|
|
3
|
-
import { Button } from "../Button";
|
|
3
|
+
import { Button, type ButtonProps } from "../Button";
|
|
4
4
|
import { useSiaLocale } from "@sia-ui/headless";
|
|
5
5
|
import "./styles.css";
|
|
6
6
|
|
|
@@ -13,6 +13,12 @@ export interface ErrorStateProps {
|
|
|
13
13
|
retryLabel?: string;
|
|
14
14
|
compact?: boolean;
|
|
15
15
|
className?: string;
|
|
16
|
+
/**
|
|
17
|
+
* Les props du bouton « Réessayer » — variante, taille, icône. Sans
|
|
18
|
+
* `onClick`, branché sur `onRetry`, ni `children` : le libellé passe par
|
|
19
|
+
* `retryLabel`.
|
|
20
|
+
*/
|
|
21
|
+
retryButtonProps?: Partial<Omit<ButtonProps, "onClick" | "children">>;
|
|
16
22
|
}
|
|
17
23
|
|
|
18
24
|
function getErrorMessage(error: unknown): string | undefined {
|
|
@@ -29,10 +35,10 @@ function getErrorMessage(error: unknown): string | undefined {
|
|
|
29
35
|
* exiger un type ferait écrire un `try` autour de chaque affichage
|
|
30
36
|
* d'erreur.
|
|
31
37
|
*/
|
|
32
|
-
export function ErrorState({ title: titleProp, description, error, action, onRetry, retryLabel: retryLabelProp, compact, className }: ErrorStateProps) {
|
|
38
|
+
export function ErrorState({ title: titleProp, description, error, action, onRetry, retryLabel: retryLabelProp, compact, className, retryButtonProps }: ErrorStateProps) {
|
|
33
39
|
const locale = useSiaLocale();
|
|
34
40
|
const retryLabel = retryLabelProp ?? locale.retry;
|
|
35
41
|
const title = titleProp ?? locale.error;
|
|
36
42
|
const message = description ?? getErrorMessage(error);
|
|
37
|
-
return <section className={cn("sia-error-state", compact && "sia-error-state--compact", className)} role="alert"><div className="sia-error-state__icon" aria-hidden="true">!</div><strong className="sia-error-state__title">{title}</strong>{message && <p className="sia-error-state__description">{message}</p>}<div className="sia-error-state__actions">{action}{onRetry && <Button variant="outline" onClick={onRetry}>{retryLabel}</Button>}</div></section>;
|
|
43
|
+
return <section className={cn("sia-error-state", compact && "sia-error-state--compact", className)} role="alert"><div className="sia-error-state__icon" aria-hidden="true">!</div><strong className="sia-error-state__title">{title}</strong>{message && <p className="sia-error-state__description">{message}</p>}<div className="sia-error-state__actions">{action}{onRetry && <Button variant="outline" {...retryButtonProps} onClick={onRetry}>{retryLabel}</Button>}</div></section>;
|
|
38
44
|
}
|