@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.
Files changed (100) hide show
  1. package/LICENSE +21 -21
  2. package/dist/index.js +2 -0
  3. package/dist/registry/package/components/Alert/index.tsx +7 -0
  4. package/dist/registry/package/components/AmountDisplay/index.tsx +8 -0
  5. package/dist/registry/package/components/AppShell/index.tsx +413 -3
  6. package/dist/registry/package/components/AppShell/styles.css +259 -7
  7. package/dist/registry/package/components/AppShell/tab-bar.tsx +134 -0
  8. package/dist/registry/package/components/AsyncState/index.tsx +7 -0
  9. package/dist/registry/package/components/AuditMeta/index.tsx +8 -0
  10. package/dist/registry/package/components/AuthLayout/index.tsx +7 -0
  11. package/dist/registry/package/components/Autocomplete/index.tsx +7 -0
  12. package/dist/registry/package/components/Avatar/index.tsx +333 -12
  13. package/dist/registry/package/components/Avatar/styles.css +236 -5
  14. package/dist/registry/package/components/Badge/index.tsx +7 -0
  15. package/dist/registry/package/components/Button/index.tsx +7 -0
  16. package/dist/registry/package/components/Calendar/index.tsx +11 -0
  17. package/dist/registry/package/components/Card/index.tsx +8 -0
  18. package/dist/registry/package/components/Checkbox/index.tsx +8 -0
  19. package/dist/registry/package/components/ColorPicker/index.tsx +7 -0
  20. package/dist/registry/package/components/CommandPalette/index.tsx +7 -0
  21. package/dist/registry/package/components/ConfirmDialog/index.tsx +7 -0
  22. package/dist/registry/package/components/Container/index.tsx +7 -0
  23. package/dist/registry/package/components/Countdown/index.tsx +7 -0
  24. package/dist/registry/package/components/CrudPage/dialogs.tsx +230 -0
  25. package/dist/registry/package/components/CrudPage/index.tsx +546 -5
  26. package/dist/registry/package/components/CrudPage/styles.css +25 -1
  27. package/dist/registry/package/components/CurrencyInput/index.tsx +7 -0
  28. package/dist/registry/package/components/DataTable/index.tsx +548 -200
  29. package/dist/registry/package/components/DataTable/query.ts +68 -0
  30. package/dist/registry/package/components/DataTable/row-actions.tsx +158 -0
  31. package/dist/registry/package/components/DataTable/styles.css +285 -73
  32. package/dist/registry/package/components/DataTable/types.ts +194 -0
  33. package/dist/registry/package/components/DataTable/values.ts +28 -0
  34. package/dist/registry/package/components/DatePicker/index.tsx +7 -0
  35. package/dist/registry/package/components/DateRangeFilter/index.tsx +7 -0
  36. package/dist/registry/package/components/DateRangePicker/index.tsx +7 -0
  37. package/dist/registry/package/components/DateTimePicker/index.tsx +7 -0
  38. package/dist/registry/package/components/Descriptions/index.tsx +7 -0
  39. package/dist/registry/package/components/Divider/index.tsx +7 -0
  40. package/dist/registry/package/components/Drawer/index.tsx +8 -0
  41. package/dist/registry/package/components/DurationDisplay/index.tsx +6 -0
  42. package/dist/registry/package/components/EmptyState/index.tsx +7 -0
  43. package/dist/registry/package/components/EntityMeta/index.tsx +7 -0
  44. package/dist/registry/package/components/ErrorState/index.tsx +8 -0
  45. package/dist/registry/package/components/EventCalendar/index.tsx +7 -0
  46. package/dist/registry/package/components/Field/index.tsx +12 -0
  47. package/dist/registry/package/components/FileUpload/index.tsx +7 -0
  48. package/dist/registry/package/components/FiltersBar/index.tsx +7 -0
  49. package/dist/registry/package/components/HoverCard/index.tsx +14 -37
  50. package/dist/registry/package/components/IconButton/index.tsx +8 -0
  51. package/dist/registry/package/components/Icons/index.tsx +15 -0
  52. package/dist/registry/package/components/ImageUpload/index.tsx +7 -0
  53. package/dist/registry/package/components/Input/index.tsx +7 -0
  54. package/dist/registry/package/components/JsonEditor/index.tsx +7 -0
  55. package/dist/registry/package/components/KpiGrid/index.tsx +7 -0
  56. package/dist/registry/package/components/Label/index.tsx +7 -0
  57. package/dist/registry/package/components/MarkdownEditor/index.tsx +7 -0
  58. package/dist/registry/package/components/MiniCalendar/index.tsx +7 -0
  59. package/dist/registry/package/components/Modal/index.tsx +8 -0
  60. package/dist/registry/package/components/MonthPicker/index.tsx +7 -0
  61. package/dist/registry/package/components/MultiSelect/index.tsx +7 -0
  62. package/dist/registry/package/components/OtpInput/index.tsx +7 -0
  63. package/dist/registry/package/components/Overlay/index.tsx +34 -24
  64. package/dist/registry/package/components/PageHeader/index.tsx +7 -0
  65. package/dist/registry/package/components/PermissionGate/index.tsx +6 -0
  66. package/dist/registry/package/components/Popover/index.tsx +7 -0
  67. package/dist/registry/package/components/QrCode/index.tsx +7 -0
  68. package/dist/registry/package/components/RadioGroup/index.tsx +7 -0
  69. package/dist/registry/package/components/Rating/index.tsx +6 -0
  70. package/dist/registry/package/components/ReferenceSelect/index.tsx +7 -0
  71. package/dist/registry/package/components/RelativeTime/index.tsx +6 -0
  72. package/dist/registry/package/components/ResizablePanel/index.tsx +7 -0
  73. package/dist/registry/package/components/Resource/index.tsx +514 -0
  74. package/dist/registry/package/components/RichTextEditor/index.tsx +7 -0
  75. package/dist/registry/package/components/SearchInput/index.tsx +8 -0
  76. package/dist/registry/package/components/Select/index.tsx +11 -0
  77. package/dist/registry/package/components/Sidebar/index.tsx +489 -113
  78. package/dist/registry/package/components/Sidebar/styles.css +96 -4
  79. package/dist/registry/package/components/Skeleton/index.tsx +7 -0
  80. package/dist/registry/package/components/Spinner/index.tsx +7 -0
  81. package/dist/registry/package/components/StatCard/index.tsx +7 -0
  82. package/dist/registry/package/components/Statistic/index.tsx +7 -0
  83. package/dist/registry/package/components/Switch/index.tsx +7 -0
  84. package/dist/registry/package/components/Tabs/index.tsx +7 -0
  85. package/dist/registry/package/components/TagsInput/index.tsx +7 -0
  86. package/dist/registry/package/components/Textarea/index.tsx +7 -0
  87. package/dist/registry/package/components/TimePicker/index.tsx +7 -0
  88. package/dist/registry/package/components/TimeRangePicker/index.tsx +6 -0
  89. package/dist/registry/package/components/Timeline/index.tsx +7 -0
  90. package/dist/registry/package/components/Timer/index.tsx +7 -0
  91. package/dist/registry/package/components/Tour/index.tsx +7 -0
  92. package/dist/registry/package/components/Transfer/index.tsx +7 -0
  93. package/dist/registry/package/components/Tree/index.tsx +7 -0
  94. package/dist/registry/package/components/TreeSelect/index.tsx +7 -0
  95. package/dist/registry/package/components/Typography/index.tsx +8 -0
  96. package/dist/registry/package/components/UnsavedChangesGuard/index.tsx +8 -0
  97. package/dist/registry/package/components/WeekPicker/index.tsx +7 -0
  98. package/dist/registry/package/components/YearPicker/index.tsx +7 -0
  99. package/dist/registry/registry.json +69 -14
  100. package/package.json +14 -14
