@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
|
@@ -12,6 +12,7 @@ import "./styles.css";
|
|
|
12
12
|
import { useOutsideClick } from "@sia-ui/react";
|
|
13
13
|
import { DrawerContext } from "./context";
|
|
14
14
|
import { overlayStack, useDisclosureState } from "@sia-ui/headless";
|
|
15
|
+
import { XIcon } from "../Icons";
|
|
15
16
|
/** Ce que la réf impérative expose — ouvrir un tiroir depuis l'extérieur. */
|
|
16
17
|
export type DrawerRef = {
|
|
17
18
|
open: () => void;
|
|
@@ -65,9 +66,10 @@ export const Drawer = forwardRef<DrawerRef, IDrawerProps>(
|
|
|
65
66
|
closeOnOutsideClick = true,
|
|
66
67
|
closeOnEsc = true,
|
|
67
68
|
children,
|
|
69
|
+
className,
|
|
68
70
|
...rest
|
|
69
71
|
},
|
|
70
|
-
ref
|
|
72
|
+
ref,
|
|
71
73
|
) => {
|
|
72
74
|
// Ouverture, fermeture et phase de sortie vivent dans @sia-ui/headless :
|
|
73
75
|
// rien là-dedans n'est propre au DOM, et le pendant mobile réutilisera la
|
|
@@ -77,6 +79,7 @@ export const Drawer = forwardRef<DrawerRef, IDrawerProps>(
|
|
|
77
79
|
phase: state,
|
|
78
80
|
open: doOpen,
|
|
79
81
|
close: doClose,
|
|
82
|
+
toggle,
|
|
80
83
|
} = useDisclosureState({
|
|
81
84
|
...(open !== undefined ? { open } : {}),
|
|
82
85
|
defaultOpen,
|
|
@@ -86,6 +89,11 @@ export const Drawer = forwardRef<DrawerRef, IDrawerProps>(
|
|
|
86
89
|
|
|
87
90
|
const drawerRef = useRef<HTMLDivElement | null>(null);
|
|
88
91
|
const zIndexRef = useRef<number | null>(null);
|
|
92
|
+
// Lue par l'écouteur d'Échap : la mettre dans ses dépendances le
|
|
93
|
+
// réabonnerait — et rebasculerait le verrou du body — à chaque rendu où
|
|
94
|
+
// `onOpenChange` est une fonction en ligne.
|
|
95
|
+
const fermer = useRef(doClose);
|
|
96
|
+
fermer.current = doClose;
|
|
89
97
|
|
|
90
98
|
/* 🔌 API IMPÉRATIVE */
|
|
91
99
|
useImperativeHandle(
|
|
@@ -93,10 +101,10 @@ export const Drawer = forwardRef<DrawerRef, IDrawerProps>(
|
|
|
93
101
|
() => ({
|
|
94
102
|
open: doOpen,
|
|
95
103
|
close: doClose,
|
|
96
|
-
toggle
|
|
104
|
+
toggle,
|
|
97
105
|
isOpen: () => isOpen,
|
|
98
106
|
}),
|
|
99
|
-
[isOpen,
|
|
107
|
+
[isOpen, doOpen, doClose, toggle],
|
|
100
108
|
);
|
|
101
109
|
|
|
102
110
|
/* Overlay stack */
|
|
@@ -121,7 +129,7 @@ export const Drawer = forwardRef<DrawerRef, IDrawerProps>(
|
|
|
121
129
|
const onKeyDown = (e: KeyboardEvent) => {
|
|
122
130
|
if (!closeOnEsc || e.key !== "Escape") return;
|
|
123
131
|
if (overlayStack.isTop(zIndexRef.current)) {
|
|
124
|
-
|
|
132
|
+
fermer.current();
|
|
125
133
|
}
|
|
126
134
|
};
|
|
127
135
|
|
|
@@ -150,6 +158,7 @@ export const Drawer = forwardRef<DrawerRef, IDrawerProps>(
|
|
|
150
158
|
<DrawerContext.Provider value={{ close: doClose }}>
|
|
151
159
|
<div
|
|
152
160
|
className="sia-drawer-root"
|
|
161
|
+
data-state={state}
|
|
153
162
|
style={{ zIndex: zIndexRef.current ?? undefined }}
|
|
154
163
|
>
|
|
155
164
|
<div className="sia-drawer-overlay" />
|
|
@@ -163,7 +172,8 @@ export const Drawer = forwardRef<DrawerRef, IDrawerProps>(
|
|
|
163
172
|
"sia-drawer",
|
|
164
173
|
`sia-drawer-${position}`,
|
|
165
174
|
`sia-drawer-${size}`,
|
|
166
|
-
state === "closing" && "is-closing"
|
|
175
|
+
state === "closing" && "is-closing",
|
|
176
|
+
className,
|
|
167
177
|
)}
|
|
168
178
|
{...rest}
|
|
169
179
|
>
|
|
@@ -173,7 +183,7 @@ export const Drawer = forwardRef<DrawerRef, IDrawerProps>(
|
|
|
173
183
|
</DrawerContext.Provider>
|
|
174
184
|
</Portal>
|
|
175
185
|
);
|
|
176
|
-
}
|
|
186
|
+
},
|
|
177
187
|
) as DrawerComponent;
|
|
178
188
|
|
|
179
189
|
Drawer.displayName = "SiaDrawer";
|
|
@@ -205,7 +215,7 @@ export function DrawerClose() {
|
|
|
205
215
|
onClick={ctx.close}
|
|
206
216
|
aria-label="Fermer"
|
|
207
217
|
>
|
|
208
|
-
|
|
218
|
+
<XIcon aria-hidden="true" />
|
|
209
219
|
</button>
|
|
210
220
|
);
|
|
211
221
|
}
|
|
@@ -7,17 +7,24 @@
|
|
|
7
7
|
.sia-drawer-overlay {
|
|
8
8
|
position: absolute;
|
|
9
9
|
inset: 0;
|
|
10
|
-
|
|
11
|
-
|
|
10
|
+
z-index: 0;
|
|
11
|
+
background: rgb(2 6 23 / .52);
|
|
12
|
+
animation: sia-drawer-fade-in var(--sia-duration-base) var(--sia-easing-entrance) both;
|
|
12
13
|
}
|
|
14
|
+
.sia-drawer-root[data-state="closing"] .sia-drawer-overlay { animation:sia-drawer-fade-out var(--sia-duration-base) var(--sia-easing-exit) both; }
|
|
13
15
|
|
|
14
16
|
.sia-drawer {
|
|
15
17
|
position: absolute;
|
|
16
|
-
|
|
18
|
+
z-index: 1;
|
|
19
|
+
color: var(--sia-foreground, #0f172a);
|
|
20
|
+
background: var(--sia-background, #fff);
|
|
21
|
+
border: 1px solid var(--sia-border, #e2e8f0);
|
|
17
22
|
display: flex;
|
|
18
23
|
flex-direction: column;
|
|
19
|
-
width: var(--sia-drawer-width);
|
|
20
|
-
|
|
24
|
+
width: min(var(--sia-drawer-width), calc(100vw - 2rem));
|
|
25
|
+
overflow: hidden;
|
|
26
|
+
isolation: isolate;
|
|
27
|
+
box-shadow: 0 20px 50px rgb(2 6 23 / .24);
|
|
21
28
|
will-change: transform;
|
|
22
29
|
}
|
|
23
30
|
|
|
@@ -27,7 +34,7 @@
|
|
|
27
34
|
right: 0;
|
|
28
35
|
top: 0;
|
|
29
36
|
height: 100%;
|
|
30
|
-
animation:
|
|
37
|
+
animation: sia-drawer-in-right var(--sia-duration-slow) var(--sia-easing-entrance) both;
|
|
31
38
|
}
|
|
32
39
|
|
|
33
40
|
/* LEFT */
|
|
@@ -35,7 +42,7 @@
|
|
|
35
42
|
left: 0;
|
|
36
43
|
top: 0;
|
|
37
44
|
height: 100%;
|
|
38
|
-
animation:
|
|
45
|
+
animation: sia-drawer-in-left var(--sia-duration-slow) var(--sia-easing-entrance) both;
|
|
39
46
|
}
|
|
40
47
|
|
|
41
48
|
/* TOP */
|
|
@@ -43,9 +50,11 @@
|
|
|
43
50
|
top: 0;
|
|
44
51
|
left: 0;
|
|
45
52
|
right: 0;
|
|
46
|
-
|
|
53
|
+
width: 100%;
|
|
54
|
+
max-width: 100%;
|
|
55
|
+
max-height: min(80vh, 48rem);
|
|
47
56
|
--sia-drawer-width: 100% !important;
|
|
48
|
-
animation:
|
|
57
|
+
animation: sia-drawer-in-top var(--sia-duration-slow) var(--sia-easing-entrance) both;
|
|
49
58
|
}
|
|
50
59
|
|
|
51
60
|
/* BOTTOM */
|
|
@@ -53,9 +62,11 @@
|
|
|
53
62
|
bottom: 0;
|
|
54
63
|
left: 0;
|
|
55
64
|
right: 0;
|
|
56
|
-
|
|
65
|
+
width: 100%;
|
|
66
|
+
max-width: 100%;
|
|
67
|
+
max-height: min(80vh, 48rem);
|
|
57
68
|
--sia-drawer-width: 100% !important;
|
|
58
|
-
animation:
|
|
69
|
+
animation: sia-drawer-in-bottom var(--sia-duration-slow) var(--sia-easing-entrance) both;
|
|
59
70
|
}
|
|
60
71
|
|
|
61
72
|
|
|
@@ -68,14 +79,15 @@
|
|
|
68
79
|
.sia-drawer-header,
|
|
69
80
|
.sia-drawer-footer {
|
|
70
81
|
padding: 16px;
|
|
71
|
-
border-bottom: 1px solid var(--sia-
|
|
82
|
+
border-bottom: 1px solid var(--sia-border, #e2e8f0);
|
|
72
83
|
display: flex;
|
|
73
84
|
align-items: center;
|
|
74
85
|
justify-content: space-between;
|
|
75
86
|
}
|
|
76
87
|
|
|
77
88
|
.sia-drawer-footer {
|
|
78
|
-
border-top: 1px solid var(--sia-
|
|
89
|
+
border-top: 1px solid var(--sia-border, #e2e8f0);
|
|
90
|
+
border-bottom: 0;
|
|
79
91
|
margin-top: auto;
|
|
80
92
|
}
|
|
81
93
|
|
|
@@ -85,67 +97,80 @@
|
|
|
85
97
|
}
|
|
86
98
|
|
|
87
99
|
.sia-drawer-title {
|
|
100
|
+
margin: 0;
|
|
88
101
|
font-size: 1.1rem;
|
|
89
102
|
font-weight: 600;
|
|
90
103
|
}
|
|
91
104
|
|
|
92
105
|
.sia-drawer-close {
|
|
106
|
+
display: grid;
|
|
107
|
+
place-items: center;
|
|
108
|
+
flex: none;
|
|
109
|
+
width: 2.25rem;
|
|
110
|
+
height: 2.25rem;
|
|
93
111
|
margin-left: auto;
|
|
94
|
-
|
|
95
|
-
|
|
112
|
+
padding: 0;
|
|
113
|
+
color: var(--sia-muted, #64748b);
|
|
114
|
+
background: transparent;
|
|
115
|
+
border: 0;
|
|
116
|
+
border-radius: var(--sia-radius, .375rem);
|
|
96
117
|
cursor: pointer;
|
|
97
|
-
font-size: 1.2rem;
|
|
98
118
|
}
|
|
119
|
+
.sia-drawer-close:hover { color:var(--sia-foreground,#0f172a); background:var(--sia-surface,#f8fafc); }
|
|
120
|
+
.sia-drawer-close:focus-visible { outline:2px solid var(--sia-primary,#2563eb); outline-offset:2px; }
|
|
121
|
+
.sia-drawer-close svg { width:1.125rem; height:1.125rem; }
|
|
99
122
|
|
|
100
123
|
.sia-drawer-right.is-closing {
|
|
101
|
-
animation:
|
|
124
|
+
animation: sia-drawer-out-right var(--sia-duration-base) var(--sia-easing-exit) both;
|
|
102
125
|
}
|
|
103
126
|
|
|
104
127
|
.sia-drawer-left.is-closing {
|
|
105
|
-
animation:
|
|
128
|
+
animation: sia-drawer-out-left var(--sia-duration-base) var(--sia-easing-exit) both;
|
|
106
129
|
}
|
|
107
130
|
|
|
108
131
|
.sia-drawer-top.is-closing {
|
|
109
|
-
animation:
|
|
132
|
+
animation: sia-drawer-out-top var(--sia-duration-base) var(--sia-easing-exit) both;
|
|
110
133
|
}
|
|
111
134
|
|
|
112
135
|
.sia-drawer-bottom.is-closing {
|
|
113
|
-
animation:
|
|
136
|
+
animation: sia-drawer-out-bottom var(--sia-duration-base) var(--sia-easing-exit) both;
|
|
114
137
|
}
|
|
115
138
|
|
|
116
139
|
/* Animations */
|
|
117
|
-
@keyframes
|
|
140
|
+
@keyframes sia-drawer-fade-in { from { opacity:0; } to { opacity:1; } }
|
|
141
|
+
@keyframes sia-drawer-fade-out { to { opacity:0; } }
|
|
142
|
+
@keyframes sia-drawer-in-right {
|
|
118
143
|
from { transform: translateX(100%); }
|
|
119
144
|
to { transform: translateX(0); }
|
|
120
145
|
}
|
|
121
146
|
|
|
122
|
-
@keyframes
|
|
147
|
+
@keyframes sia-drawer-in-left {
|
|
123
148
|
from { transform: translateX(-100%); }
|
|
124
149
|
to { transform: translateX(0); }
|
|
125
150
|
}
|
|
126
151
|
|
|
127
|
-
@keyframes
|
|
152
|
+
@keyframes sia-drawer-in-top {
|
|
128
153
|
from { transform: translateY(-100%); }
|
|
129
154
|
to { transform: translateY(0); }
|
|
130
155
|
}
|
|
131
156
|
|
|
132
|
-
@keyframes
|
|
157
|
+
@keyframes sia-drawer-in-bottom {
|
|
133
158
|
from { transform: translateY(100%); }
|
|
134
159
|
to { transform: translateY(0); }
|
|
135
160
|
}
|
|
136
161
|
|
|
137
|
-
@keyframes
|
|
162
|
+
@keyframes sia-drawer-out-right {
|
|
138
163
|
to { transform: translateX(100%); }
|
|
139
164
|
}
|
|
140
165
|
|
|
141
|
-
@keyframes
|
|
166
|
+
@keyframes sia-drawer-out-left {
|
|
142
167
|
to { transform: translateX(-100%); }
|
|
143
168
|
}
|
|
144
169
|
|
|
145
|
-
@keyframes
|
|
170
|
+
@keyframes sia-drawer-out-top {
|
|
146
171
|
to { transform: translateY(-100%); }
|
|
147
172
|
}
|
|
148
173
|
|
|
149
|
-
@keyframes
|
|
174
|
+
@keyframes sia-drawer-out-bottom {
|
|
150
175
|
to { transform: translateY(100%); }
|
|
151
176
|
}
|
|
@@ -1,10 +1,15 @@
|
|
|
1
|
-
import { type CSSProperties, type ReactNode } from "react";
|
|
1
|
+
import { useState, type CSSProperties, type ReactNode } from "react";
|
|
2
2
|
import { cn } from "@sia-ui/utils";
|
|
3
3
|
import {
|
|
4
|
+
readSubmitError,
|
|
4
5
|
useLocalForm,
|
|
6
|
+
useSiaLocale,
|
|
5
7
|
type FormAdapter,
|
|
8
|
+
type FormErrors,
|
|
9
|
+
type FormSubmitResult,
|
|
6
10
|
type FormValidator,
|
|
7
11
|
} from "@sia-ui/headless";
|
|
12
|
+
import { Alert } from "../Alert";
|
|
8
13
|
import { Button, type ButtonProps } from "../Button";
|
|
9
14
|
import { Field, type FieldProps, type FieldValue } from "../Field";
|
|
10
15
|
import "./styles.css";
|
|
@@ -32,6 +37,16 @@ function isGroup(entry: FormEntry): entry is FormFieldGroup {
|
|
|
32
37
|
return "group" in entry;
|
|
33
38
|
}
|
|
34
39
|
|
|
40
|
+
/** Les noms de champs, groupes aplatis. */
|
|
41
|
+
export function formFieldNames(fields: FormEntry[]): string[] {
|
|
42
|
+
const noms: string[] = [];
|
|
43
|
+
for (const entree of fields) {
|
|
44
|
+
if (isGroup(entree)) noms.push(...entree.fields.map((f) => f.name));
|
|
45
|
+
else noms.push(entree.name);
|
|
46
|
+
}
|
|
47
|
+
return noms;
|
|
48
|
+
}
|
|
49
|
+
|
|
35
50
|
/**
|
|
36
51
|
* Les valeurs qu'un formulaire déclaratif peut porter.
|
|
37
52
|
*
|
|
@@ -54,7 +69,16 @@ export interface FormProps<TValues extends FormShape = FormShape> {
|
|
|
54
69
|
defaultValues?: TValues;
|
|
55
70
|
/** La validation. Un schéma Zod s'y branche en cinq lignes. */
|
|
56
71
|
validate?: FormValidator<TValues>;
|
|
57
|
-
|
|
72
|
+
/**
|
|
73
|
+
* L'envoi.
|
|
74
|
+
*
|
|
75
|
+
* Rendre des erreurs par champ les pose sous les champs. Lever une erreur
|
|
76
|
+
* l'affiche dans une alerte en tête des boutons — et une `HttpError` de
|
|
77
|
+
* `@sia-ui/api` voit en plus ses erreurs de champ posées à leur place.
|
|
78
|
+
*/
|
|
79
|
+
onSubmit?: (
|
|
80
|
+
values: TValues,
|
|
81
|
+
) => FormSubmitResult<TValues> | Promise<FormSubmitResult<TValues>>;
|
|
58
82
|
columns?: number;
|
|
59
83
|
submitText?: ReactNode;
|
|
60
84
|
clearText?: ReactNode;
|
|
@@ -118,6 +142,38 @@ export function Form<TValues extends FormShape = FormShape>({
|
|
|
118
142
|
...(onSubmit ? { onSubmit } : {}),
|
|
119
143
|
});
|
|
120
144
|
const form = adapter ?? interne;
|
|
145
|
+
const locale = useSiaLocale();
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* L'échec du dernier envoi, quand il ne se range pas sous un champ.
|
|
149
|
+
*
|
|
150
|
+
* Sans lui, un 409 ou un 500 devenait une promesse rejetée que personne ne
|
|
151
|
+
* lisait : le bouton se relâchait, et rien ne disait pourquoi.
|
|
152
|
+
*/
|
|
153
|
+
const [echec, setEchec] = useState<string | null>(null);
|
|
154
|
+
|
|
155
|
+
const envoyer = () => {
|
|
156
|
+
setEchec(null);
|
|
157
|
+
form.submit().catch((error: unknown) => {
|
|
158
|
+
const { fields: erreurs, message } = readSubmitError(error);
|
|
159
|
+
const connus = new Set(formFieldNames(fields));
|
|
160
|
+
const sousLesChamps: Record<string, string> = {};
|
|
161
|
+
const ailleurs: string[] = [];
|
|
162
|
+
|
|
163
|
+
for (const [nom, texte] of Object.entries(erreurs)) {
|
|
164
|
+
if (connus.has(nom)) sousLesChamps[nom] = texte;
|
|
165
|
+
else ailleurs.push(texte);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
form.setErrors(sousLesChamps as FormErrors<TValues>);
|
|
169
|
+
// Ce que les champs affichent déjà n'est pas répété dans l'alerte ; ce
|
|
170
|
+
// qui vise un champ absent du formulaire, si.
|
|
171
|
+
if (ailleurs.length > 0) setEchec(ailleurs.join(" "));
|
|
172
|
+
else if (Object.keys(sousLesChamps).length === 0) {
|
|
173
|
+
setEchec(message ?? locale.submitError);
|
|
174
|
+
}
|
|
175
|
+
});
|
|
176
|
+
};
|
|
121
177
|
|
|
122
178
|
const busy = loading || form.state.isSubmitting;
|
|
123
179
|
const submitDisabled =
|
|
@@ -157,7 +213,7 @@ export function Form<TValues extends FormShape = FormShape>({
|
|
|
157
213
|
noValidate
|
|
158
214
|
onSubmit={(event) => {
|
|
159
215
|
event.preventDefault();
|
|
160
|
-
|
|
216
|
+
envoyer();
|
|
161
217
|
}}
|
|
162
218
|
>
|
|
163
219
|
<div className="sia-form-grid" style={grille(columns)}>
|
|
@@ -184,6 +240,16 @@ export function Form<TValues extends FormShape = FormShape>({
|
|
|
184
240
|
)}
|
|
185
241
|
</div>
|
|
186
242
|
|
|
243
|
+
{echec && (
|
|
244
|
+
<Alert
|
|
245
|
+
tone="danger"
|
|
246
|
+
className="sia-form-error"
|
|
247
|
+
onDismiss={() => setEchec(null)}
|
|
248
|
+
>
|
|
249
|
+
{echec}
|
|
250
|
+
</Alert>
|
|
251
|
+
)}
|
|
252
|
+
|
|
187
253
|
{actions ?? (
|
|
188
254
|
<div className="sia-form-actions">
|
|
189
255
|
{showClear && (
|
|
@@ -182,6 +182,15 @@ export function InfoIcon(props: IconProps) {
|
|
|
182
182
|
</IconBase>
|
|
183
183
|
);
|
|
184
184
|
}
|
|
185
|
+
|
|
186
|
+
export function SettingsIcon(props: IconProps) {
|
|
187
|
+
return (
|
|
188
|
+
<IconBase {...props}>
|
|
189
|
+
<circle cx="12" cy="12" r="3" />
|
|
190
|
+
<path d="M19.4 15a1.7 1.7 0 0 0 .3 1.9l.1.1-2.8 2.8-.1-.1a1.7 1.7 0 0 0-1.9-.3 1.7 1.7 0 0 0-1 1.6v.2h-4V21a1.7 1.7 0 0 0-1-1.6 1.7 1.7 0 0 0-1.9.3l-.1.1L4.2 17l.1-.1a1.7 1.7 0 0 0 .3-1.9A1.7 1.7 0 0 0 3 14H2.8v-4H3a1.7 1.7 0 0 0 1.6-1 1.7 1.7 0 0 0-.3-1.9L4.2 7 7 4.2l.1.1A1.7 1.7 0 0 0 9 4.6 1.7 1.7 0 0 0 10 3v-.2h4V3a1.7 1.7 0 0 0 1 1.6 1.7 1.7 0 0 0 1.9-.3l.1-.1L19.8 7l-.1.1a1.7 1.7 0 0 0-.3 1.9 1.7 1.7 0 0 0 1.6 1h.2v4H21a1.7 1.7 0 0 0-1.6 1Z" />
|
|
191
|
+
</IconBase>
|
|
192
|
+
);
|
|
193
|
+
}
|
|
185
194
|
export function EyeIcon(props: IconProps) {
|
|
186
195
|
return (
|
|
187
196
|
<IconBase {...props}>
|
|
@@ -1,7 +1,15 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
useEffect,
|
|
3
|
+
useId,
|
|
4
|
+
useMemo,
|
|
5
|
+
useState,
|
|
6
|
+
type FormEvent,
|
|
7
|
+
type ReactNode,
|
|
8
|
+
} from "react";
|
|
2
9
|
import { cn } from "@sia-ui/utils";
|
|
3
10
|
import { paginationItems, useSiaLocale } from "@sia-ui/headless";
|
|
4
11
|
import { ChevronLeftIcon, ChevronRightIcon } from "../Icons";
|
|
12
|
+
import { Select } from "../Select";
|
|
5
13
|
import "./styles.css";
|
|
6
14
|
|
|
7
15
|
export interface PaginationProps {
|
|
@@ -22,10 +30,46 @@ export interface PaginationProps {
|
|
|
22
30
|
showTotal?: boolean;
|
|
23
31
|
/** Masque les libellés et ne garde que les flèches. */
|
|
24
32
|
compact?: boolean;
|
|
33
|
+
|
|
34
|
+
/* ─────────────────────────────────────────────── taille de page */
|
|
35
|
+
|
|
36
|
+
/** Le nombre de lignes par page en vigueur. */
|
|
37
|
+
pageSize?: number;
|
|
38
|
+
/**
|
|
39
|
+
* Fourni avec `pageSize`, ajoute le choix du nombre de lignes par page.
|
|
40
|
+
*
|
|
41
|
+
* Seul le nouveau nombre est remonté : revenir en page 1 est l'affaire de
|
|
42
|
+
* qui tient l'état — `setPerPage` de `useTableQuery` le fait déjà.
|
|
43
|
+
*/
|
|
44
|
+
onPageSizeChange?: (pageSize: number) => void;
|
|
45
|
+
/**
|
|
46
|
+
* Les tailles proposées. Par défaut `10, 20, 50, 100`. Une taille en
|
|
47
|
+
* vigueur absente de la liste y est ajoutée, pour que le champ ne paraisse
|
|
48
|
+
* jamais vide.
|
|
49
|
+
*/
|
|
50
|
+
pageSizeOptions?: number[];
|
|
51
|
+
/** Le libellé du choix. Par défaut `locale.perPage` — « Par page ». */
|
|
52
|
+
pageSizeLabel?: string;
|
|
53
|
+
/**
|
|
54
|
+
* Remplace entièrement le contrôle — des boutons segmentés, un champ libre.
|
|
55
|
+
* Le choix des tailles et leur tri sont déjà faits.
|
|
56
|
+
*/
|
|
57
|
+
renderPageSize?: (context: PageSizeContext) => ReactNode;
|
|
58
|
+
|
|
25
59
|
ariaLabel?: string;
|
|
26
60
|
className?: string;
|
|
27
61
|
}
|
|
28
62
|
|
|
63
|
+
/** Ce que reçoit `renderPageSize`. */
|
|
64
|
+
export interface PageSizeContext {
|
|
65
|
+
pageSize: number;
|
|
66
|
+
options: number[];
|
|
67
|
+
onChange: (pageSize: number) => void;
|
|
68
|
+
label: string;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export const DEFAULT_PAGE_SIZE_OPTIONS = [10, 20, 50, 100];
|
|
72
|
+
|
|
29
73
|
/**
|
|
30
74
|
* La navigation entre les pages.
|
|
31
75
|
*
|
|
@@ -42,6 +86,11 @@ export function Pagination({
|
|
|
42
86
|
jumpTo = false,
|
|
43
87
|
showTotal = false,
|
|
44
88
|
compact = false,
|
|
89
|
+
pageSize,
|
|
90
|
+
onPageSizeChange,
|
|
91
|
+
pageSizeOptions = DEFAULT_PAGE_SIZE_OPTIONS,
|
|
92
|
+
pageSizeLabel,
|
|
93
|
+
renderPageSize,
|
|
45
94
|
ariaLabel,
|
|
46
95
|
className,
|
|
47
96
|
}: PaginationProps) {
|
|
@@ -59,9 +108,25 @@ export function Pagination({
|
|
|
59
108
|
// un retour arrière du navigateur.
|
|
60
109
|
useEffect(() => setDraft(String(current)), [current]);
|
|
61
110
|
|
|
62
|
-
|
|
111
|
+
const tailles = useMemo(() => {
|
|
112
|
+
if (pageSize === undefined || pageSizeOptions.includes(pageSize)) {
|
|
113
|
+
return pageSizeOptions;
|
|
114
|
+
}
|
|
115
|
+
return [...pageSizeOptions, pageSize].sort((a, b) => a - b);
|
|
116
|
+
}, [pageSize, pageSizeOptions]);
|
|
117
|
+
|
|
118
|
+
const choixTaille =
|
|
119
|
+
pageSize !== undefined && onPageSizeChange && tailles.length > 0;
|
|
120
|
+
const plusieursPages = totalPages > 1;
|
|
121
|
+
const tailleLibelle = pageSizeLabel ?? locale.perPage;
|
|
63
122
|
|
|
64
|
-
|
|
123
|
+
// Une seule page n'a rien à paginer — sauf si l'on peut encore changer de
|
|
124
|
+
// taille : après avoir choisi 100 lignes, il faut pouvoir revenir à 10.
|
|
125
|
+
if (!plusieursPages && !choixTaille) return null;
|
|
126
|
+
|
|
127
|
+
const items = plusieursPages
|
|
128
|
+
? paginationItems(current, totalPages, siblingCount)
|
|
129
|
+
: [];
|
|
65
130
|
|
|
66
131
|
const jump = (event: FormEvent) => {
|
|
67
132
|
event.preventDefault();
|
|
@@ -84,71 +149,113 @@ export function Pagination({
|
|
|
84
149
|
)}
|
|
85
150
|
aria-label={ariaLabel ?? "Pagination"}
|
|
86
151
|
>
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
<
|
|
95
|
-
{!compact && <span>{previousLabel}</span>}
|
|
96
|
-
</button>
|
|
97
|
-
|
|
98
|
-
{items.map((item) =>
|
|
99
|
-
typeof item === "number" ? (
|
|
152
|
+
{/*
|
|
153
|
+
Deux groupes, qui passent à la ligne chacun d'un seul tenant : la
|
|
154
|
+
navigation d'un côté, les réglages de l'autre. Sans eux, un écran un
|
|
155
|
+
peu étroit renvoyait « Par page » seul sur une seconde ligne, loin de
|
|
156
|
+
son champ.
|
|
157
|
+
*/}
|
|
158
|
+
{plusieursPages && (
|
|
159
|
+
<div className="sia-pagination__pages">
|
|
100
160
|
<button
|
|
101
161
|
type="button"
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
aria-
|
|
105
|
-
onClick={() => onPageChange(
|
|
162
|
+
className="sia-pagination__step"
|
|
163
|
+
disabled={current === 1}
|
|
164
|
+
aria-label={previousLabel}
|
|
165
|
+
onClick={() => onPageChange(current - 1)}
|
|
106
166
|
>
|
|
107
|
-
|
|
167
|
+
<ChevronLeftIcon />
|
|
168
|
+
{!compact && <span>{previousLabel}</span>}
|
|
108
169
|
</button>
|
|
109
|
-
) : (
|
|
110
|
-
<span key={item} className="sia-pagination__gap" aria-hidden="true">
|
|
111
|
-
…
|
|
112
|
-
</span>
|
|
113
|
-
),
|
|
114
|
-
)}
|
|
115
170
|
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
171
|
+
{items.map((item) =>
|
|
172
|
+
typeof item === "number" ? (
|
|
173
|
+
<button
|
|
174
|
+
type="button"
|
|
175
|
+
key={item}
|
|
176
|
+
className="sia-pagination__page"
|
|
177
|
+
aria-current={item === current ? "page" : undefined}
|
|
178
|
+
onClick={() => onPageChange(item)}
|
|
179
|
+
>
|
|
180
|
+
{item}
|
|
181
|
+
</button>
|
|
182
|
+
) : (
|
|
183
|
+
<span key={item} className="sia-pagination__gap" aria-hidden="true">
|
|
184
|
+
…
|
|
185
|
+
</span>
|
|
186
|
+
),
|
|
187
|
+
)}
|
|
188
|
+
|
|
189
|
+
<button
|
|
190
|
+
type="button"
|
|
191
|
+
className="sia-pagination__step"
|
|
192
|
+
disabled={current === totalPages}
|
|
193
|
+
aria-label={nextLabel}
|
|
194
|
+
onClick={() => onPageChange(current + 1)}
|
|
195
|
+
>
|
|
196
|
+
{!compact && <span>{nextLabel}</span>}
|
|
197
|
+
<ChevronRightIcon />
|
|
198
|
+
</button>
|
|
199
|
+
</div>
|
|
131
200
|
)}
|
|
132
201
|
|
|
133
|
-
{jumpTo && (
|
|
134
|
-
<
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
202
|
+
{(choixTaille || (plusieursPages && (showTotal || jumpTo))) && (
|
|
203
|
+
<div className="sia-pagination__settings">
|
|
204
|
+
{plusieursPages && showTotal && (
|
|
205
|
+
<span className="sia-pagination__total">
|
|
206
|
+
{locale.page} {current} / {totalPages}
|
|
207
|
+
</span>
|
|
208
|
+
)}
|
|
209
|
+
|
|
210
|
+
{choixTaille && (
|
|
211
|
+
<div className="sia-pagination__size">
|
|
212
|
+
{renderPageSize ? (
|
|
213
|
+
renderPageSize({
|
|
214
|
+
pageSize,
|
|
215
|
+
options: tailles,
|
|
216
|
+
onChange: onPageSizeChange,
|
|
217
|
+
label: tailleLibelle,
|
|
218
|
+
})
|
|
219
|
+
) : (
|
|
220
|
+
<>
|
|
221
|
+
<span aria-hidden="true">{tailleLibelle}</span>
|
|
222
|
+
<Select
|
|
223
|
+
variant="embedded"
|
|
224
|
+
aria-label={tailleLibelle}
|
|
225
|
+
placement="top-end"
|
|
226
|
+
value={String(pageSize)}
|
|
227
|
+
options={tailles.map((taille) => ({
|
|
228
|
+
value: String(taille),
|
|
229
|
+
label: String(taille),
|
|
230
|
+
}))}
|
|
231
|
+
onValueChange={(valeur) => onPageSizeChange(Number(valeur))}
|
|
232
|
+
/>
|
|
233
|
+
</>
|
|
234
|
+
)}
|
|
235
|
+
</div>
|
|
236
|
+
)}
|
|
237
|
+
|
|
238
|
+
{plusieursPages && jumpTo && (
|
|
239
|
+
<form className="sia-pagination__jump" onSubmit={jump}>
|
|
240
|
+
<label htmlFor={jumpId}>{locale.goToPage}</label>
|
|
241
|
+
<input
|
|
242
|
+
id={jumpId}
|
|
243
|
+
// `text` plutôt que `number` : le champ numérique natif accepte
|
|
244
|
+
// « 1e5 » et « -- », et vide sa valeur sans prévenir quand elle
|
|
245
|
+
// ne lui plaît pas.
|
|
246
|
+
type="text"
|
|
247
|
+
inputMode="numeric"
|
|
248
|
+
value={draft}
|
|
249
|
+
aria-label={locale.goToPage}
|
|
250
|
+
onChange={(event) => setDraft(event.target.value.replace(/\D/g, ""))}
|
|
251
|
+
onBlur={jump}
|
|
252
|
+
/>
|
|
253
|
+
<span className="sia-pagination__jump-total" aria-hidden="true">
|
|
254
|
+
/ {totalPages}
|
|
255
|
+
</span>
|
|
256
|
+
</form>
|
|
257
|
+
)}
|
|
258
|
+
</div>
|
|
152
259
|
)}
|
|
153
260
|
</nav>
|
|
154
261
|
);
|