@sia-ui/cli 0.2.1 → 0.3.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/LICENSE +21 -21
- package/dist/index.js +2 -0
- package/dist/registry/package/components/Alert/index.tsx +7 -0
- package/dist/registry/package/components/AmountDisplay/index.tsx +8 -0
- package/dist/registry/package/components/AppShell/index.tsx +413 -3
- package/dist/registry/package/components/AppShell/styles.css +259 -7
- package/dist/registry/package/components/AppShell/tab-bar.tsx +134 -0
- package/dist/registry/package/components/AsyncState/index.tsx +7 -0
- package/dist/registry/package/components/AuditMeta/index.tsx +8 -0
- package/dist/registry/package/components/AuthLayout/index.tsx +7 -0
- package/dist/registry/package/components/Autocomplete/index.tsx +7 -0
- package/dist/registry/package/components/Avatar/index.tsx +333 -12
- package/dist/registry/package/components/Avatar/styles.css +236 -5
- package/dist/registry/package/components/Badge/index.tsx +7 -0
- package/dist/registry/package/components/Button/index.tsx +7 -0
- package/dist/registry/package/components/Calendar/index.tsx +11 -0
- package/dist/registry/package/components/Card/index.tsx +8 -0
- package/dist/registry/package/components/Checkbox/index.tsx +8 -0
- package/dist/registry/package/components/ColorPicker/index.tsx +7 -0
- package/dist/registry/package/components/CommandPalette/index.tsx +7 -0
- package/dist/registry/package/components/ConfirmDialog/index.tsx +7 -0
- package/dist/registry/package/components/Container/index.tsx +7 -0
- package/dist/registry/package/components/Countdown/index.tsx +7 -0
- package/dist/registry/package/components/CrudPage/dialogs.tsx +230 -0
- package/dist/registry/package/components/CrudPage/index.tsx +546 -5
- package/dist/registry/package/components/CrudPage/styles.css +25 -1
- package/dist/registry/package/components/CurrencyInput/index.tsx +7 -0
- package/dist/registry/package/components/DataTable/index.tsx +548 -200
- package/dist/registry/package/components/DataTable/query.ts +68 -0
- package/dist/registry/package/components/DataTable/row-actions.tsx +158 -0
- package/dist/registry/package/components/DataTable/styles.css +285 -73
- package/dist/registry/package/components/DataTable/types.ts +194 -0
- package/dist/registry/package/components/DataTable/values.ts +28 -0
- package/dist/registry/package/components/DatePicker/index.tsx +7 -0
- package/dist/registry/package/components/DateRangeFilter/index.tsx +7 -0
- package/dist/registry/package/components/DateRangePicker/index.tsx +7 -0
- package/dist/registry/package/components/DateTimePicker/index.tsx +7 -0
- package/dist/registry/package/components/Descriptions/index.tsx +7 -0
- package/dist/registry/package/components/Divider/index.tsx +7 -0
- package/dist/registry/package/components/Drawer/index.tsx +8 -0
- package/dist/registry/package/components/DurationDisplay/index.tsx +6 -0
- package/dist/registry/package/components/EmptyState/index.tsx +7 -0
- package/dist/registry/package/components/EntityMeta/index.tsx +7 -0
- package/dist/registry/package/components/ErrorState/index.tsx +8 -0
- package/dist/registry/package/components/EventCalendar/index.tsx +7 -0
- package/dist/registry/package/components/Field/index.tsx +12 -0
- package/dist/registry/package/components/FileUpload/index.tsx +7 -0
- package/dist/registry/package/components/FiltersBar/index.tsx +7 -0
- package/dist/registry/package/components/HoverCard/index.tsx +14 -37
- package/dist/registry/package/components/IconButton/index.tsx +8 -0
- package/dist/registry/package/components/Icons/index.tsx +15 -0
- package/dist/registry/package/components/ImageUpload/index.tsx +7 -0
- package/dist/registry/package/components/Input/index.tsx +7 -0
- package/dist/registry/package/components/JsonEditor/index.tsx +7 -0
- package/dist/registry/package/components/KpiGrid/index.tsx +7 -0
- package/dist/registry/package/components/Label/index.tsx +7 -0
- package/dist/registry/package/components/MarkdownEditor/index.tsx +7 -0
- package/dist/registry/package/components/MiniCalendar/index.tsx +7 -0
- package/dist/registry/package/components/Modal/index.tsx +8 -0
- package/dist/registry/package/components/MonthPicker/index.tsx +7 -0
- package/dist/registry/package/components/MultiSelect/index.tsx +7 -0
- package/dist/registry/package/components/OtpInput/index.tsx +7 -0
- package/dist/registry/package/components/Overlay/index.tsx +34 -24
- package/dist/registry/package/components/PageHeader/index.tsx +7 -0
- package/dist/registry/package/components/PermissionGate/index.tsx +6 -0
- package/dist/registry/package/components/Popover/index.tsx +7 -0
- package/dist/registry/package/components/QrCode/index.tsx +7 -0
- package/dist/registry/package/components/RadioGroup/index.tsx +7 -0
- package/dist/registry/package/components/Rating/index.tsx +6 -0
- package/dist/registry/package/components/ReferenceSelect/index.tsx +7 -0
- package/dist/registry/package/components/RelativeTime/index.tsx +6 -0
- package/dist/registry/package/components/ResizablePanel/index.tsx +7 -0
- package/dist/registry/package/components/Resource/index.tsx +514 -0
- package/dist/registry/package/components/RichTextEditor/index.tsx +7 -0
- package/dist/registry/package/components/SearchInput/index.tsx +8 -0
- package/dist/registry/package/components/Select/index.tsx +11 -0
- package/dist/registry/package/components/Sidebar/index.tsx +489 -113
- package/dist/registry/package/components/Sidebar/styles.css +96 -4
- package/dist/registry/package/components/Skeleton/index.tsx +7 -0
- package/dist/registry/package/components/Spinner/index.tsx +7 -0
- package/dist/registry/package/components/StatCard/index.tsx +7 -0
- package/dist/registry/package/components/Statistic/index.tsx +7 -0
- package/dist/registry/package/components/Switch/index.tsx +7 -0
- package/dist/registry/package/components/Tabs/index.tsx +7 -0
- package/dist/registry/package/components/TagsInput/index.tsx +7 -0
- package/dist/registry/package/components/Textarea/index.tsx +7 -0
- package/dist/registry/package/components/TimePicker/index.tsx +7 -0
- package/dist/registry/package/components/TimeRangePicker/index.tsx +6 -0
- package/dist/registry/package/components/Timeline/index.tsx +7 -0
- package/dist/registry/package/components/Timer/index.tsx +7 -0
- package/dist/registry/package/components/Tour/index.tsx +7 -0
- package/dist/registry/package/components/Transfer/index.tsx +7 -0
- package/dist/registry/package/components/Tree/index.tsx +7 -0
- package/dist/registry/package/components/TreeSelect/index.tsx +7 -0
- package/dist/registry/package/components/Typography/index.tsx +8 -0
- package/dist/registry/package/components/UnsavedChangesGuard/index.tsx +8 -0
- package/dist/registry/package/components/WeekPicker/index.tsx +7 -0
- package/dist/registry/package/components/YearPicker/index.tsx +7 -0
- package/dist/registry/registry.json +69 -14
- package/package.json +14 -14
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
import type { Key, ReactNode } from "react";
|
|
2
|
+
import type { PermissionRule } from "@sia-ui/headless";
|
|
3
|
+
import type { ComponentTone } from "@sia-ui/tokens";
|
|
4
|
+
|
|
5
|
+
/** Liste dense, ou grille de cartes. */
|
|
6
|
+
export type DataTableView = "table" | "cards";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Le rôle d'une colonne dans la carte.
|
|
10
|
+
*
|
|
11
|
+
* `title` sert d'en-tête — la première colonne le prend si rien n'est
|
|
12
|
+
* déclaré. `meta` s'affiche en paire libellé / valeur. `hidden` reste au
|
|
13
|
+
* tableau seul : une carte qui recopie douze colonnes n'est plus une carte.
|
|
14
|
+
*/
|
|
15
|
+
export type ColumnCardRole = "title" | "meta" | "hidden";
|
|
16
|
+
|
|
17
|
+
export interface DataTableColumn<T> {
|
|
18
|
+
key: string;
|
|
19
|
+
header: ReactNode;
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Où lire la valeur : une clé de l'objet, ou une fonction pour les valeurs
|
|
23
|
+
* calculées. Sert au rendu par défaut, à la carte et à la recherche locale.
|
|
24
|
+
*/
|
|
25
|
+
accessor?: keyof T | ((row: T) => unknown);
|
|
26
|
+
|
|
27
|
+
/** Rendu personnalisé. Prioritaire sur `accessor`. */
|
|
28
|
+
cell?: (row: T, index: number) => ReactNode;
|
|
29
|
+
|
|
30
|
+
align?: "start" | "center" | "end";
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Largeur suggérée — `120px`, `20%`.
|
|
34
|
+
*
|
|
35
|
+
* En `layout="auto"` (défaut) le navigateur n'y voit qu'une indication.
|
|
36
|
+
* Pour qu'elle soit tenue, passer en `layout="fixed"`.
|
|
37
|
+
*/
|
|
38
|
+
width?: string;
|
|
39
|
+
minWidth?: string;
|
|
40
|
+
|
|
41
|
+
/** Coupe au lieu d'élargir. Indispensable en `layout="fixed"`. */
|
|
42
|
+
truncate?: boolean;
|
|
43
|
+
|
|
44
|
+
/** Retire la colonne du tableau sans toucher à la carte. */
|
|
45
|
+
hidden?: boolean;
|
|
46
|
+
|
|
47
|
+
/** Rend l'en-tête cliquable. Le tri lui-même reste au serveur. */
|
|
48
|
+
sortable?: boolean;
|
|
49
|
+
/** Le champ envoyé au serveur, si différent de `key`. */
|
|
50
|
+
sortKey?: string;
|
|
51
|
+
|
|
52
|
+
/** Place de la colonne dans la carte. */
|
|
53
|
+
card?: ColumnCardRole;
|
|
54
|
+
|
|
55
|
+
headerClassName?: string;
|
|
56
|
+
cellClassName?: string;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export interface RowAction<T> {
|
|
60
|
+
key: string;
|
|
61
|
+
label: ReactNode;
|
|
62
|
+
icon?: ReactNode;
|
|
63
|
+
tone?: ComponentTone;
|
|
64
|
+
|
|
65
|
+
/** Masque l'action pour cette ligne — un solde nul, un envoi déjà annulé. */
|
|
66
|
+
hidden?: (row: T, index: number) => boolean;
|
|
67
|
+
disabled?: (row: T, index: number) => boolean;
|
|
68
|
+
|
|
69
|
+
/** Même grammaire de droits que la navigation. */
|
|
70
|
+
permission?: PermissionRule;
|
|
71
|
+
|
|
72
|
+
/** Demande confirmation. Obligatoire pour tout ce qui détruit. */
|
|
73
|
+
confirm?: {
|
|
74
|
+
title: ReactNode;
|
|
75
|
+
description?: ReactNode;
|
|
76
|
+
confirmLabel?: string;
|
|
77
|
+
cancelLabel?: string;
|
|
78
|
+
destructive?: boolean;
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
onSelect: (row: T, index: number) => void | Promise<void>;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export interface DataTableProps<T> {
|
|
85
|
+
columns: Array<DataTableColumn<T>>;
|
|
86
|
+
data: T[];
|
|
87
|
+
|
|
88
|
+
/** La clé d'une ligne. Sans elle, l'index — à éviter si les lignes bougent. */
|
|
89
|
+
getRowKey?: (row: T, index: number) => Key;
|
|
90
|
+
|
|
91
|
+
/* ─────────────────────────────────────────────── en-tête et recherche */
|
|
92
|
+
|
|
93
|
+
title?: ReactNode;
|
|
94
|
+
description?: ReactNode;
|
|
95
|
+
/** Zone libre : filtres, sélecteur de période. */
|
|
96
|
+
toolbar?: ReactNode;
|
|
97
|
+
/** Actions globales, à droite. */
|
|
98
|
+
actions?: ReactNode;
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Les clés sur lesquelles porte la recherche.
|
|
102
|
+
*
|
|
103
|
+
* Absente, aucun champ n'est affiché : un tableau sans champ déclaré n'a
|
|
104
|
+
* rien à chercher. Le filtrage est local, sur les lignes déjà chargées.
|
|
105
|
+
*/
|
|
106
|
+
searchKeys?: string[];
|
|
107
|
+
searchPlaceholder?: string;
|
|
108
|
+
/** Recherche déléguée. Fournie, le tableau cesse de filtrer lui-même. */
|
|
109
|
+
onSearch?: (query: string) => void;
|
|
110
|
+
|
|
111
|
+
/* ─────────────────────────────────────────────────────── mise en page */
|
|
112
|
+
|
|
113
|
+
layout?: "auto" | "fixed";
|
|
114
|
+
/** Hauteur du corps. Sans elle, `stickyHeader` n'a rien où se coller. */
|
|
115
|
+
maxHeight?: string;
|
|
116
|
+
stickyHeader?: boolean;
|
|
117
|
+
density?: "compact" | "default" | "comfortable";
|
|
118
|
+
/** Un fond alterné, une ligne sur deux. */
|
|
119
|
+
striped?: boolean;
|
|
120
|
+
bordered?: boolean;
|
|
121
|
+
|
|
122
|
+
/* ────────────────────────────────────────────────── actions de ligne */
|
|
123
|
+
|
|
124
|
+
rowActions?: Array<RowAction<T>> | ((row: T, index: number) => Array<RowAction<T>>);
|
|
125
|
+
/** Au-delà, les actions passent dans un menu. */
|
|
126
|
+
inlineActionsLimit?: number;
|
|
127
|
+
actionsHeader?: ReactNode;
|
|
128
|
+
/** `icon` garde la colonne étroite sur un tableau déjà large. */
|
|
129
|
+
actionsDisplay?: "icon" | "label";
|
|
130
|
+
|
|
131
|
+
/** Évalue les règles de permission des actions. */
|
|
132
|
+
can?: (rule: PermissionRule) => boolean;
|
|
133
|
+
|
|
134
|
+
/* ────────────────────────────────────────────────── modes d'affichage */
|
|
135
|
+
|
|
136
|
+
view?: DataTableView;
|
|
137
|
+
defaultView?: DataTableView;
|
|
138
|
+
onViewChange?: (view: DataTableView) => void;
|
|
139
|
+
showViewToggle?: boolean;
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Bascule en cartes sous le point de rupture. Actif par défaut : un
|
|
143
|
+
* tableau à sept colonnes est illisible sur un téléphone.
|
|
144
|
+
*/
|
|
145
|
+
responsive?: boolean;
|
|
146
|
+
cardsBreakpoint?: number;
|
|
147
|
+
|
|
148
|
+
/** Rendu complet d'une carte. Sans lui, elle est déduite des colonnes. */
|
|
149
|
+
renderCard?: (row: T, index: number) => ReactNode;
|
|
150
|
+
|
|
151
|
+
/* ──────────────────────────────────────────────────────────── états */
|
|
152
|
+
|
|
153
|
+
loading?: boolean;
|
|
154
|
+
/** Lignes fantômes pendant le premier chargement. */
|
|
155
|
+
skeletonRows?: number;
|
|
156
|
+
error?: ReactNode;
|
|
157
|
+
onRetry?: () => void;
|
|
158
|
+
empty?: {
|
|
159
|
+
title?: ReactNode;
|
|
160
|
+
description?: ReactNode;
|
|
161
|
+
action?: ReactNode;
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
/* ───────────────────────────────────────────────────────── sélection */
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Ajoute une colonne de cases à cocher.
|
|
168
|
+
*
|
|
169
|
+
* Sur une liste paginée côté serveur, la case d'en-tête ne porte que sur la
|
|
170
|
+
* page affichée : le composant ne connaît pas les lignes qu'il n'a pas
|
|
171
|
+
* reçues, et prétendre « tout sélectionner » serait un mensonge.
|
|
172
|
+
*/
|
|
173
|
+
selectable?: boolean;
|
|
174
|
+
selectedKeys?: Key[];
|
|
175
|
+
defaultSelectedKeys?: Key[];
|
|
176
|
+
onSelectionChange?: (keys: Key[], rows: T[]) => void;
|
|
177
|
+
isRowSelectable?: (row: T, index: number) => boolean;
|
|
178
|
+
/** Barre affichée dès qu'une ligne est cochée. */
|
|
179
|
+
selectionActions?: (context: {
|
|
180
|
+
keys: Key[];
|
|
181
|
+
rows: T[];
|
|
182
|
+
clear: () => void;
|
|
183
|
+
}) => ReactNode;
|
|
184
|
+
|
|
185
|
+
/* ───────────────────────────────────────────────────────────── tri */
|
|
186
|
+
|
|
187
|
+
sort?: { field: string; direction: "asc" | "desc" } | null;
|
|
188
|
+
onSortChange?: (field: string) => void;
|
|
189
|
+
|
|
190
|
+
onRowClick?: (row: T, index: number) => void;
|
|
191
|
+
rowClassName?: (row: T, index: number) => string | undefined;
|
|
192
|
+
caption?: string;
|
|
193
|
+
className?: string;
|
|
194
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
import type { DataTableColumn } from "./types";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Lire une colonne.
|
|
6
|
+
*
|
|
7
|
+
* Tenu à part du tableau parce que trois endroits en ont besoin : le tableau
|
|
8
|
+
* lui-même, la carte, et la vue de détail de `CrudPage`. Recopié, ce petit
|
|
9
|
+
* morceau se serait mis à diverger — la vue de détail aurait ignoré les
|
|
10
|
+
* `accessor` calculés, et personne ne l'aurait remarqué avant de voir une
|
|
11
|
+
* ligne vide dans un modal.
|
|
12
|
+
*/
|
|
13
|
+
export function valeurDe<T>(row: T, column: DataTableColumn<T>): unknown {
|
|
14
|
+
if (typeof column.accessor === "function") return column.accessor(row);
|
|
15
|
+
if (column.accessor !== undefined) return row[column.accessor];
|
|
16
|
+
return (row as Record<string, unknown>)[column.key];
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** Ce qu'une cellule affiche : son rendu propre, ou sa valeur en texte. */
|
|
20
|
+
export function renduCellule<T>(
|
|
21
|
+
row: T,
|
|
22
|
+
index: number,
|
|
23
|
+
column: DataTableColumn<T>,
|
|
24
|
+
): ReactNode {
|
|
25
|
+
if (column.cell) return column.cell(row, index);
|
|
26
|
+
const valeur = valeurDe(row, column);
|
|
27
|
+
return valeur === null || valeur === undefined ? null : String(valeur);
|
|
28
|
+
}
|
|
@@ -40,6 +40,13 @@ function defaultFormat(value: string, locale: string) {
|
|
|
40
40
|
return Number.isNaN(date.getTime()) ? value : new Intl.DateTimeFormat(locale, { day: "2-digit", month: "short", year: "numeric" }).format(date);
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
+
/**
|
|
44
|
+
* Une date, choisie au calendrier ou saisie au clavier.
|
|
45
|
+
*
|
|
46
|
+
* La valeur est une chaîne `AAAA-MM-JJ` : un objet `Date` porte une heure
|
|
47
|
+
* et un fuseau dont une date d'échéance n'a que faire, et qui la décalent
|
|
48
|
+
* d'un jour selon l'endroit d'où on la lit.
|
|
49
|
+
*/
|
|
43
50
|
export const DatePicker = forwardRef<HTMLButtonElement, DatePickerProps>(function DatePicker({
|
|
44
51
|
id,
|
|
45
52
|
name,
|
|
@@ -5,4 +5,11 @@ import "./styles.css";
|
|
|
5
5
|
|
|
6
6
|
export interface DateRangePreset { label: ReactNode; value: DateRangeValue; }
|
|
7
7
|
export interface DateRangeFilterProps { value: DateRangeValue; onValueChange: (value: DateRangeValue) => void; presets?: DateRangePreset[]; onClear?: () => void; className?: string; }
|
|
8
|
+
/**
|
|
9
|
+
* Une période, avec ses raccourcis.
|
|
10
|
+
*
|
|
11
|
+
* Les périodes utiles se comptent sur une main — ce mois, le trimestre,
|
|
12
|
+
* l'exercice — et les proposer évite de saisir deux dates pour la question
|
|
13
|
+
* que tout le monde pose en premier.
|
|
14
|
+
*/
|
|
8
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}>Effacer</button>}</section>; }
|
|
@@ -21,6 +21,13 @@ export interface DateRangePickerProps extends Pick<CalendarProps, "locale" | "fi
|
|
|
21
21
|
disabled?: boolean;
|
|
22
22
|
className?: string;
|
|
23
23
|
}
|
|
24
|
+
/**
|
|
25
|
+
* Un début et une fin, choisis ensemble.
|
|
26
|
+
*
|
|
27
|
+
* Deux champs séparés laissent poser une fin antérieure au début, et il
|
|
28
|
+
* faut alors décider lequel des deux avait tort. Ici les deux bornes sont
|
|
29
|
+
* une seule valeur, et le calendrier les tient dans l'ordre.
|
|
30
|
+
*/
|
|
24
31
|
export function DateRangePicker({ value, defaultValue = { start: "", end: "" }, onValueChange, min, max, startLabel = "Date de début", endLabel = "Date de fin", allowClear = true, placement = "bottom-start", tone = "primary", invalid, disabled, locale = "fr-FR", firstDayOfWeek = 1, disabledDate, cellRender, showWeek, className }: DateRangePickerProps) {
|
|
25
32
|
const anchorRef = useRef<HTMLButtonElement>(null);
|
|
26
33
|
const [internal, setInternal] = useState(defaultValue);
|
|
@@ -23,6 +23,13 @@ export interface DateTimePickerProps {
|
|
|
23
23
|
timeProps?: Omit<TimePickerProps, "value" | "defaultValue" | "onValueChange" | "tone" | "invalid" | "disabled">;
|
|
24
24
|
className?: string;
|
|
25
25
|
}
|
|
26
|
+
/**
|
|
27
|
+
* Une date et une heure, dans un seul champ.
|
|
28
|
+
*
|
|
29
|
+
* `needConfirm` existe parce que l'heure se choisit après la date : sans
|
|
30
|
+
* validation explicite, le panneau se referme sur la date et l'heure reste
|
|
31
|
+
* celle de la veille.
|
|
32
|
+
*/
|
|
26
33
|
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) {
|
|
27
34
|
const [internal, setInternal] = useState(defaultValue);
|
|
28
35
|
const current = value ?? internal;
|
|
@@ -4,4 +4,11 @@ import "./styles.css";
|
|
|
4
4
|
|
|
5
5
|
export interface DescriptionItem { key: string; label: ReactNode; value: ReactNode; span?: 1 | 2 | 3; }
|
|
6
6
|
export interface DescriptionsProps { items: DescriptionItem[]; title?: ReactNode; columns?: 1 | 2 | 3; bordered?: boolean; className?: string; }
|
|
7
|
+
/**
|
|
8
|
+
* Des paires libellé / valeur, en colonnes.
|
|
9
|
+
*
|
|
10
|
+
* C'est la forme d'une fiche en lecture — ce qu'un formulaire montre quand
|
|
11
|
+
* il n'y a rien à saisir. `span` laisse une valeur longue occuper toute la
|
|
12
|
+
* ligne plutôt que d'être coupée dans une colonne étroite.
|
|
13
|
+
*/
|
|
7
14
|
export function Descriptions({ items, title, columns = 2, bordered = false, className }: DescriptionsProps) { return <section className={cn("sia-descriptions", bordered && "sia-descriptions--bordered", className)}>{title && <h3>{title}</h3>}<dl className={`sia-descriptions__grid sia-descriptions__grid--${columns}`}>{items.map((item) => <div key={item.key} style={{ gridColumn: `span ${item.span ?? 1}` }}><dt>{item.label}</dt><dd>{item.value}</dd></div>)}</dl></section>; }
|
|
@@ -7,6 +7,13 @@ export interface DividerProps extends HTMLAttributes<HTMLDivElement> {
|
|
|
7
7
|
decorative?: boolean;
|
|
8
8
|
}
|
|
9
9
|
|
|
10
|
+
/**
|
|
11
|
+
* Un trait entre deux choses.
|
|
12
|
+
*
|
|
13
|
+
* `decorative` décide s'il compte pour un lecteur d'écran. Un trait qui
|
|
14
|
+
* sépare deux groupes de sens est une information; un trait qui aère ne
|
|
15
|
+
* l'est pas, et l'annoncer ajoute du bruit à chaque parcours.
|
|
16
|
+
*/
|
|
10
17
|
export function Divider({ orientation = "horizontal", decorative = true, className, ...props }: DividerProps) {
|
|
11
18
|
return <div className={cn("sia-divider", `sia-divider--${orientation}`, className)} role={decorative ? "presentation" : "separator"} aria-orientation={decorative ? undefined : orientation} {...props} />;
|
|
12
19
|
}
|
|
@@ -46,6 +46,14 @@ export type DrawerComponent = ForwardRefExoticComponent<
|
|
|
46
46
|
Close: typeof DrawerClose;
|
|
47
47
|
};
|
|
48
48
|
|
|
49
|
+
/**
|
|
50
|
+
* Un panneau qui entre par un bord.
|
|
51
|
+
*
|
|
52
|
+
* Il sert là où une boîte modale serait trop : un filtre qu'on ajuste en
|
|
53
|
+
* regardant la liste derrière, un formulaire qu'on remplit sans perdre sa
|
|
54
|
+
* place. Le focus y est enfermé tant qu'il est ouvert, comme dans un
|
|
55
|
+
* modal — sans quoi la tabulation continue dans la page cachée.
|
|
56
|
+
*/
|
|
49
57
|
export const Drawer = forwardRef<DrawerRef, IDrawerProps>(
|
|
50
58
|
(
|
|
51
59
|
{
|
|
@@ -1,3 +1,9 @@
|
|
|
1
1
|
import { cn } from "@sia-ui/utils";
|
|
2
2
|
export interface DurationDisplayProps { milliseconds: number; locale?: string; style?: "short" | "long" | "clock"; className?: string; }
|
|
3
|
+
/**
|
|
4
|
+
* Une durée, en mots ou en pendule.
|
|
5
|
+
*
|
|
6
|
+
* Trois styles pour trois usages : `clock` pour un chronomètre qu'on lit en
|
|
7
|
+
* continu, `short` pour une colonne de tableau, `long` pour une phrase.
|
|
8
|
+
*/
|
|
3
9
|
export function DurationDisplay({ milliseconds, locale = "fr-FR", style = "short", className }: DurationDisplayProps) { const totalSeconds = Math.max(0, Math.floor(milliseconds / 1000)); const hours = Math.floor(totalSeconds / 3600); const minutes = Math.floor((totalSeconds % 3600) / 60); const seconds = totalSeconds % 60; if (style === "clock") return <span className={cn("sia-duration-display", className)}>{[hours, minutes, seconds].map((value) => String(value).padStart(2, "0")).join(":")}</span>; const formatter = new Intl.NumberFormat(locale); const parts = [[hours, style === "long" ? "heure" : "h"], [minutes, style === "long" ? "minute" : "min"], [seconds, style === "long" ? "seconde" : "s"]] as const; return <span className={cn("sia-duration-display", className)}>{parts.filter(([value]) => value > 0).map(([value, unit]) => `${formatter.format(value)} ${unit}${style === "long" && value > 1 ? "s" : ""}`).join(" ") || `0 ${style === "long" ? "seconde" : "s"}`}</span>; }
|
|
@@ -11,6 +11,13 @@ export interface EmptyStateProps {
|
|
|
11
11
|
className?: string;
|
|
12
12
|
}
|
|
13
13
|
|
|
14
|
+
/**
|
|
15
|
+
* Ce qu'on montre quand il n'y a rien.
|
|
16
|
+
*
|
|
17
|
+
* Une liste vide sans explication ressemble à un chargement bloqué. Elle
|
|
18
|
+
* porte donc une action : le vide initial est le meilleur moment pour
|
|
19
|
+
* proposer de créer le premier élément.
|
|
20
|
+
*/
|
|
14
21
|
export function EmptyState({ icon, title, description, action, compact, className }: EmptyStateProps) {
|
|
15
22
|
return (
|
|
16
23
|
<section className={cn("sia-empty-state", compact && "sia-empty-state--compact", className)}>
|
|
@@ -32,6 +32,13 @@ function Detail({
|
|
|
32
32
|
);
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
+
/**
|
|
36
|
+
* Les traces d'un objet : créé le, modifié par, identifiant.
|
|
37
|
+
*
|
|
38
|
+
* L'identifiant est normalisé pour être lu à voix haute au téléphone —
|
|
39
|
+
* c'est la première chose qu'on demande à un client, et une chaîne de
|
|
40
|
+
* trente-six caractères ne se dicte pas.
|
|
41
|
+
*/
|
|
35
42
|
export function EntityMeta({
|
|
36
43
|
createdAt,
|
|
37
44
|
updatedAt,
|
|
@@ -21,6 +21,14 @@ function getErrorMessage(error: unknown): string | undefined {
|
|
|
21
21
|
return undefined;
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
+
/**
|
|
25
|
+
* Un échec, et de quoi en sortir.
|
|
26
|
+
*
|
|
27
|
+
* `error` accepte n'importe quoi : une `Error`, une réponse HTTP, une
|
|
28
|
+
* chaîne. Ce qui arrive d'un appel réseau n'a pas de forme garantie, et
|
|
29
|
+
* exiger un type ferait écrire un `try` autour de chaque affichage
|
|
30
|
+
* d'erreur.
|
|
31
|
+
*/
|
|
24
32
|
export function ErrorState({ title: titleProp, description, error, action, onRetry, retryLabel: retryLabelProp, compact, className }: ErrorStateProps) {
|
|
25
33
|
const locale = useSiaLocale();
|
|
26
34
|
const retryLabel = retryLabelProp ?? locale.retry;
|
|
@@ -4,5 +4,12 @@ import "./styles.css";
|
|
|
4
4
|
|
|
5
5
|
export interface CalendarEvent { id: string; date: string; title: string; tone?: ComponentTone; }
|
|
6
6
|
export interface EventCalendarProps extends Omit<CalendarProps, "renderDay"> { events: CalendarEvent[]; maxEventsPerDay?: number; onEventClick?: (event: CalendarEvent) => void; }
|
|
7
|
+
/**
|
|
8
|
+
* Un calendrier qui porte des événements.
|
|
9
|
+
*
|
|
10
|
+
* `maxEventsPerDay` plafonne l'affichage : une journée à douze rendez-vous
|
|
11
|
+
* déformerait toute la grille, et les douze ne se lisent de toute façon pas
|
|
12
|
+
* dans une case de calendrier.
|
|
13
|
+
*/
|
|
7
14
|
export function EventCalendar({ events, maxEventsPerDay = 2, onEventClick, ...props }: EventCalendarProps) { const renderDay = (day: CalendarDay) => { const matches = events.filter((event) => event.date === day.date).slice(0, maxEventsPerDay); return <span className="sia-event-calendar__events">{matches.map((event) => <button type="button" key={event.id} className={`sia-event-calendar__event sia-event-calendar__event--${event.tone ?? "primary"}`} onClick={() => onEventClick?.(event)}>{event.title}</button>)}</span>; }; return <Calendar {...props} className={cnEvent(props.className)} renderDay={renderDay} />; }
|
|
8
15
|
function cnEvent(className?: string) { return ["sia-event-calendar", className].filter(Boolean).join(" "); }
|
|
@@ -559,6 +559,18 @@ function pick<K extends string, V>(key: K, value: V | undefined) {
|
|
|
559
559
|
return (value === undefined ? {} : { [key]: value }) as { [P in K]?: V };
|
|
560
560
|
}
|
|
561
561
|
|
|
562
|
+
/**
|
|
563
|
+
* Le champ, et tout ce qui l'entoure.
|
|
564
|
+
*
|
|
565
|
+
* Un libellé, un contrôle, un message d'erreur, et les attributs qui les
|
|
566
|
+
* relient — `id`, `aria-describedby`, `aria-invalid`. Ce câblage est
|
|
567
|
+
* toujours le même et toujours oublié quelque part : ici il est fait une
|
|
568
|
+
* fois.
|
|
569
|
+
*
|
|
570
|
+
* `type` choisit le contrôle parmi les vingt-sept que la bibliothèque
|
|
571
|
+
* fournit. Le champ ne les réimplémente pas : il les monte et leur passe le
|
|
572
|
+
* branchement.
|
|
573
|
+
*/
|
|
562
574
|
export function Field(rawProps: FieldProps) {
|
|
563
575
|
const props = applyBinding(rawProps);
|
|
564
576
|
const {
|
|
@@ -15,6 +15,13 @@ export interface FileUploadProps extends Omit<
|
|
|
15
15
|
description?: string;
|
|
16
16
|
onFilesChange?: (files: File[]) => void;
|
|
17
17
|
}
|
|
18
|
+
/**
|
|
19
|
+
* Un dépôt de fichiers, par clic ou par glisser.
|
|
20
|
+
*
|
|
21
|
+
* Le glisser-déposer seul exclut le clavier et le tactile; le bouton seul
|
|
22
|
+
* ignore le geste que tout le monde essaie d'abord. Les deux mènent au même
|
|
23
|
+
* champ natif.
|
|
24
|
+
*/
|
|
18
25
|
export function FileUpload({
|
|
19
26
|
label = "Ajouter des fichiers",
|
|
20
27
|
description = "Glissez les fichiers ici ou parcourez votre appareil.",
|
|
@@ -13,6 +13,13 @@ export interface FiltersBarProps {
|
|
|
13
13
|
className?: string;
|
|
14
14
|
}
|
|
15
15
|
|
|
16
|
+
/**
|
|
17
|
+
* La barre au-dessus d'une liste : recherche, filtres, actions.
|
|
18
|
+
*
|
|
19
|
+
* `activeCount` affiche combien de filtres sont en vigueur. Sans ce compte,
|
|
20
|
+
* une liste filtrée ressemble à une liste vide, et l'on cherche la panne
|
|
21
|
+
* avant de penser au filtre posé la veille.
|
|
22
|
+
*/
|
|
16
23
|
export function FiltersBar({ search, filters, actions, activeCount = 0, onReset, resetLabel: resetLabelProp, className }: FiltersBarProps) {
|
|
17
24
|
const locale = useSiaLocale();
|
|
18
25
|
const resetLabel = resetLabelProp ?? locale.reset;
|
|
@@ -1,14 +1,12 @@
|
|
|
1
1
|
import {
|
|
2
2
|
cloneElement,
|
|
3
3
|
isValidElement,
|
|
4
|
-
useCallback,
|
|
5
|
-
useEffect,
|
|
6
4
|
useRef,
|
|
7
|
-
useState,
|
|
8
5
|
type ReactElement,
|
|
9
6
|
type ReactNode,
|
|
10
7
|
} from "react";
|
|
11
8
|
import { cn } from "@sia-ui/utils";
|
|
9
|
+
import { useHoverIntent } from "@sia-ui/headless";
|
|
12
10
|
import { Overlay, type OverlayPlacement } from "../Overlay";
|
|
13
11
|
import "./styles.css";
|
|
14
12
|
|
|
@@ -48,49 +46,29 @@ export function HoverCard({
|
|
|
48
46
|
className,
|
|
49
47
|
}: HoverCardProps) {
|
|
50
48
|
const anchorRef = useRef<HTMLElement>(null);
|
|
51
|
-
const timer = useRef<ReturnType<typeof setTimeout> | undefined>(undefined);
|
|
52
|
-
const [internalOpen, setInternalOpen] = useState(false);
|
|
53
|
-
const open = openProp ?? internalOpen;
|
|
54
49
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
(next: boolean, delay: number) => {
|
|
65
|
-
if (timer.current) clearTimeout(timer.current);
|
|
66
|
-
timer.current = setTimeout(() => setOpen(next), delay);
|
|
67
|
-
},
|
|
68
|
-
[setOpen],
|
|
69
|
-
);
|
|
70
|
-
|
|
71
|
-
useEffect(
|
|
72
|
-
() => () => {
|
|
73
|
-
if (timer.current) clearTimeout(timer.current);
|
|
74
|
-
},
|
|
75
|
-
[],
|
|
76
|
-
);
|
|
50
|
+
// Les deux délais vivent dans `@sia-ui/headless` : le volet de navigation
|
|
51
|
+
// du `Sidebar` pose exactement la même question, et deux réglages qui
|
|
52
|
+
// divergent donnent deux sensations différentes dans la même application.
|
|
53
|
+
const { open, triggerProps, panelProps, setOpen } = useHoverIntent({
|
|
54
|
+
openDelay,
|
|
55
|
+
closeDelay,
|
|
56
|
+
...(openProp !== undefined ? { open: openProp } : {}),
|
|
57
|
+
...(onOpenChange ? { onOpenChange } : {}),
|
|
58
|
+
});
|
|
77
59
|
|
|
78
60
|
const trigger = isValidElement(children)
|
|
79
61
|
? cloneElement(children, {
|
|
80
62
|
ref: anchorRef,
|
|
81
|
-
|
|
82
|
-
// fiche qu'on ne peut atteindre qu'à la souris n'existe pas pour
|
|
83
|
-
// tout le monde.
|
|
63
|
+
...triggerProps,
|
|
84
64
|
onFocus: () => {
|
|
85
65
|
children.props.onFocus?.();
|
|
86
|
-
|
|
66
|
+
triggerProps.onFocus();
|
|
87
67
|
},
|
|
88
|
-
onBlur: () => schedule(false, closeDelay),
|
|
89
68
|
onMouseEnter: () => {
|
|
90
69
|
children.props.onMouseEnter?.();
|
|
91
|
-
|
|
70
|
+
triggerProps.onMouseEnter();
|
|
92
71
|
},
|
|
93
|
-
onMouseLeave: () => schedule(false, closeDelay),
|
|
94
72
|
} as never)
|
|
95
73
|
: children;
|
|
96
74
|
|
|
@@ -110,8 +88,7 @@ export function HoverCard({
|
|
|
110
88
|
className="sia-hover-card__body"
|
|
111
89
|
// Survoler le panneau le maintient ouvert : sans cela il se ferme
|
|
112
90
|
// dès que la souris quitte le déclencheur pour y aller.
|
|
113
|
-
|
|
114
|
-
onMouseLeave={() => schedule(false, closeDelay)}
|
|
91
|
+
{...panelProps}
|
|
115
92
|
>
|
|
116
93
|
{content}
|
|
117
94
|
</div>
|
|
@@ -13,6 +13,14 @@ export interface IconButtonProps extends ButtonHTMLAttributes<HTMLButtonElement>
|
|
|
13
13
|
tooltip?: Omit<TooltipProps, "children">;
|
|
14
14
|
}
|
|
15
15
|
|
|
16
|
+
/**
|
|
17
|
+
* Une action réduite à son icône.
|
|
18
|
+
*
|
|
19
|
+
* `label` est obligatoire : une icône seule n'a pas de nom, et un lecteur
|
|
20
|
+
* d'écran annoncerait « bouton » sans dire lequel. Le libellé sert aussi
|
|
21
|
+
* d'infobulle — ce que l'icône signifie doit rester atteignable à la
|
|
22
|
+
* souris comme au clavier.
|
|
23
|
+
*/
|
|
16
24
|
export const IconButton = forwardRef<HTMLButtonElement, IconButtonProps>(
|
|
17
25
|
({ label, icon, variant: variantProp, size: sizeProp, radius: radiusProp, tooltip, className, type = "button", ...props }, ref) => {
|
|
18
26
|
const defaults = useComponentDefaults<IconButtonProps>("iconButton");
|
|
@@ -1,3 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Le jeu d'icônes commun.
|
|
3
|
+
*
|
|
4
|
+
* Il n'a pas de composant principal : c'est une collection, et chaque icône y
|
|
5
|
+
* est une fonction. Toutes passent par le même socle — un trait de 1,8, des
|
|
6
|
+
* extrémités arrondies, une taille en `em` qui suit celle du texte, et un
|
|
7
|
+
* `aria-hidden` d'office. Une icône ne porte jamais de sens à elle seule;
|
|
8
|
+
* c'est le libellé à côté qui le porte.
|
|
9
|
+
*
|
|
10
|
+
* Ce jeu couvre ce dont la bibliothèque a besoin, et rien de plus. Un produit
|
|
11
|
+
* qui veut sa propre iconographie passe ses icônes en props plutôt que
|
|
12
|
+
* d'étendre celle-ci : embarquer trois cents dessins pour en servir douze
|
|
13
|
+
* pèserait sur tous les projets.
|
|
14
|
+
*/
|
|
15
|
+
|
|
1
16
|
import type { SVGProps } from "react";
|
|
2
17
|
|
|
3
18
|
type IconProps = SVGProps<SVGSVGElement>;
|
|
@@ -3,4 +3,11 @@ import { FileUpload, type FileUploadProps } from "../FileUpload";
|
|
|
3
3
|
import { cn } from "@sia-ui/utils";
|
|
4
4
|
import "./styles.css";
|
|
5
5
|
export interface ImageUploadProps extends Omit<FileUploadProps, "accept" | "onFilesChange"> { accept?: string; value?: string; onValueChange?: (file: File | null, previewUrl?: string) => void; previewAlt?: string; }
|
|
6
|
+
/**
|
|
7
|
+
* Un dépôt de fichier qui montre ce qu'on vient de choisir.
|
|
8
|
+
*
|
|
9
|
+
* L'aperçu est local, construit avant tout envoi : attendre le retour du
|
|
10
|
+
* serveur pour afficher l'image laisse un carré vide au moment précis où
|
|
11
|
+
* l'on veut vérifier qu'on n'a pas pris la mauvaise.
|
|
12
|
+
*/
|
|
6
13
|
export function ImageUpload({ accept = "image/*", value, onValueChange, previewAlt = "Aperçu de l'image", className, ...props }: ImageUploadProps) { const [preview, setPreview] = useState(value); useEffect(() => setPreview(value), [value]); useEffect(() => () => { if (preview?.startsWith("blob:")) URL.revokeObjectURL(preview); }, [preview]); const select = (files: File[]) => { const file = files[0]; if (!file) return; const next = URL.createObjectURL(file); setPreview(next); onValueChange?.(file, next); }; return <div className={cn("sia-image-upload", className)}>{preview && <div className="sia-image-upload__preview"><img src={preview} alt={previewAlt} /><button type="button" onClick={() => { setPreview(undefined); onValueChange?.(null); }}>Supprimer</button></div>}<FileUpload {...props} accept={accept} multiple={false} onFilesChange={select} /></div>; }
|
|
@@ -23,6 +23,13 @@ export interface InputProps extends InputHTMLAttributes<HTMLInputElement> {
|
|
|
23
23
|
radius?: "default" | "none" | "sm" | "lg" | "full";
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
+
/**
|
|
27
|
+
* La saisie d'une ligne de texte.
|
|
28
|
+
*
|
|
29
|
+
* `left` et `right` accueillent ce qui accompagne la valeur — une unité,
|
|
30
|
+
* une icône de recherche, un bouton de réinitialisation — à l'intérieur du
|
|
31
|
+
* cadre. Posés à côté, ils s'en désalignent dès que la taille change.
|
|
32
|
+
*/
|
|
26
33
|
export const Input = forwardRef<HTMLInputElement, InputProps>(
|
|
27
34
|
(
|
|
28
35
|
{
|
|
@@ -3,4 +3,11 @@ import { Textarea } from "../Textarea";
|
|
|
3
3
|
import { cn } from "@sia-ui/utils";
|
|
4
4
|
import "./styles.css";
|
|
5
5
|
export interface JsonEditorProps extends Omit<TextareaHTMLAttributes<HTMLTextAreaElement>, "value" | "defaultValue" | "onChange"> { value?: string; defaultValue?: string; onValueChange?: (value: string, parsed: unknown | undefined) => void; formatOnBlur?: boolean; }
|
|
6
|
+
/**
|
|
7
|
+
* Une zone de saisie qui connaît la forme de ce qu'on y écrit.
|
|
8
|
+
*
|
|
9
|
+
* Elle signale une syntaxe invalide pendant la frappe et rend l'objet
|
|
10
|
+
* analysé à côté du texte : l'appelant reçoit donc les deux, et n'a pas à
|
|
11
|
+
* refaire un `JSON.parse` dans un `try`.
|
|
12
|
+
*/
|
|
6
13
|
export function JsonEditor({ value, defaultValue = "{}", onValueChange, formatOnBlur = true, className, ...props }: JsonEditorProps) { const [internal, setInternal] = useState(defaultValue); const current = value ?? internal; const result = useMemo(() => { try { return { valid: true, parsed: JSON.parse(current) as unknown }; } catch { return { valid: false, parsed: undefined }; } }, [current]); const update = (next: string) => { let parsed: unknown | undefined; try { parsed = JSON.parse(next) as unknown; } catch { parsed = undefined; } if (value === undefined) setInternal(next); onValueChange?.(next, parsed); }; return <div className={cn("sia-code-editor", className)}><Textarea {...props} value={current} invalid={!result.valid} resize="vertical" spellCheck={false} onChange={(event) => update(event.target.value)} onBlur={(event) => { if (formatOnBlur && result.valid) update(JSON.stringify(result.parsed, null, 2)); props.onBlur?.(event); }} /><small className={result.valid ? "is-valid" : "is-invalid"}>{result.valid ? "JSON valide" : "JSON invalide"}</small></div>; }
|
|
@@ -6,6 +6,13 @@ export interface KpiGridProps extends HTMLAttributes<HTMLDivElement> {
|
|
|
6
6
|
columns?: 1 | 2 | 3 | 4;
|
|
7
7
|
}
|
|
8
8
|
|
|
9
|
+
/**
|
|
10
|
+
* Une rangée d'indicateurs.
|
|
11
|
+
*
|
|
12
|
+
* Le nombre de colonnes est déclaré, pas déduit du nombre de cartes : trois
|
|
13
|
+
* indicateurs sur quatre colonnes laissent un trou, et ce trou dit qu'il
|
|
14
|
+
* manque une donnée.
|
|
15
|
+
*/
|
|
9
16
|
export function KpiGrid({ columns = 4, className, ...props }: KpiGridProps) {
|
|
10
17
|
return <div className={cn("sia-kpi-grid", `sia-kpi-grid--${columns}`, className)} {...props} />;
|
|
11
18
|
}
|
|
@@ -16,6 +16,13 @@ export interface LabelProps
|
|
|
16
16
|
/** Alias historique, attendu par les composants venus du registre. */
|
|
17
17
|
export type ILabelProps = LabelProps;
|
|
18
18
|
|
|
19
|
+
/**
|
|
20
|
+
* Le nom d'un champ.
|
|
21
|
+
*
|
|
22
|
+
* L'astérisque d'obligation est portée par une prop plutôt qu'écrite dans
|
|
23
|
+
* le texte : elle reçoit ainsi un `aria-hidden`, et le champ est annoncé
|
|
24
|
+
* « obligatoire » plutôt que « nom étoile ».
|
|
25
|
+
*/
|
|
19
26
|
export const Label = React.forwardRef<HTMLLabelElement, LabelProps>(
|
|
20
27
|
({ className, children, required, muted, tone, ...props }, ref) => {
|
|
21
28
|
return (
|