@@ -1,9 +1,550 @@
1
- import { type ReactNode } from "react";
1
+ import { useMemo, useState, type Key, type ReactNode } from "react";
2
+ import { cn } from "@sia-ui/utils";
3
+ import type { FormValidator, PermissionRule } from "@sia-ui/headless";
2
4
  import { PageHeader } from "../PageHeader";
3
- import { AsyncState } from "../AsyncState";
5
+ import { Button } from "../Button";
4
6
  import { Pagination } from "../Pagination";
5
- import { cn } from "@sia-ui/utils";
7
+ import { DataTable } from "../DataTable";
8
+ import { EyeIcon, PlusIcon, TrashIcon } from "../Icons";
9
+ import type {
10
+ DataTableColumn,
11
+ DataTableProps,
12
+ RowAction,
13
+ } from "../DataTable/types";
14
+ import type { FormEntry, FormShape } from "../Form";
15
+ import {
16
+ resourceColumns,
17
+ resourceDefaults,
18
+ resourceDetailColumns,
19
+ resourceFormFields,
20
+ resourcePermissions,
21
+ resourceRowKey,
22
+ resourceSearchKeys,
23
+ resourceValidator,
24
+ type ResourceDefinition,
25
+ } from "../Resource";
26
+ import {
27
+ CrudDialogs,
28
+ type CrudDialogOptions,
29
+ type CrudDialogState,
30
+ } from "./dialogs";
6
31
  import "./styles.css";
