@sia-ui/cli 0.3.2 → 0.5.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 (27) hide show
  1. package/CHANGELOG.md +41 -0
  2. package/README.md +74 -19
  3. package/dist/registry/package/components/AppShell/index.tsx +101 -7
  4. package/dist/registry/package/components/AppShell/styles.css +87 -0
  5. package/dist/registry/package/components/Calendar/index.tsx +4 -4
  6. package/dist/registry/package/components/CrudPage/dialogs.tsx +20 -20
  7. package/dist/registry/package/components/CrudPage/index.tsx +34 -4
  8. package/dist/registry/package/components/DataTable/index.tsx +5 -1
  9. package/dist/registry/package/components/DataTable/types.ts +8 -0
  10. package/dist/registry/package/components/Drawer/index.tsx +17 -7
  11. package/dist/registry/package/components/Drawer/styles.css +53 -28
  12. package/dist/registry/package/components/Form/index.tsx +69 -3
  13. package/dist/registry/package/components/Icons/index.tsx +9 -0
  14. package/dist/registry/package/components/Pagination/index.tsx +168 -61
  15. package/dist/registry/package/components/Pagination/styles.css +133 -38
  16. package/dist/registry/package/components/PhoneInput/index.tsx +46 -29
  17. package/dist/registry/package/components/PhoneInput/styles.css +7 -21
  18. package/dist/registry/package/components/Select/index.tsx +34 -8
  19. package/dist/registry/package/components/Select/styles.css +18 -0
  20. package/dist/registry/package/components/Sidebar/index.tsx +23 -3
  21. package/dist/registry/package/components/Sidebar/styles.css +1 -13
  22. package/dist/registry/package/components/Timer/index.tsx +1 -1
  23. package/dist/registry/package/components/Toggle/styles.css +3 -2
  24. package/dist/registry/package/lib/form-adapter-rhf.tsx +23 -10
  25. package/dist/registry/registry.json +19 -12
  26. package/dist/registry/styles/sia-ui.css +17 -0
  27. package/package.json +5 -5
package/CHANGELOG.md CHANGED
@@ -1,5 +1,46 @@
1
1
  # @sia-ui/cli
2
2
 
3
+ ## 0.5.0
4
+
5
+ ### Patch Changes
6
+
7
+ - Erreurs serveur dans les formulaires, client HTTP plus sûr, registre vérifié.
8
+
9
+ ## Défauts
10
+ - **Registre `drawer`** — `context.ts` et la dépendance `icons` manquaient :
11
+ `sia-ui add app-shell` produisait un projet qui ne compilait pas. Un test
12
+ vérifie désormais que chaque import relatif de chaque entrée est apporté par
13
+ son installation.
14
+ - **`sia-ui.css`** pose `box-sizing: border-box` (spécificité nulle). Sans
15
+ reset côté projet, `AppShell` débordait de 32 px.
16
+ - **`@sia-ui/api`** ne rejoue plus d'office que `GET` et `HEAD`
17
+ (`retryMethods`). Une écriture n'est rejouée que si l'appel pose `retry`.
18
+ - Plus aucun avertissement `react-hooks/exhaustive-deps` dans les composants.
19
+
20
+ ## Formulaires et erreurs serveur
21
+ - `Form` intercepte un envoi rejeté : erreurs de champ sous les champs, le
22
+ reste dans une alerte. `onSubmit` peut aussi rendre `{ champ: message }`
23
+ (`FormSubmitResult`), dans `useLocalForm`, `Form`, `CrudPage` et
24
+ l'adaptateur react-hook-form. `CrudPage` garde alors sa boîte ouverte.
25
+ - Les erreurs posées par `setErrors` survivent à la validation locale jusqu'à
26
+ la modification de leur champ.
27
+ - `HttpError.toFormErrors()`, option `fieldErrors` du client et préréglage
28
+ `nestFieldErrors` (class-validator). `readSubmitError` et `hasFormErrors`
29
+ dans `@sia-ui/headless`.
30
+ - La pagination à plat `{ data, total, page, limit }` est reconnue, et `get`
31
+ ne déballe plus `data` quand l'objet porte une pagination.
32
+
33
+ ## Tableaux
34
+ - `searchDelay` sur `DataTable` et `CrudPage`.
35
+ - `CrudPage` place les `extraRowActions` avant « Supprimer ».
36
+
37
+ ## 0.4.0
38
+
39
+ ### Minor Changes
40
+
41
+ - Aligne tous les packages publics SIA UI sur la version 0.4.0 afin de fournir
42
+ un ensemble cohérent à installer, documenter et maintenir.
43
+
3
44
  ## 0.3.2
