@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.
- package/CHANGELOG.md +41 -0
- package/README.md +74 -19
- package/dist/registry/package/components/AppShell/index.tsx +101 -7
- package/dist/registry/package/components/AppShell/styles.css +87 -0
- package/dist/registry/package/components/Calendar/index.tsx +4 -4
- package/dist/registry/package/components/CrudPage/dialogs.tsx +20 -20
- package/dist/registry/package/components/CrudPage/index.tsx +34 -4
- package/dist/registry/package/components/DataTable/index.tsx +5 -1
- package/dist/registry/package/components/DataTable/types.ts +8 -0
- package/dist/registry/package/components/Drawer/index.tsx +17 -7
- package/dist/registry/package/components/Drawer/styles.css +53 -28
- package/dist/registry/package/components/Form/index.tsx +69 -3
- package/dist/registry/package/components/Icons/index.tsx +9 -0
- package/dist/registry/package/components/Pagination/index.tsx +168 -61
- package/dist/registry/package/components/Pagination/styles.css +133 -38
- package/dist/registry/package/components/PhoneInput/index.tsx +46 -29
- package/dist/registry/package/components/PhoneInput/styles.css +7 -21
- package/dist/registry/package/components/Select/index.tsx +34 -8
- package/dist/registry/package/components/Select/styles.css +18 -0
- package/dist/registry/package/components/Sidebar/index.tsx +23 -3
- package/dist/registry/package/components/Sidebar/styles.css +1 -13
- package/dist/registry/package/components/Timer/index.tsx +1 -1
- package/dist/registry/package/components/Toggle/styles.css +3 -2
- package/dist/registry/package/lib/form-adapter-rhf.tsx +23 -10
- package/dist/registry/registry.json +19 -12
- package/dist/registry/styles/sia-ui.css +17 -0
- 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
|
-
|
|
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
|
-
*
|
|
101
|
-
*
|
|
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.
|
|
111
|
-
*
|
|
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 &&
|
|
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
|
|
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
|
-
|
|
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,
|
|
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
|
|
2
|
+
import {
|
|
3
|
+
hasFormErrors,
|
|
4
|
+
type FormSubmitResult,
|
|
5
|
+
type FormValidator,
|
|
6
|
+
} from "@sia-ui/headless";
|
|
3
7
|
import { Modal } from "../Modal";
|
|
4
|
-
import {
|
|
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
|
-
*
|
|
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
|
|
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
|
|
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
|
-
) =>
|
|
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 {
|
|
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
|
-
/**
|
|
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
|
-
) =>
|
|
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
|
-
...
|
|
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
|
|