7
32
 
8
- export interface CrudPageProps { title: ReactNode; description?: ReactNode; actions?: ReactNode; toolbar?: ReactNode; children: ReactNode; loading?: boolean; error?: ReactNode; empty?: boolean; emptyTitle?: ReactNode; page?: number; totalPages?: number; onPageChange?: (page: number) => void; className?: string; }
9
- export function CrudPage({ title, description, actions, toolbar, children, loading = false, error, empty = false, emptyTitle, page, totalPages, onPageChange, className }: CrudPageProps) { return <section className={cn("sia-crud-page", className)}><PageHeader title={title} description={description} actions={actions} />{toolbar}<AsyncState loading={loading} error={error} empty={empty} emptyTitle={emptyTitle}>{children}</AsyncState>{page !== undefined && totalPages !== undefined && onPageChange && <Pagination page={page} totalPages={totalPages} onPageChange={onPageChange} />}</section>; }
33
+ export {
34
+ CrudDialogs,
35
+ type CrudDialogOptions,
36
+ type CrudDialogState,
37
+ } from "./dialogs";
38
+
39
+ /**
40
+ * La personnalisation d'une action standard.
41
+ *
42
+ * Tout est facultatif : fournir le gestionnaire suffit à obtenir un bouton
43
+ * qui marche. Ce qui est ici ne sert qu'à s'écarter du défaut — un autre
44
+ * libellé, une règle de droits, une confirmation en plus ou en moins.
45
+ */
46
+ export interface CrudActionOptions<T> {
47
+ label?: ReactNode;
48
+ icon?: ReactNode;
49
+ permission?: PermissionRule;
50
+ hidden?: (row: T, index: number) => boolean;
51
+ disabled?: (row: T, index: number) => boolean;
52
+ /** `false` retire la confirmation que `delete` a par défaut. */
53
+ confirm?: RowAction<T>["confirm"] | false;
54
+ }
55
+
56
+ export interface CrudCreateOptions {
57
+ label?: ReactNode;
58
+ icon?: ReactNode;
59
+ permission?: PermissionRule;
60
+ disabled?: boolean;
61
+ }
62
+
63
+ interface CrudPageBase<T> {
64
+ /** À défaut, le pluriel de la ressource. */
65
+ title?: ReactNode;
66
+ description?: ReactNode;
67
+ eyebrow?: ReactNode;
68
+
69
+ data: T[];
70
+ getRowKey?: (row: T, index: number) => Key;
71
+
72
+ /* ───────────────────────────────── les quatre opérations, par défaut */
73
+
74
+ /**
75
+ * Ce que fait le bouton de création.
76
+ *
77
+ * Facultatif : avec `fields`, la page ouvre elle-même son formulaire. Le
78
+ * fournir sert à partir ailleurs — une page dédiée, un assistant en
79
+ * plusieurs étapes — et prend alors le pas sur la boîte.
80
+ */
81
+ onCreate?: () => void;
82
+ /** Idem : sans lui, la boîte de détail s'ouvre. */
83
+ onView?: (row: T, index: number) => void;
84
+ /** Idem : sans lui, le formulaire s'ouvre prérempli. */
85
+ onEdit?: (row: T, index: number) => void;
86
+ /** Confirmée d'office. Détruire sans demander est une faute. */
87
+ onDelete?: (row: T, index: number) => void | Promise<void>;
88
+
89
+ /* ──────────────────────────────────── le formulaire de la page */
90
+
91
+ /**
92
+ * Les champs de création et de modification.
93
+ *
94
+ * Les déclarer suffit à obtenir un CRUD complet : le bouton « Créer » ouvre
95
+ * un formulaire vide, « Modifier » le même prérempli, « Consulter » la
96
+ * ligne en lecture. C'est la même description que `Form` — aucun schéma
97
+ * propre à cette page, aucune bibliothèque imposée.
98
+ */
99
+ fields?: FormEntry[];
100
+
101
+ /** Ce que la page fait d'un formulaire envoyé. */
102
+ onSubmit?: (
103
+ values: FormShape,
104
+ context: { mode: "create" | "edit"; row?: T; index?: number },
105
+ ) => void | Promise<void>;
106
+
107
+ /**
108
+ * La validation, sous la même forme que `Form`.
109
+ *
110
+ * `createValidator` de `@sia-ui/utils/validation` en produit une; un Zod s'y
111
+ * branche en cinq lignes, sans que le composant connaisse Zod.
112
+ */
113
+ validate?: FormValidator<FormShape>;
114
+
115
+ /** Valeurs de départ d'une création. Par défaut, des champs vides. */
116
+ createDefaults?: FormShape;
117
+
118
+ /**
119
+ * Comment une ligne devient des valeurs de formulaire.
120
+ *
121
+ * Par défaut, les champs de même nom sont repris tels quels. À fournir dès
122
+ * que la ligne et le formulaire ne parlent pas la même langue — une date
123
+ * ISO à découper, un objet lié à réduire à son identifiant.
124
+ */
125
+ toFormValues?: (row: T) => FormShape;
126
+
127
+ /**
128
+ * La vue de détail.
129
+ *
130
+ * Par défaut dérivée des colonnes, qui disent déjà quoi montrer. Une
131
+ * fonction la remplace, `false` la retire avec son action.
132
+ */
133
+ detail?: boolean | ((row: T, index: number) => ReactNode);
134
+
135
+ /** Titres, largeur et nombre de colonnes des boîtes. */
136
+ dialog?: CrudDialogOptions;
137
+
138
+ /**
139
+ * De quoi s'écarter des défauts.
140
+ *
141
+ * `false` retire une action alors même que son gestionnaire existe — utile
142
+ * quand le gestionnaire sert ailleurs, par exemple à un raccourci clavier.
143
+ */
144
+ operations?: {
145
+ create?: CrudCreateOptions | false;
146
+ view?: CrudActionOptions<T> | false;
147
+ edit?: CrudActionOptions<T> | false;
148
+ delete?: CrudActionOptions<T> | false;
149
+ };
150
+
151
+ /** Des actions de ligne en plus des quatre standard. */
152
+ extraRowActions?: Array<RowAction<T>> | ((row: T, index: number) => Array<RowAction<T>>);
153
+
154
+ /** Suppression groupée. Ajoute la sélection et sa barre d'actions. */
155
+ onBulkDelete?: (keys: Key[], rows: T[]) => void | Promise<void>;
156
+
157
+ can?: (rule: PermissionRule) => boolean;
158
+
159
+ /* ────────────────────────────────────────────────── autour du tableau */
160
+
161
+ /** Actions de page, à droite du titre. S'ajoute au bouton de création. */
162
+ headerActions?: ReactNode;
163
+ toolbar?: ReactNode;
164
+ searchKeys?: string[];
165
+ searchPlaceholder?: string;
166
+ onSearch?: (query: string) => void;
167
+
168
+ loading?: boolean;
169
+ error?: ReactNode;
170
+ onRetry?: () => void;
171
+ empty?: DataTableProps<T>["empty"];
172
+
173
+ page?: number;
174
+ totalPages?: number;
175
+ onPageChange?: (page: number) => void;
176
+
177
+ /**
178
+ * Ce qui n'a pas de raccourci ici passe au tableau tel quel — densité,
179
+ * mode cartes, tri, colonnes figées.
180
+ */
181
+ table?: Partial<Omit<DataTableProps<T>, "columns" | "data">>;
182
+
183
+ /** Remplace le tableau. Le reste de la page continue de fonctionner. */
184
+ children?: ReactNode;
185
+
186
+ className?: string;
187
+ }
188
+
189
+ /**
190
+ * Les props de la page.
191
+ *
192
+ * Deux façons de la remplir, et une seule des deux à la fois : une ressource
193
+ * déclarée, d'où tout se déduit, ou des colonnes écrites à la main. L'union
194
+ * est là pour que TypeScript exige l'une ou l'autre — une page sans colonnes
195
+ * ni ressource ne montrerait rien, et le dirait à l'exécution.
196
+ */
197
+ export type CrudPageProps<T> = CrudPageBase<T> &
198
+ (
199
+ | {
200
+ /**
201
+ * La ressource, déclarée une fois avec `defineResource`.
202
+ *
203
+ * Colonnes, formulaire, détail, validation, recherche, clé de ligne
204
+ * et droits en sont dérivés. Chacun reste remplaçable — `columns`,
205
+ * `fields`, `validate`, `searchKeys` passent devant la dérivation.
206
+ */
207
+ resource: ResourceDefinition<T>;
208
+ columns?: Array<DataTableColumn<T>>;
209
+ }
210
+ | {
211
+ resource?: undefined;
212
+ columns: Array<DataTableColumn<T>>;
213
+ }
214
+ );
215
+
216
+ /** Une icône de crayon. Dessinée ici : le jeu commun n'en a pas. */
217
+ function EditIcon() {
218
+ return (
219
+ <svg viewBox="0 0 24 24" width="16" height="16" aria-hidden="true">
220
+ <path
221
+ d="M4 20h4l10-10a2.5 2.5 0 0 0-3.5-3.5L4.5 16.5 4 20Z"
222
+ fill="none"
223
+ stroke="currentColor"
224
+ strokeWidth="1.7"
225
+ strokeLinejoin="round"
226
+ />
227
+ </svg>
228
+ );
229
+ }
230
+
231
+ /**
232
+ * Une page de liste, avec ses quatre opérations.
233
+ *
234
+ * Créer, consulter, modifier, supprimer : ce sont les mêmes partout, et les
235
+ * réécrire à chaque écran produit quatre variantes qui divergent — celle qui
236
+ * oublie la confirmation, celle qui ne vérifie pas les droits, celle dont le
237
+ * bouton ne se désactive pas pendant l'appel.
238
+ *
239
+ * Ici, fournir le gestionnaire suffit. Ne pas le fournir retire l'action.
240
+ */
241
+ export function CrudPage<T>({
242
+ resource,
243
+ title,
244
+ description,
245
+ eyebrow,
246
+ columns: colonnesFournies,
247
+ data,
248
+ getRowKey: cleFournie,
249
+ onCreate,
250
+ onView,
251
+ onEdit,
252
+ onDelete,
253
+ fields: champsFournis,
254
+ onSubmit,
255
+ validate: validationFournie,
256
+ createDefaults,
257
+ toFormValues,
258
+ detail,
259
+ dialog,
260
+ operations,
261
+ extraRowActions,
262
+ onBulkDelete,
263
+ can,
264
+ headerActions,
265
+ toolbar,
266
+ searchKeys: clesDeRecherche,
267
+ searchPlaceholder,
268
+ onSearch,
269
+ loading = false,
270
+ error,
271
+ onRetry,
272
+ empty,
273
+ page,
274
+ totalPages,
275
+ onPageChange,
276
+ table,
277
+ children,
278
+ className,
279
+ }: CrudPageProps<T>) {
280
+ const [boite, setBoite] = useState<CrudDialogState<T> | null>(null);
281
+
282
+ /**
283
+ * Ce que la ressource donne, et ce qui passe devant.
284
+ *
285
+ * Chaque dérivation cède à la prop correspondante : déclarer une ressource
286
+ * ne doit jamais empêcher de reprendre la main sur un point précis. Les
287
+ * dérivations sont mémoïsées — elles parcourent la déclaration entière, et
288
+ * une page de liste se rend à chaque frappe dans sa barre de recherche.
289
+ */
290
+ const derive = useMemo(
291
+ () =>
292
+ resource
293
+ ? {
294
+ columns: resourceColumns(resource),
295
+ detailColumns: resourceDetailColumns(resource),
296
+ fields: resourceFormFields(resource),
297
+ validate: resourceValidator(resource),
298
+ searchKeys: resourceSearchKeys(resource),
299
+ defaults: resourceDefaults(resource),
300
+ getRowKey: resourceRowKey(resource),
301
+ permissions: resourcePermissions(resource),
302
+ formColumns: resource.formColumns,
303
+ label: resource.label,
304
+ }
305
+ : undefined,
306
+ [resource],
307
+ );
308
+
309
+ const columns = colonnesFournies ?? derive?.columns ?? [];
310
+ const fields = champsFournis ?? derive?.fields;
311
+ const validate = validationFournie ?? derive?.validate;
312
+ const searchKeys = clesDeRecherche ?? derive?.searchKeys;
313
+ const getRowKey = cleFournie ?? derive?.getRowKey;
314
+
315
+ /**
316
+ * Qui ouvre quoi.
317
+ *
318
+ * Le gestionnaire fourni l'emporte toujours : c'est la porte de sortie vers
319
+ * une page dédiée. À défaut, la page ouvre sa propre boîte — dès lors
320
+ * qu'elle a de quoi la remplir. Une action reste absente quand ni l'un ni
321
+ * l'autre n'est possible : un bouton qui n'ouvre rien est pire qu'un bouton
322
+ * manquant.
323
+ */
324
+ const formulaireDispo = Boolean(fields && onSubmit);
325
+ const detailDispo =
326
+ detail === undefined ? Boolean(fields) : detail !== false;
327
+
328
+ /*
329
+ Mémoïsés, et pas par principe : ces trois fonctions entrent dans les
330
+ dépendances des actions de ligne. Recréées à chaque rendu, elles
331
+ annuleraient cette mémoïsation — et une page de liste se rend à chaque
332
+ frappe dans sa barre de recherche.
333
+ */
334
+ const creer = useMemo(
335
+ () =>
336
+ onCreate ??
337
+ (formulaireDispo ? () => setBoite({ mode: "create" }) : undefined),
338
+ [onCreate, formulaireDispo],
339
+ );
340
+ const modifier = useMemo(
341
+ () =>
342
+ onEdit ??
343
+ (formulaireDispo
344
+ ? (row: T, index: number) => setBoite({ mode: "edit", row, index })
345
+ : undefined),
346
+ [onEdit, formulaireDispo],
347
+ );
348
+ const consulter = useMemo(
349
+ () =>
350
+ onView ??
351
+ (detailDispo
352
+ ? (row: T, index: number) => setBoite({ mode: "view", row, index })
353
+ : undefined),
354
+ [onView, detailDispo],
355
+ );
356
+
357
+ /**
358
+ * Les actions de ligne, dans l'ordre où on les lit : consulter, modifier,
359
+ * supprimer. La destruction en dernier, et jamais à côté d'une action
360
+ * bénigne qu'on viserait de travers.
361
+ */
362
+ const actionsDeLigne = useMemo(() => {
363
+ const liste: Array<RowAction<T>> = [];
364
+
365
+ const ajouter = (
366
+ key: "view" | "edit" | "delete",
367
+ handler: ((row: T, index: number) => void | Promise<void>) | undefined,
368
+ defauts: Omit<RowAction<T>, "key" | "onSelect">,
369
+ ) => {
370
+ const options = operations?.[key];
371
+ if (!handler || options === false) return;
372
+
373
+ // Le droit déclaré sur l'opération l'emporte sur celui de la ressource,
374
+ // qui n'est qu'une convention appliquée faute de mieux.
375
+ const droit = options?.permission ?? derive?.permissions[key];
376
+
377
+ const confirmation =
378
+ options?.confirm === false
379
+ ? undefined
380
+ : (options?.confirm ?? defauts.confirm);
381
+
382
+ liste.push({
383
+ key,
384
+ label: options?.label ?? defauts.label,
385
+ icon: options?.icon ?? defauts.icon,
386
+ ...(defauts.tone ? { tone: defauts.tone } : {}),
387
+ ...(droit !== undefined ? { permission: droit } : {}),
388
+ ...(options?.hidden ? { hidden: options.hidden } : {}),
389
+ ...(options?.disabled ? { disabled: options.disabled } : {}),
390
+ ...(confirmation ? { confirm: confirmation } : {}),
391
+ onSelect: handler,
392
+ });
393
+ };
394
+
395
+ ajouter("view", consulter, { label: "Consulter", icon: <EyeIcon /> });
396
+ ajouter("edit", modifier, { label: "Modifier", icon: <EditIcon /> });
397
+ ajouter("delete", onDelete, {
398
+ label: "Supprimer",
399
+ icon: <TrashIcon />,
400
+ tone: "danger",
401
+ // Détruire sans demander est une faute. On peut la retirer — encore
402
+ // faut-il l'avoir écrit.
403
+ confirm: {
404
+ title: "Supprimer cet élément ?",
405
+ description: "Cette action est définitive.",
406
+ confirmLabel: "Supprimer",
407
+ destructive: true,
408
+ },
409
+ });
410
+
411
+ return liste;
412
+ }, [operations, onDelete, modifier, consulter, derive]);
413
+
414
+ const toutesLesActions = useMemo(() => {
415
+ if (!extraRowActions) {
416
+ return actionsDeLigne.length > 0 ? actionsDeLigne : undefined;
417
+ }
418
+
419
+ return (row: T, index: number) => [
420
+ ...actionsDeLigne,
421
+ ...(typeof extraRowActions === "function"
422
+ ? extraRowActions(row, index)
423
+ : extraRowActions),
424
+ ];
425
+ }, [actionsDeLigne, extraRowActions]);
426
+
427
+ /**
428
+ * Le bouton de création.
429
+ *
430
+ * Trois conditions, dans cet ordre : un gestionnaire, une action non
431
+ * retirée, et le droit de s'en servir. Un bouton qui ouvre un formulaire
432
+ * que le serveur refusera est pire qu'un bouton absent.
433
+ */
434
+ const creation = operations?.create === false ? null : (operations?.create ?? {});
435
+ const droitDeCreer = creation?.permission ?? derive?.permissions.create;
436
+ const creationAutorisee =
437
+ droitDeCreer === undefined || !can || can(droitDeCreer);
438
+
439
+ const boutonCreer =
440
+ creer && creation && creationAutorisee ? (
441
+ <Button onClick={creer} disabled={creation.disabled ?? false}>
442
+ {creation.icon ?? <PlusIcon />}
443
+ {creation.label ?? derive?.label?.create ?? "Créer"}
444
+ </Button>
445
+ ) : null;
446
+ const enTeteActions =
447
+ boutonCreer || headerActions ? (
448
+ <>
449
+ {headerActions}
450
+ {boutonCreer}
451
+ </>
452
+ ) : undefined;
453
+
454
+ return (
455
+ <section className={cn("sia-crud-page", className)}>
456
+ <PageHeader
457
+ title={title ?? derive?.label?.plural ?? resource?.name ?? ""}
458
+ {...(description ?? resource?.description
459
+ ? { description: description ?? resource?.description }
460
+ : {})}
461
+ {...(eyebrow ? { eyebrow } : {})}
462
+ {...(enTeteActions ? { actions: enTeteActions } : {})}
463
+ />
464
+
465
+ {children ?? (
466
+ <DataTable<T>
467
+ columns={columns}
468
+ data={data}
469
+ {...(getRowKey ? { getRowKey } : {})}
470
+ {...(toolbar ? { toolbar } : {})}
471
+ {...(searchKeys ? { searchKeys } : {})}
472
+ {...(searchPlaceholder ? { searchPlaceholder } : {})}
473
+ {...(onSearch ? { onSearch } : {})}
474
+ {...(toutesLesActions ? { rowActions: toutesLesActions } : {})}
475
+ {...(can ? { can } : {})}
476
+ loading={loading}
477
+ {...(error ? { error } : {})}
478
+ {...(onRetry ? { onRetry } : {})}
479
+ {...(empty ? { empty } : {})}
480
+ {...(onBulkDelete
481
+ ? {
482
+ selectable: true,
483
+ selectionActions: ({ keys, rows, clear }) => (
484
+ <Button
485
+ size="sm"
486
+ variant="outline"
487
+ tone="danger"
488
+ onClick={async () => {
489
+ await onBulkDelete(keys, rows);
490
+ clear();
491
+ }}
492
+ >
493
+ <TrashIcon />
494
+ Supprimer
495
+ </Button>
496
+ ),
497
+ }
498
+ : {})}
499
+ /*
500
+ Trois actions en ligne plutôt que deux : consulter, modifier et
501
+ supprimer sont précisément ce que cette page existe pour offrir.
502
+ La limite du tableau, elle, reste à deux — un tableau quelconque
503
+ n'a pas de raison d'étaler ses boutons.
504
+ */
505
+ inlineActionsLimit={3}
506
+ {...table}
507
+ />
508
+ )}
509
+
510
+ <CrudDialogs<T>
511
+ state={boite}
512
+ onClose={() => setBoite(null)}
513
+ columns={derive?.detailColumns ?? columns}
514
+ fields={fields}
515
+ validate={validate}
516
+ onSubmit={onSubmit}
517
+ createDefaults={createDefaults ?? derive?.defaults}
518
+ toFormValues={toFormValues}
519
+ renderDetail={typeof detail === "function" ? detail : undefined}
520
+ getRowKey={getRowKey}
521
+ /*
522
+ Les titres des boîtes reprennent le nom de la ressource, faute de
523
+ mieux : « Modifier la facture » se lit mieux que « Modifier », et
524
+ l'écrire trois fois par page de liste est précisément ce que cette
525
+ déclaration existe pour éviter.
526
+ */
527
+ dialog={{
528
+ ...(derive?.label?.singular
529
+ ? {
530
+ createTitle: derive.label.create ?? "Créer",
531
+ editTitle: <>Modifier — {derive.label.singular}</>,
532
+ viewTitle: derive.label.singular,
533
+ }
534
+ : {}),
535
+ ...(derive?.formColumns ? { columns: derive.formColumns } : {}),
536
+ ...dialog,
537
+ }}
538
+ />
539
+
540
+ {page !== undefined && totalPages !== undefined && onPageChange && (
541
+ <Pagination
542
+ page={page}
543
+ totalPages={totalPages}
544
+ onPageChange={onPageChange}
545
+ showTotal
546
+ />
547
+ )}
548
+ </section>
549
+ );
550
+ }
@@ -1 +1,25 @@
1
- .sia-crud-page { display:grid; gap:1.25rem; }
1
+ /**
2
+ * La page de liste.
3
+ *
4
+ * Presque rien : le titre, le tableau et la pagination portent leur propre
5
+ * style. Ce qui reste est l'espacement entre eux, et c'est précisément ce
6
+ * qu'une page ne doit pas redéfinir écran par écran.
7
+ */
8
+
9
+ .sia-crud-page {
10
+ display: flex;
11
+ flex-direction: column;
12
+ gap: 1.25rem;
13
+ min-width: 0;
14
+ }
15
+
16
+ /* La pagination se détache du tableau sans ligne supplémentaire : le tableau
17
+ en a déjà une sous sa dernière ligne. */
18
+ .sia-crud-page > .sia-pagination {
19
+ align-self: center;
20
+ margin-top: .25rem;
21
+ }
22
+
23
+ @media (max-width: 47.9375rem) {
24
+ .sia-crud-page { gap: 1rem; }
25
+ }
@@ -4,6 +4,13 @@ import "./styles.css";
4
4
 
5
5
  export interface CurrencyInputProps extends Omit<InputHTMLAttributes<HTMLInputElement>, "value" | "defaultValue" | "onChange" | "inputMode"> { value?: number | null; defaultValue?: number | null; currency?: string; locale?: string; onValueChange?: (value: number | null) => void; invalid?: boolean; }
6
6
 
7
+ /**
8
+ * La saisie d'un montant.
9
+ *
10
+ * La valeur rendue est un nombre, jamais la chaîne affichée : les espaces
11
+ * de milliers et le symbole appartiennent à l'affichage, et un serveur qui
12
+ * reçoit « 1 250 000 F CFA » ne peut rien en faire.
13
+ */
7
14
  export const CurrencyInput = forwardRef<HTMLInputElement, CurrencyInputProps>(({ value, defaultValue = null, currency = "XAF", locale = "fr-FR", onValueChange, invalid, className, onBlur, onFocus, ...props }, ref) => {
8
15
  const [focused, setFocused] = useState(false);
9
16
  const [internal, setInternal] = useState<number | null>(defaultValue);