4
45
 
5
46
  ### Patch Changes
package/README.md CHANGED
@@ -1,5 +1,5 @@
1
- <p align="center"><img src="https://beatjo.github.io/sia-ui-site/brand/sia-ui-logo.png" alt="SIA UI" width="320"></p>
2
-
1
+ <p align="center"><img src="https://beatjo.github.io/sia-ui-site/brand/sia-ui-logo.png" alt="SIA UI" width="320"></p>
2
+
3
3
  # @sia-ui/cli
4
4
 
5
5
  Installer les composants SIA UI dans une application, comme shadcn/ui.
@@ -51,22 +51,76 @@ add src/components/DataTable/styles.css
51
51
 
52
52
  Une chose s'écrit au niveau le plus bas où elle a du sens, et une seule fois.
53
53
  Voir [la documentation](https://beatjo.github.io/sia-ui-site).
54
-
55
- ## Contribuer
56
-
57
- Le code source principal de SIA UI est privé pour le moment. Les retours publics passent par le dépôt `BeatJo/sia-ui-site` : bugs, demandes de composants, corrections de documentation et questions d'usage.
58
-
59
- - Signaler un bug : https://github.com/BeatJo/sia-ui-site/issues
60
- - Lire la documentation : https://beatjo.github.io/sia-ui-site/
61
- - Voir la démonstration : https://beatjo.github.io/sia-ui-site/preview/
62
-
63
- Les contributions directes au code des packages se font sur invitation dans le dépôt source privé.
64
- ## Journal des changements
65
-
66
- Le contenu ci-dessous reprend intégralement `CHANGELOG.md` pour rester visible sur npm.
67
-
54
+
55
+ ## Contribuer
56
+
57
+ Le code source principal de SIA UI est privé pour le moment. Les retours publics passent par le dépôt `BeatJo/sia-ui-site` : bugs, demandes de composants, corrections de documentation et questions d'usage.
58
+
59
+ - Signaler un bug : https://github.com/BeatJo/sia-ui-site/issues
60
+ - Lire la documentation : https://beatjo.github.io/sia-ui-site/
61
+ - Voir la démonstration : https://beatjo.github.io/sia-ui-site/preview/
62
+
63
+ Les contributions directes au code des packages se font sur invitation dans le dépôt source privé.
64
+ ## Journal des changements
65
+
66
+ Le contenu ci-dessous reprend intégralement `CHANGELOG.md` pour rester visible sur npm.
67
+
68
+ <!-- sia:changelog:start -->
68
69
  # @sia-ui/cli
69
70
 
71
+ ## 0.5.0
72
+
73
+ ### Patch Changes
74
+
75
+ - Erreurs serveur dans les formulaires, client HTTP plus sûr, registre vérifié.
76
+
77
+ ## Défauts
78
+ - **Registre `drawer`** — `context.ts` et la dépendance `icons` manquaient :
79
+ `sia-ui add app-shell` produisait un projet qui ne compilait pas. Un test
80
+ vérifie désormais que chaque import relatif de chaque entrée est apporté par
81
+ son installation.
82
+ - **`sia-ui.css`** pose `box-sizing: border-box` (spécificité nulle). Sans
83
+ reset côté projet, `AppShell` débordait de 32 px.
84
+ - **`@sia-ui/api`** ne rejoue plus d'office que `GET` et `HEAD`
85
+ (`retryMethods`). Une écriture n'est rejouée que si l'appel pose `retry`.
86
+ - Plus aucun avertissement `react-hooks/exhaustive-deps` dans les composants.
87
+
88
+ ## Formulaires et erreurs serveur
89
+ - `Form` intercepte un envoi rejeté : erreurs de champ sous les champs, le
90
+ reste dans une alerte. `onSubmit` peut aussi rendre `{ champ: message }`
91
+ (`FormSubmitResult`), dans `useLocalForm`, `Form`, `CrudPage` et
92
+ l'adaptateur react-hook-form. `CrudPage` garde alors sa boîte ouverte.
93
+ - Les erreurs posées par `setErrors` survivent à la validation locale jusqu'à
94
+ la modification de leur champ.
95
+ - `HttpError.toFormErrors()`, option `fieldErrors` du client et préréglage
96
+ `nestFieldErrors` (class-validator). `readSubmitError` et `hasFormErrors`
97
+ dans `@sia-ui/headless`.
98
+ - La pagination à plat `{ data, total, page, limit }` est reconnue, et `get`
99
+ ne déballe plus `data` quand l'objet porte une pagination.
100
+
101
+ ## Tableaux
102
+ - `searchDelay` sur `DataTable` et `CrudPage`.
103
+ - `CrudPage` place les `extraRowActions` avant « Supprimer ».
104
+
105
+ ## 0.4.0
106
+
107
+ ### Minor Changes
108
+
109
+ - Aligne tous les packages publics SIA UI sur la version 0.4.0 afin de fournir
110
+ un ensemble cohérent à installer, documenter et maintenir.
111
+
112
+ ## 0.3.2
113
+
114
+ ### Patch Changes
115
+
116
+ - Ajoute l’identité visuelle officielle de SIA UI et améliore la documentation publique, les changelogs et les ressources de contribution.
117
+
118
+ ## 0.3.1
119
+
120
+ ### Patch Changes
121
+
122
+ - Expose public contribution flow on npm
123
+
70
124
  ## 0.3.0
71
125
 
72
126
  ### Minor Changes
@@ -226,8 +280,9 @@ Le contenu ci-dessous reprend intégralement `CHANGELOG.md` pour rester visible
226
280
 
227
281
  ### Patch Changes
228
282
 
229
- - Include component styles when installing registry components
230
-
231
- ## Licence
283
+ - Include component styles when installing registry components
284
+ <!-- sia:changelog:end -->
285
+
286
+ ## Licence
232
287
 
233
288
  MIT
@@ -3,6 +3,8 @@ import {
3
3
  useMemo,
4
4
  useState,
5
5
  type CSSProperties,
6
+ type ElementType,
7
+ type HTMLAttributes,
6
8
  type ReactNode,
7
9
  } from "react";
8
10
  import { cn } from "@sia-ui/utils";
@@ -53,6 +55,87 @@ export type AppShellMobileNav = "drawer" | "tabs" | "both";
53
55
 
54
56
  export type AppShellContentWidth = "wide" | "narrow" | "full";
55
57
 
58
+ export interface AppShellBrandProps extends HTMLAttributes<HTMLDivElement> {
59
+ /** Symbole conservé lorsque la navigation devient un rail. */
60
+ icon: ReactNode;
61
+ /** Nom du produit, masqué automatiquement dans le rail. */
62
+ name: ReactNode;
63
+ /** Ligne secondaire facultative, masquée avec le nom. */
64
+ description?: ReactNode;
65
+ }
66
+
67
+ /**
68
+ * Identité de produit adaptée au rail de l'`AppShell`.
69
+ *
70
+ * L'application fournit le symbole et le texte une seule fois. Le composant
71
+ * masque sa copie dès que son parent reçoit `data-collapsed`.
72
+ */
73
+ export function AppShellBrand({
74
+ icon,
75
+ name,
76
+ description,
77
+ className,
78
+ ...props
79
+ }: AppShellBrandProps) {
80
+ return (
81
+ <div className={cn("sia-shell-brand", className)} {...props}>
82
+ <span className="sia-shell-brand__icon" aria-hidden="true">
83
+ {icon}
84
+ </span>
85
+ <span className="sia-shell-brand__copy">
86
+ <span className="sia-shell-brand__name">{name}</span>
87
+ {description !== undefined && (
88
+ <span className="sia-shell-brand__description">{description}</span>
89
+ )}
90
+ </span>
91
+ </div>
92
+ );
93
+ }
94
+
95
+ export interface AppShellRailItemProps extends HTMLAttributes<HTMLElement> {
96
+ /** Élément rendu : contenu simple, action ou lien. */
97
+ as?: "div" | "button" | "a";
98
+ /** Symbole toujours visible dans le rail. */
99
+ icon: ReactNode;
100
+ /** Libellé masqué visuellement dans le rail et conservé comme nom accessible. */
101
+ label: string;
102
+ href?: string;
103
+ type?: "button" | "submit" | "reset";
104
+ disabled?: boolean;
105
+ }
106
+
107
+ /** Contenu icône + libellé qui s'adapte automatiquement au rail. */
108
+ export function AppShellRailItem({
109
+ as = "div",
110
+ icon,
111
+ label,
112
+ className,
113
+ href,
114
+ type = "button",
115
+ disabled,
116
+ ...props
117
+ }: AppShellRailItemProps) {
118
+ const Component: ElementType = as;
119
+ const elementProps = {
120
+ ...props,
121
+ ...(as === "a" && href ? { href } : {}),
122
+ ...(as === "button" ? { type, disabled } : {}),
123
+ };
124
+
125
+ return (
126
+ <Component
127
+ className={cn("sia-shell-rail-item", className)}
128
+ aria-label={props["aria-label"] ?? (as !== "div" ? label : undefined)}
129
+ {...elementProps}
130
+ >
131
+ <span className="sia-shell-rail-item__icon" aria-hidden="true">
132
+ {icon}
133
+ </span>
134
+ <span className="sia-shell-rail-item__label">{label}</span>
135
+ </Component>
136
+ );
137
+ }
138
+
56
139
  export interface AppShellProps {
57
140
  nav: SidebarEntry[];
58
141
  children: ReactNode;
@@ -97,8 +180,8 @@ export interface AppShellProps {
97
180
  /**
98
181
  * En tête de barre latérale : logo, nom du produit, sélecteur d'entité.
99
182
  *
100
- * Reçoit `data-collapsed` en mode rail, comme `sidebarFooter` : sans quoi
101
- * un nom de produit écrit en toutes lettres déborde des quatre rem du rail.
183
+ * `AppShellBrand` masque son texte automatiquement dans le rail. Un contenu
184
+ * entièrement personnalisé reçoit toujours `data-collapsed` sur son parent.
102
185
  */
103
186
  brand?: ReactNode;
104
187
 
@@ -107,8 +190,8 @@ export interface AppShellProps {
107
190
  *
108
191
  * C'est la place de la déconnexion et des réglages du compte — ce qui sort
109
192
  * de l'application, par opposition à l'en-tête, réservé à ce qui agit sur
110
- * l'écran courant. Reçoit `data-collapsed` en mode rail, de quoi masquer un
111
- * libellé sans changer de composant.
193
+ * l'écran courant. `AppShellRailItem` masque son libellé automatiquement ;
194
+ * un contenu personnalisé peut encore lire `data-collapsed` sur son parent.
112
195
  */
113
196
  sidebarFooter?: ReactNode;
114
197
 
@@ -306,7 +389,9 @@ export function AppShell({
306
389
  {/* Sur petit écran, la barre latérale n'a pas la place de rester
307
390
  ouverte : elle devient un tiroir. Sans lui, l'arborescence complète
308
391
  ne serait atteignable que par « Plus », ce qui ne se devine pas. */}
309
- {!compact && !masquee && <aside className="sia-shell__aside">{barre}</aside>}
392
+ {!compact && !masquee && (
393
+ <aside className="sia-shell__aside">{barre}</aside>
394
+ )}
310
395
 
311
396
  <div className="sia-shell__main">
312
397
  {(header || collapsible !== false || (compact && montrerTiroir)) && (
@@ -348,7 +433,12 @@ export function AppShell({
348
433
  </header>
349
434
  )}
350
435
 
351
- <main className={cn("sia-shell__content", `sia-shell__content--${contentWidth}`)}>
436
+ <main
437
+ className={cn(
438
+ "sia-shell__content",
439
+ `sia-shell__content--${contentWidth}`,
440
+ )}
441
+ >
352
442
  {page && <div className="sia-shell__page">{page}</div>}
353
443
  {children}
354
444
  </main>
@@ -374,7 +464,11 @@ export function AppShell({
374
464
  size="sm"
375
465
  className="sia-shell__drawer"
376
466
  >
377
- {barre}
467
+ <Drawer.Header>
468
+ <Drawer.Title>Navigation</Drawer.Title>
469
+ <Drawer.Close />
470
+ </Drawer.Header>
471
+ <Drawer.Body>{barre}</Drawer.Body>
378
472
  </Drawer>
379
473
  )}
380
474
  </div>
@@ -57,6 +57,92 @@
57
57
  padding: .5rem;
58
58
  }
59
59
 
60
+ /* Les contenus standards de la barre connaissent le rail. L'application
61
+ fournit une icône et un texte une seule fois ; AppShell porte l'état sur le
62
+ parent, et ces primitives prennent en charge la présentation. */
63
+ .sia-shell-brand,
64
+ .sia-shell-rail-item {
65
+ display: flex;
66
+ align-items: center;
67
+ gap: .625rem;
68
+ min-width: 0;
69
+ }
70
+
71
+ .sia-shell-brand { padding: .25rem; }
72
+
73
+ .sia-shell-brand__icon {
74
+ display: grid;
75
+ place-items: center;
76
+ flex: none;
77
+ width: 2.75rem;
78
+ height: 2.75rem;
79
+ }
80
+
81
+ .sia-shell-brand__icon > img,
82
+ .sia-shell-brand__icon > svg {
83
+ max-width: 100%;
84
+ max-height: 100%;
85
+ object-fit: contain;
86
+ }
87
+
88
+ .sia-shell-brand__copy { min-width: 0; }
89
+ .sia-shell-brand__name,
90
+ .sia-shell-brand__description {
91
+ display: block;
92
+ overflow: hidden;
93
+ text-overflow: ellipsis;
94
+ white-space: nowrap;
95
+ }
96
+ .sia-shell-brand__name { font-weight: 700; }
97
+ .sia-shell-brand__description {
98
+ margin-top: .1rem;
99
+ color: var(--sia-muted);
100
+ font-size: .72rem;
101
+ }
102
+
103
+ .sia-shell-rail-item {
104
+ width: 100%;
105
+ padding: .375rem;
106
+ border: 0;
107
+ border-radius: var(--sia-radius);
108
+ background: transparent;
109
+ color: var(--sia-muted);
110
+ font: inherit;
111
+ font-size: .75rem;
112
+ text-decoration: none;
113
+ }
114
+ .sia-shell-rail-item:is(button, a) { cursor: pointer; }
115
+ .sia-shell-rail-item:is(button, a):hover {
116
+ background: var(--sia-surface-raised, var(--sia-surface));
117
+ color: var(--sia-foreground);
118
+ }
119
+ .sia-shell-rail-item:disabled { opacity: .55; cursor: not-allowed; }
120
+ .sia-shell-rail-item:is(button, a):focus-visible {
121
+ outline: 2px solid var(--sia-primary);
122
+ outline-offset: 2px;
123
+ }
124
+ .sia-shell-rail-item__icon {
125
+ display: grid;
126
+ place-items: center;
127
+ flex: none;
128
+ width: 1.5rem;
129
+ height: 1.5rem;
130
+ }
131
+ .sia-shell-rail-item__label {
132
+ min-width: 0;
133
+ overflow: hidden;
134
+ text-overflow: ellipsis;
135
+ white-space: nowrap;
136
+ }
137
+
138
+ [data-collapsed] .sia-shell-brand,
139
+ [data-collapsed] .sia-shell-rail-item {
140
+ justify-content: center;
141
+ gap: 0;
142
+ }
143
+ [data-collapsed] .sia-shell-brand__copy,
144
+ [data-collapsed] .sia-shell-rail-item__label { display: none; }
145
+
60
146
  .sia-shell__main {
61
147
  display: flex;
62
148
  flex-direction: column;
@@ -154,6 +240,7 @@
154
240
  padding-bottom: calc(var(--sia-shell-tabs) + env(safe-area-inset-bottom, 0px));
155
241
  }
156
242
 
243
+ .sia-shell__drawer .sia-drawer-body { flex:1; min-height:0; padding:.5rem; overflow:hidden; }
157
244
  .sia-shell__drawer .sia-sidebar { height: 100%; }
158
245
 
159
246
  /* ── La barre d'onglets ─────────────────────────────────────────────────── */
@@ -1,4 +1,4 @@
1
- import { useMemo, useState, type ReactNode } from "react";
1
+ import { useCallback, useMemo, useState, type ReactNode } from "react";
2
2
  import { cn, toneClass } from "@sia-ui/utils";
3
3
  import type { ComponentTone } from "@sia-ui/tokens";
4
4
  import { ChevronLeftIcon, ChevronRightIcon } from "../Icons";
@@ -119,10 +119,10 @@ export function Calendar({
119
119
  if (mode === undefined) setPanelMode(next);
120
120
  onPanelChange?.(monthIso(controlledMonth), next);
121
121
  };
122
- const isDisabled = (date: string) => Boolean(
122
+ const isDisabled = useCallback((date: string) => Boolean(
123
123
  disabledDate?.(date) ||
124
124
  (validRange && (date < validRange[0] || date > validRange[1])),
125
- );
125
+ ), [disabledDate, validRange]);
126
126
  const selectDate = (date: string) => {
127
127
  if (isDisabled(date)) return;
128
128
  if (selectionMode === "range") {
@@ -160,7 +160,7 @@ export function Calendar({
160
160
  ...(rangeEdge ? { rangeEdge } : {}),
161
161
  };
162
162
  });
163
- }, [year, monthIndex, firstDayOfWeek, disabledDate, validRange, currentValue, currentRange.start, currentRange.end]);
163
+ }, [year, monthIndex, firstDayOfWeek, isDisabled, currentValue, currentRange.start, currentRange.end]);
164
164
  const weekdays = Array.from({ length: 7 }, (_, index) => new Intl.DateTimeFormat(locale, { weekday: "short" }).format(new Date(2024, 0, 7 + firstDayOfWeek + index)));
165
165
  const monthName = new Intl.DateTimeFormat(locale, { month: "long" }).format(controlledMonth);
166
166
  const updateFromString = (next: string) => setMonth(parseMonth(next));
@@ -1,7 +1,16 @@
1
1
  import { type Key, type ReactNode } from "react";
2
- import type { FormValidator } from "@sia-ui/headless";
2
+ import {
3
+ hasFormErrors,
4
+ type FormSubmitResult,
5
+ type FormValidator,
6
+ } from "@sia-ui/headless";
3
7
  import { Modal } from "../Modal";
4
- import { Form, type FormEntry, type FormShape } from "../Form";
8
+ import {
9
+ Form,
10
+ formFieldNames,
11
+ type FormEntry,
12
+ type FormShape,
13
+ } from "../Form";
5
14
  import { Descriptions } from "../Descriptions";
6
15
  import type { DataTableColumn } from "../DataTable/types";
7
16
  import { renduCellule } from "../DataTable/values";
@@ -29,24 +38,12 @@ export interface CrudDialogOptions {
29
38
  }
30
39
 
31
40
  /**
32
- * Les noms de champs, groupes aplatis.
33
- *
34
- * Servent à fabriquer un formulaire vide : sans valeur initiale déclarée, un
35
- * champ non contrôlé le devient à la première frappe, et React s'en plaint à
36
- * juste titre.
41
+ * Un formulaire vide : sans valeur initiale déclarée, un champ non contrôlé
42
+ * le devient à la première frappe, et React s'en plaint à juste titre.
37
43
  */
38
- function nomsDeChamps(fields: FormEntry[]): string[] {
39
- const noms: string[] = [];
40
- for (const entree of fields) {
41
- if ("group" in entree) noms.push(...entree.fields.map((f) => f.name));
42
- else noms.push(entree.name);
43
- }
44
- return noms;
45
- }
46
-
47
44
  export function valeursVides(fields: FormEntry[]): FormShape {
48
45
  const valeurs: FormShape = {};
49
- for (const nom of nomsDeChamps(fields)) valeurs[nom] = "";
46
+ for (const nom of formFieldNames(fields)) valeurs[nom] = "";
50
47
  return valeurs;
51
48
  }
52
49
 
@@ -60,7 +57,7 @@ export function valeursVides(fields: FormEntry[]): FormShape {
60
57
  export function valeursDeLigne<T>(row: T, fields: FormEntry[]): FormShape {
61
58
  const source = row as Record<string, unknown>;
62
59
  const valeurs: FormShape = {};
63
- for (const nom of nomsDeChamps(fields)) {
60
+ for (const nom of formFieldNames(fields)) {
64
61
  const valeur = source[nom];
65
62
  valeurs[nom] =
66
63
  typeof valeur === "string" ||
@@ -84,7 +81,7 @@ export interface CrudDialogsProps<T> {
84
81
  | ((
85
82
  values: FormShape,
86
83
  context: { mode: "create" | "edit"; row?: T; index?: number },
87
- ) => void | Promise<void>)
84
+ ) => FormSubmitResult<FormShape> | Promise<FormSubmitResult<FormShape>>)
88
85
  | undefined;
89
86
 
90
87
  createDefaults?: FormShape | undefined;
@@ -177,11 +174,14 @@ export function CrudDialogs<T>({
177
174
  // conviennent.
178
175
  requireDirty={modification}
179
176
  onSubmit={async (values) => {
180
- await onSubmit?.(values, {
177
+ const resultat = await onSubmit?.(values, {
181
178
  mode: creation ? "create" : "edit",
182
179
  ...(state?.row !== undefined ? { row: state.row } : {}),
183
180
  ...(state?.index !== undefined ? { index: state.index } : {}),
184
181
  });
182
+ // Refusé par le serveur : la boîte reste ouverte, les erreurs
183
+ // sous les champs. La fermer perdrait la saisie.
184
+ if (hasFormErrors(resultat)) return resultat;
185
185
  onClose();
186
186
  }}
187
187
  />
@@ -1,6 +1,10 @@
1
1
  import { useMemo, useState, type Key, type ReactNode } from "react";
2
2
  import { cn } from "@sia-ui/utils";
3
- import type { FormValidator, PermissionRule } from "@sia-ui/headless";
3
+ import type {
4
+ FormSubmitResult,
5
+ FormValidator,
6
+ PermissionRule,
7
+ } from "@sia-ui/headless";
4
8
  import { PageHeader } from "../PageHeader";
5
9
  import { Button } from "../Button";
6
10
  import { Pagination } from "../Pagination";
@@ -98,11 +102,16 @@ interface CrudPageBase<T> {
98
102
  */
99
103
  fields?: FormEntry[];
100
104
 
101
- /** Ce que la page fait d'un formulaire envoyé. */
105
+ /**
106
+ * Ce que la page fait d'un formulaire envoyé.
107
+ *
108
+ * Rendre des erreurs par champ, ou lever une `HttpError`, garde la boîte
109
+ * ouverte avec les erreurs affichées ; sinon elle se ferme.
110
+ */
102
111
  onSubmit?: (
103
112
  values: FormShape,
104
113
  context: { mode: "create" | "edit"; row?: T; index?: number },
105
- ) => void | Promise<void>;
114
+ ) => FormSubmitResult<FormShape> | Promise<FormSubmitResult<FormShape>>;
106
115
 
107
116
  /**
108
117
  * La validation, sous la même forme que `Form`.
@@ -164,6 +173,8 @@ interface CrudPageBase<T> {
164
173
  searchKeys?: string[];
165
174
  searchPlaceholder?: string;
166
175
  onSearch?: (query: string) => void;
176
+ /** Temporise `onSearch` — voir `DataTable`. 300 pour une recherche serveur. */
177
+ searchDelay?: number;
167
178
 
168
179
  loading?: boolean;
169
180
  error?: ReactNode;
@@ -173,6 +184,11 @@ interface CrudPageBase<T> {
173
184
  page?: number;
174
185
  totalPages?: number;
175
186
  onPageChange?: (page: number) => void;
187
+ /** Avec `onPageSizeChange`, ajoute le choix du nombre de lignes par page. */
188
+ pageSize?: number;
189
+ onPageSizeChange?: (pageSize: number) => void;
190
+ /** Les tailles proposées — voir `Pagination`. */
191
+ pageSizeOptions?: number[];
176
192
 
177
193
  /**
178
194
  * Ce qui n'a pas de raccourci ici passe au tableau tel quel — densité,
@@ -266,6 +282,7 @@ export function CrudPage<T>({
266
282
  searchKeys: clesDeRecherche,
267
283
  searchPlaceholder,
268
284
  onSearch,
285
+ searchDelay,
269
286
  loading = false,
270
287
  error,
271
288
  onRetry,
@@ -273,6 +290,9 @@ export function CrudPage<T>({
273
290
  page,
274
291
  totalPages,
275
292
  onPageChange,
293
+ pageSize,
294
+ onPageSizeChange,
295
+ pageSizeOptions,
276
296
  table,
277
297
  children,
278
298
  className,
@@ -416,11 +436,17 @@ export function CrudPage<T>({
416
436
  return actionsDeLigne.length > 0 ? actionsDeLigne : undefined;
417
437
  }
418
438
 
439
+ // Les ajouts se glissent avant « Supprimer » : la destruction reste la
440
+ // dernière action, là où on ne la vise pas par mégarde.
441
+ const suppression = actionsDeLigne.filter((a) => a.key === "delete");
442
+ const autres = actionsDeLigne.filter((a) => a.key !== "delete");
443
+
419
444
  return (row: T, index: number) => [
420
- ...actionsDeLigne,
445
+ ...autres,
421
446
  ...(typeof extraRowActions === "function"
422
447
  ? extraRowActions(row, index)
423
448
  : extraRowActions),
449
+ ...suppression,
424
450
  ];
425
451
  }, [actionsDeLigne, extraRowActions]);
426
452
 
@@ -471,6 +497,7 @@ export function CrudPage<T>({
471
497
  {...(searchKeys ? { searchKeys } : {})}
472
498
  {...(searchPlaceholder ? { searchPlaceholder } : {})}
473
499
  {...(onSearch ? { onSearch } : {})}
500
+ {...(searchDelay !== undefined ? { searchDelay } : {})}
474
501
  {...(toutesLesActions ? { rowActions: toutesLesActions } : {})}
475
502
  {...(can ? { can } : {})}
476
503
  loading={loading}
@@ -543,6 +570,9 @@ export function CrudPage<T>({
543
570
  totalPages={totalPages}
544
571
  onPageChange={onPageChange}
545
572
  showTotal
573
+ {...(pageSize !== undefined ? { pageSize } : {})}
574
+ {...(onPageSizeChange ? { onPageSizeChange } : {})}
575
+ {...(pageSizeOptions ? { pageSizeOptions } : {})}
546
576
  />
547
577
  )}
548
578
  </section>
@@ -54,6 +54,7 @@ export function DataTable<T>({
54
54
  searchKeys,
55
55
  searchPlaceholder,
56
56
  onSearch,
57
+ searchDelay = 0,
57
58
  layout = "auto",
58
59
  maxHeight,
59
60
  stickyHeader = false,
@@ -210,9 +211,12 @@ export function DataTable<T>({
210
211
  <SearchInput
211
212
  value={recherche}
212
213
  placeholder={searchPlaceholder ?? locale.search}
214
+ // Avec un délai, c'est le champ qui temporise : il sait aussi
215
+ // déclencher tout de suite sur Entrée et à l'effacement.
216
+ {...(onSearch && searchDelay > 0 ? { onSearch, searchDelay } : {})}
213
217
  onValueChange={(v) => {
214
218
  setRecherche(v);
215
- onSearch?.(v);
219
+ if (searchDelay <= 0) onSearch?.(v);
216
220
  }}
217
221
  />
218
222
  )}
@@ -107,6 +107,14 @@ export interface DataTableProps<T> {
107
107
  searchPlaceholder?: string;
108
108
  /** Recherche déléguée. Fournie, le tableau cesse de filtrer lui-même. */
109
109
  onSearch?: (query: string) => void;
110
+ /**
111
+ * Millisecondes d'inactivité avant d'appeler `onSearch`.
112
+ *
113
+ * `0` — le défaut — l'appelle à chaque frappe, ce qui convient à un
114
+ * filtrage local. Pour une recherche serveur, 300 évite une requête par
115
+ * lettre ; Entrée et l'effacement déclenchent sans attendre.
116
+ */
117
+ searchDelay?: number;
110
118
 
111
119
  /* ─────────────────────────────────────────────────────── mise en page */
112
120