@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
|
@@ -1,19 +1,58 @@
|
|
|
1
|
-
|
|
1
|
+
/* ── Disposition ──────────────────────────────────────────────────────────
|
|
2
|
+
Deux groupes : la navigation à gauche, les réglages à droite. Chacun passe
|
|
3
|
+
à la ligne d'un seul tenant — jamais un libellé séparé de son champ. Tout
|
|
4
|
+
partage une hauteur, `--sia-pagination-size`, pour que boutons, liste et
|
|
5
|
+
champ s'alignent au pixel. */
|
|
6
|
+
.sia-pagination {
|
|
7
|
+
--sia-pagination-size: 2.25rem;
|
|
8
|
+
container: sia-pagination / inline-size;
|
|
9
|
+
display: flex;
|
|
10
|
+
flex-wrap: wrap;
|
|
11
|
+
align-items: center;
|
|
12
|
+
justify-content: space-between;
|
|
13
|
+
gap: .75rem 1.5rem;
|
|
14
|
+
width: 100%;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
.sia-pagination__pages {
|
|
18
|
+
display: flex;
|
|
19
|
+
flex-wrap: wrap;
|
|
20
|
+
align-items: center;
|
|
21
|
+
gap: .3rem;
|
|
22
|
+
min-width: 0;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
.sia-pagination__settings {
|
|
26
|
+
display: flex;
|
|
27
|
+
flex-wrap: wrap;
|
|
28
|
+
align-items: center;
|
|
29
|
+
justify-content: flex-end;
|
|
30
|
+
gap: .5rem 1.25rem;
|
|
31
|
+
margin-left: auto;
|
|
32
|
+
color: var(--sia-muted);
|
|
33
|
+
font-size: .8rem;
|
|
34
|
+
white-space: nowrap;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/* ── Boutons de page ──────────────────────────────────────────────────── */
|
|
2
38
|
|
|
3
|
-
.sia-
|
|
39
|
+
.sia-pagination__step,
|
|
40
|
+
.sia-pagination__page {
|
|
4
41
|
display: inline-flex;
|
|
5
42
|
align-items: center;
|
|
6
43
|
justify-content: center;
|
|
7
44
|
gap: .35rem;
|
|
8
|
-
min-width:
|
|
9
|
-
|
|
45
|
+
min-width: var(--sia-pagination-size);
|
|
46
|
+
height: var(--sia-pagination-size);
|
|
10
47
|
padding: 0 .65rem;
|
|
11
48
|
border: 1px solid var(--sia-border);
|
|
12
49
|
border-radius: var(--sia-radius);
|
|
13
50
|
color: var(--sia-foreground);
|
|
14
51
|
background: var(--sia-background);
|
|
15
52
|
font: inherit;
|
|
53
|
+
font-size: .875rem;
|
|
16
54
|
font-variant-numeric: tabular-nums;
|
|
55
|
+
line-height: 1;
|
|
17
56
|
cursor: pointer;
|
|
18
57
|
transition:
|
|
19
58
|
background-color var(--sia-duration-fast) var(--sia-easing-standard),
|
|
@@ -21,15 +60,24 @@
|
|
|
21
60
|
color var(--sia-duration-fast) var(--sia-easing-standard),
|
|
22
61
|
transform var(--sia-duration-instant) var(--sia-easing-standard);
|
|
23
62
|
}
|
|
24
|
-
.
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
.sia-
|
|
63
|
+
/* Le survol ne touche pas la page courante. Écrit `button:hover`, il passait
|
|
64
|
+
devant `[aria-current]` par spécificité : fond clair sous un chiffre resté
|
|
65
|
+
blanc, donc illisible. */
|
|
66
|
+
.sia-pagination__step:hover:not(:disabled),
|
|
67
|
+
.sia-pagination__page:hover:not([aria-current="page"]) {
|
|
68
|
+
border-color: var(--sia-border-strong);
|
|
69
|
+
background: var(--sia-surface);
|
|
70
|
+
}
|
|
71
|
+
.sia-pagination__step:active:not(:disabled),
|
|
72
|
+
.sia-pagination__page:active { transform: scale(.94); }
|
|
73
|
+
.sia-pagination__step:disabled { cursor: not-allowed; opacity: .5; }
|
|
74
|
+
.sia-pagination__step:focus-visible,
|
|
75
|
+
.sia-pagination__page:focus-visible {
|
|
28
76
|
outline: 3px solid color-mix(in srgb, var(--sia-primary) 30%, transparent);
|
|
29
77
|
outline-offset: 2px;
|
|
30
78
|
}
|
|
31
79
|
|
|
32
|
-
.sia-
|
|
80
|
+
.sia-pagination__page[aria-current="page"] {
|
|
33
81
|
color: var(--sia-primary-foreground);
|
|
34
82
|
border-color: var(--sia-primary);
|
|
35
83
|
background: var(--sia-primary);
|
|
@@ -38,6 +86,14 @@
|
|
|
38
86
|
le seul changement de couleur se remarque mal. */
|
|
39
87
|
animation: sia-pagination-current var(--sia-duration-base) var(--sia-easing-spring);
|
|
40
88
|
}
|
|
89
|
+
/* La page courante garde sa couleur au survol, un ton plus sombre pour dire
|
|
90
|
+
qu'elle reste un bouton. */
|
|
91
|
+
.sia-pagination__page[aria-current="page"]:hover {
|
|
92
|
+
color: var(--sia-primary-foreground);
|
|
93
|
+
border-color: color-mix(in srgb, var(--sia-primary) 85%, var(--sia-foreground));
|
|
94
|
+
background: color-mix(in srgb, var(--sia-primary) 85%, var(--sia-foreground));
|
|
95
|
+
}
|
|
96
|
+
|
|
41
97
|
@keyframes sia-pagination-current {
|
|
42
98
|
0% { transform: scale(.85); }
|
|
43
99
|
60% { transform: scale(1.06); }
|
|
@@ -45,49 +101,64 @@
|
|
|
45
101
|
}
|
|
46
102
|
|
|
47
103
|
.sia-pagination__step svg { flex: none; font-size: .9em; }
|
|
48
|
-
.sia-pagination__gap {
|
|
104
|
+
.sia-pagination__gap {
|
|
105
|
+
display: inline-flex;
|
|
106
|
+
align-items: center;
|
|
107
|
+
justify-content: center;
|
|
108
|
+
min-width: 1.25rem;
|
|
109
|
+
height: var(--sia-pagination-size);
|
|
110
|
+
color: var(--sia-muted);
|
|
111
|
+
}
|
|
49
112
|
|
|
50
113
|
.sia-pagination--compact .sia-pagination__step { padding: 0 .5rem; }
|
|
51
114
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
}
|
|
115
|
+
/* ── Réglages ─────────────────────────────────────────────────────────────
|
|
116
|
+
Libellé discret à gauche, contrôle à droite, même hauteur que les boutons.
|
|
117
|
+
Taille de page et saut direct sont habillés à l'identique. */
|
|
56
118
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
.sia-pagination__total {
|
|
60
|
-
margin-left: .5rem;
|
|
61
|
-
color: var(--sia-muted);
|
|
62
|
-
font-size: .8rem;
|
|
63
|
-
font-variant-numeric: tabular-nums;
|
|
64
|
-
white-space: nowrap;
|
|
65
|
-
}
|
|
119
|
+
.sia-pagination__total { font-variant-numeric: tabular-nums; }
|
|
66
120
|
|
|
121
|
+
.sia-pagination__size,
|
|
67
122
|
.sia-pagination__jump {
|
|
68
|
-
display: flex;
|
|
123
|
+
display: inline-flex;
|
|
69
124
|
align-items: center;
|
|
70
|
-
gap: .
|
|
71
|
-
margin-left: .5rem;
|
|
72
|
-
color: var(--sia-muted);
|
|
73
|
-
font-size: .8rem;
|
|
74
|
-
white-space: nowrap;
|
|
125
|
+
gap: .5rem;
|
|
75
126
|
}
|
|
76
127
|
.sia-pagination__jump label { cursor: pointer; }
|
|
77
128
|
|
|
129
|
+
.sia-pagination__size .sia-select__trigger,
|
|
78
130
|
.sia-pagination__jump input {
|
|
79
|
-
|
|
80
|
-
min-height: 2.25rem;
|
|
81
|
-
padding: 0 .5rem;
|
|
131
|
+
height: var(--sia-pagination-size);
|
|
82
132
|
border: 1px solid var(--sia-border);
|
|
83
133
|
border-radius: var(--sia-radius);
|
|
84
|
-
background: var(--sia-background);
|
|
85
134
|
color: var(--sia-foreground);
|
|
135
|
+
background: var(--sia-background);
|
|
86
136
|
font: inherit;
|
|
137
|
+
font-size: .875rem;
|
|
87
138
|
font-variant-numeric: tabular-nums;
|
|
139
|
+
transition:
|
|
140
|
+
background-color var(--sia-duration-fast) var(--sia-easing-standard),
|
|
141
|
+
border-color var(--sia-duration-fast) var(--sia-easing-standard);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/* Le Select logé prend l'allure d'un champ de la barre. */
|
|
145
|
+
.sia-pagination__size .sia-select__trigger {
|
|
146
|
+
min-height: 0;
|
|
147
|
+
min-width: 4.25rem;
|
|
148
|
+
padding: 0 .5rem 0 .7rem;
|
|
149
|
+
justify-content: space-between;
|
|
150
|
+
}
|
|
151
|
+
.sia-pagination__size .sia-select__trigger:hover:not(:disabled) {
|
|
152
|
+
border-color: var(--sia-border-strong);
|
|
153
|
+
background: var(--sia-surface);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
.sia-pagination__jump input {
|
|
157
|
+
width: 3.25rem;
|
|
158
|
+
padding: 0 .5rem;
|
|
88
159
|
text-align: center;
|
|
89
|
-
transition: border-color var(--sia-duration-fast) var(--sia-easing-standard);
|
|
90
160
|
}
|
|
161
|
+
.sia-pagination__jump input:hover { border-color: var(--sia-border-strong); }
|
|
91
162
|
.sia-pagination__jump input:focus-visible {
|
|
92
163
|
border-color: var(--sia-primary);
|
|
93
164
|
outline: 3px solid color-mix(in srgb, var(--sia-primary) 30%, transparent);
|
|
@@ -96,8 +167,32 @@
|
|
|
96
167
|
|
|
97
168
|
.sia-pagination__jump-total { font-variant-numeric: tabular-nums; }
|
|
98
169
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
170
|
+
/* ── Largeur disponible ───────────────────────────────────────────────────
|
|
171
|
+
Réglé sur la largeur de la barre, pas de l'écran : une pagination posée
|
|
172
|
+
dans une carte étroite se replie comme sur un téléphone.
|
|
173
|
+
|
|
174
|
+
Sous 52rem, les deux groupes ne tiennent plus côte à côte : les réglages
|
|
175
|
+
prennent leur propre ligne, pleine largeur : le total à gauche, sous la
|
|
176
|
+
navigation, les contrôles groupés à droite. */
|
|
177
|
+
@container sia-pagination (max-width: 52rem) {
|
|
178
|
+
.sia-pagination__settings {
|
|
179
|
+
justify-content: flex-end;
|
|
180
|
+
width: 100%;
|
|
181
|
+
margin-left: 0;
|
|
182
|
+
}
|
|
183
|
+
/* L'information à gauche, les contrôles groupés à droite. */
|
|
184
|
+
.sia-pagination__total { margin-right: auto; }
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/* Sous 34rem, tout se centre : la navigation sur sa ligne, les réglages sur
|
|
188
|
+
les suivantes, sans élément isolé contre un bord. */
|
|
189
|
+
@container sia-pagination (max-width: 34rem) {
|
|
190
|
+
.sia-pagination__pages { justify-content: center; width: 100%; }
|
|
191
|
+
.sia-pagination__settings { justify-content: center; column-gap: 1.5rem; }
|
|
192
|
+
.sia-pagination__total { margin-right: 0; }
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/* Sous 26rem, les libellés des flèches tombent ; les flèches restent. */
|
|
196
|
+
@container sia-pagination (max-width: 26rem) {
|
|
197
|
+
.sia-pagination__step span { display: none; }
|
|
103
198
|
}
|
|
@@ -2,6 +2,7 @@ import { forwardRef, useMemo, useState } from "react";
|
|
|
2
2
|
import { cn } from "@sia-ui/utils";
|
|
3
3
|
import { useSiaLocale } from "@sia-ui/headless";
|
|
4
4
|
import { Input, type InputProps } from "../Input";
|
|
5
|
+
import { Select, type SelectOption } from "../Select";
|
|
5
6
|
import "./styles.css";
|
|
6
7
|
|
|
7
8
|
export interface PhoneCountry {
|
|
@@ -128,6 +129,18 @@ export const PhoneInput = forwardRef<HTMLInputElement, PhoneInputProps>(
|
|
|
128
129
|
? value.slice(country.dial.length)
|
|
129
130
|
: digitsOnly(value);
|
|
130
131
|
|
|
132
|
+
// La recherche porte sur le nom, l'indicatif et le code ISO : « cam »,
|
|
133
|
+
// « 237 » et « cm » trouvent tous le Cameroun.
|
|
134
|
+
const options = useMemo<SelectOption[]>(
|
|
135
|
+
() =>
|
|
136
|
+
countries.map((entry) => ({
|
|
137
|
+
value: entry.code,
|
|
138
|
+
label: `${entry.flag} ${entry.label} (${entry.dial})`,
|
|
139
|
+
keywords: [entry.code],
|
|
140
|
+
})),
|
|
141
|
+
[countries],
|
|
142
|
+
);
|
|
143
|
+
|
|
131
144
|
const commit = (next: string) => {
|
|
132
145
|
if (!controlled) setInternal(next);
|
|
133
146
|
onValueChange?.(next);
|
|
@@ -150,36 +163,40 @@ export const PhoneInput = forwardRef<HTMLInputElement, PhoneInputProps>(
|
|
|
150
163
|
commit(next ? `${country.dial}${next}` : "");
|
|
151
164
|
}}
|
|
152
165
|
left={
|
|
153
|
-
|
|
154
|
-
<span className="sia-
|
|
155
|
-
|
|
166
|
+
lockCountry ? (
|
|
167
|
+
<span className="sia-phone__country">
|
|
168
|
+
<span className="sia-phone__flag" aria-hidden="true">
|
|
169
|
+
{country.flag}
|
|
170
|
+
</span>
|
|
171
|
+
<span className="sia-phone__dial">{country.dial}</span>
|
|
156
172
|
</span>
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
173
|
+
) : (
|
|
174
|
+
<Select
|
|
175
|
+
variant="embedded"
|
|
176
|
+
searchable
|
|
177
|
+
className="sia-phone__country"
|
|
178
|
+
aria-label={locale.countryCode}
|
|
179
|
+
options={options}
|
|
180
|
+
value={country.code}
|
|
181
|
+
disabled={disabled ?? false}
|
|
182
|
+
renderValue={() => (
|
|
183
|
+
<span className="sia-phone__value">
|
|
184
|
+
<span className="sia-phone__flag" aria-hidden="true">
|
|
185
|
+
{country.flag}
|
|
186
|
+
</span>
|
|
187
|
+
<span className="sia-phone__dial">{country.dial}</span>
|
|
188
|
+
</span>
|
|
189
|
+
)}
|
|
190
|
+
onValueChange={(code) => {
|
|
191
|
+
const next = countries.find((entry) => entry.code === code);
|
|
192
|
+
if (!next) return;
|
|
193
|
+
setPicked(next.code);
|
|
194
|
+
// Le numéro national suit le changement d'indicatif : il est
|
|
195
|
+
// rare mais courant de corriger le pays après coup.
|
|
196
|
+
commit(national ? `${next.dial}${national}` : "");
|
|
197
|
+
}}
|
|
198
|
+
/>
|
|
199
|
+
)
|
|
183
200
|
}
|
|
184
201
|
/>
|
|
185
202
|
);
|
|
@@ -1,35 +1,21 @@
|
|
|
1
1
|
.sia-phone__field { font-variant-numeric: tabular-nums; letter-spacing: .02em; }
|
|
2
2
|
|
|
3
|
+
/* L'indicatif, séparé du numéro par un filet. Choisi dans le `Select` du
|
|
4
|
+
système, en variante logée : même panneau, même recherche, même clavier
|
|
5
|
+
que partout ailleurs — là où le `<select>` natif n'offrait ni recherche ni
|
|
6
|
+
le thème. */
|
|
3
7
|
.sia-phone__country {
|
|
4
|
-
position: relative;
|
|
5
8
|
display: inline-flex;
|
|
6
9
|
align-items: center;
|
|
7
10
|
gap: .3rem;
|
|
8
|
-
padding-right: .
|
|
11
|
+
padding-right: .35rem;
|
|
9
12
|
border-right: 1px solid var(--sia-border);
|
|
10
13
|
color: var(--sia-foreground);
|
|
11
14
|
font-size: .85rem;
|
|
12
15
|
white-space: nowrap;
|
|
13
|
-
transition: background-color var(--sia-duration-fast) var(--sia-easing-standard);
|
|
14
|
-
border-radius: var(--sia-radius) 0 0 var(--sia-radius);
|
|
15
16
|
}
|
|
16
|
-
.sia-phone__country
|
|
17
|
+
.sia-phone__country .sia-select__trigger { align-self: stretch; }
|
|
17
18
|
|
|
19
|
+
.sia-phone__value { display: inline-flex; align-items: center; gap: .3rem; }
|
|
18
20
|
.sia-phone__flag { font-size: 1.05rem; line-height: 1; }
|
|
19
21
|
.sia-phone__dial { color: var(--sia-muted); font-variant-numeric: tabular-nums; }
|
|
20
|
-
|
|
21
|
-
/* Le `select` natif recouvre l'affichage : il ouvre la liste du système, qui
|
|
22
|
-
est la seule à fonctionner correctement sur mobile — un menu réimplémenté
|
|
23
|
-
se retrouve sous le clavier virtuel. */
|
|
24
|
-
.sia-phone__select {
|
|
25
|
-
position: absolute;
|
|
26
|
-
inset: 0;
|
|
27
|
-
width: 100%;
|
|
28
|
-
height: 100%;
|
|
29
|
-
border: 0;
|
|
30
|
-
opacity: 0;
|
|
31
|
-
cursor: pointer;
|
|
32
|
-
appearance: none;
|
|
33
|
-
}
|
|
34
|
-
.sia-phone__select:disabled { cursor: not-allowed; }
|
|
35
|
-
.sia-phone__select:focus-visible + * { outline: 2px solid var(--sia-primary); }
|
|
@@ -10,7 +10,7 @@ import {
|
|
|
10
10
|
type KeyboardEvent,
|
|
11
11
|
type ReactNode,
|
|
12
12
|
} from "react";
|
|
13
|
-
import { cn } from "@sia-ui/utils";
|
|
13
|
+
import { cn, looseIncludes } from "@sia-ui/utils";
|
|
14
14
|
import { Overlay, type OverlayPlacement } from "../Overlay";
|
|
15
15
|
import { Spinner } from "../Spinner";
|
|
16
16
|
import { ChevronDownIcon, SearchIcon, CheckIcon, XIcon } from "../Icons";
|
|
@@ -46,6 +46,19 @@ export interface SelectProps {
|
|
|
46
46
|
invalid?: boolean;
|
|
47
47
|
required?: boolean;
|
|
48
48
|
placement?: OverlayPlacement;
|
|
49
|
+
/**
|
|
50
|
+
* Ce que montre le déclencheur. Par défaut, le libellé de l'option.
|
|
51
|
+
*
|
|
52
|
+
* La liste garde ses libellés complets — « 🇨🇲 Cameroun (+237) » — pendant
|
|
53
|
+
* que le champ fermé n'affiche que ce qui tient : « 🇨🇲 +237 ».
|
|
54
|
+
*/
|
|
55
|
+
renderValue?: (option: SelectOption | undefined) => ReactNode;
|
|
56
|
+
/**
|
|
57
|
+
* `embedded` retire bordure, fond et largeur pleine du déclencheur : le
|
|
58
|
+
* `Select` se loge alors dans un autre contrôle — l'indicatif d'un
|
|
59
|
+
* `PhoneInput`, la taille de page d'une `Pagination`.
|
|
60
|
+
*/
|
|
61
|
+
variant?: "default" | "embedded";
|
|
49
62
|
className?: string;
|
|
50
63
|
"aria-label"?: string;
|
|
51
64
|
"aria-describedby"?: string;
|
|
@@ -71,8 +84,7 @@ function optionsFromChildren(children: ReactNode): SelectOption[] {
|
|
|
71
84
|
function optionText(option: SelectOption) {
|
|
72
85
|
return [option.label, option.description, ...(option.keywords ?? [])]
|
|
73
86
|
.map((value) => (typeof value === "string" || typeof value === "number" ? String(value) : ""))
|
|
74
|
-
.join(" ")
|
|
75
|
-
.toLocaleLowerCase();
|
|
87
|
+
.join(" ");
|
|
76
88
|
}
|
|
77
89
|
|
|
78
90
|
/**
|
|
@@ -108,6 +120,8 @@ export const Select = forwardRef<HTMLButtonElement, SelectProps>(function Select
|
|
|
108
120
|
invalid = false,
|
|
109
121
|
required = false,
|
|
110
122
|
placement = "bottom-start",
|
|
123
|
+
renderValue,
|
|
124
|
+
variant = "default",
|
|
111
125
|
className,
|
|
112
126
|
"aria-invalid": ariaInvalid,
|
|
113
127
|
"aria-required": ariaRequired,
|
|
@@ -136,9 +150,10 @@ export const Select = forwardRef<HTMLButtonElement, SelectProps>(function Select
|
|
|
136
150
|
const sourceOptions = fetcher ? remoteOptions : staticOptions;
|
|
137
151
|
const visibleOptions = useMemo(() => {
|
|
138
152
|
if (fetcher) return sourceOptions;
|
|
139
|
-
|
|
153
|
+
// Accents et casse ignorés : « sen » trouve « Sénégal ».
|
|
154
|
+
const needle = query.trim();
|
|
140
155
|
return needle
|
|
141
|
-
? sourceOptions.filter((option) => optionText(option)
|
|
156
|
+
? sourceOptions.filter((option) => looseIncludes(optionText(option), needle))
|
|
142
157
|
: sourceOptions;
|
|
143
158
|
}, [fetcher, query, sourceOptions]);
|
|
144
159
|
const selectedOption = [...staticOptions, ...remoteOptions].find(
|
|
@@ -168,7 +183,11 @@ export const Select = forwardRef<HTMLButtonElement, SelectProps>(function Select
|
|
|
168
183
|
|
|
169
184
|
useEffect(() => {
|
|
170
185
|
if (!open) return;
|
|
171
|
-
|
|
186
|
+
// La valeur en vigueur si elle est dans la liste, la première option
|
|
187
|
+
// sinon : après une recherche qui l'écarte, Entrée doit choisir le premier
|
|
188
|
+
// résultat plutôt que ne rien faire.
|
|
189
|
+
const selected = visibleOptions.findIndex((option) => option.value === currentValue);
|
|
190
|
+
setActiveIndex(selected >= 0 ? selected : visibleOptions.findIndex((option) => !option.disabled));
|
|
172
191
|
if (searchable || fetcher) window.setTimeout(() => searchRef.current?.focus(), 0);
|
|
173
192
|
}, [currentValue, fetcher, open, searchable, visibleOptions]);
|
|
174
193
|
|
|
@@ -234,7 +253,14 @@ export const Select = forwardRef<HTMLButtonElement, SelectProps>(function Select
|
|
|
234
253
|
const activeOption = activeIndex >= 0 ? visibleOptions[activeIndex] : undefined;
|
|
235
254
|
|
|
236
255
|
return (
|
|
237
|
-
<div
|
|
256
|
+
<div
|
|
257
|
+
className={cn(
|
|
258
|
+
"sia-select",
|
|
259
|
+
variant === "embedded" && "sia-select--embedded",
|
|
260
|
+
disabled && "sia-select--disabled",
|
|
261
|
+
className,
|
|
262
|
+
)}
|
|
263
|
+
>
|
|
238
264
|
{name && <input type="hidden" name={name} value={currentValue} />}
|
|
239
265
|
<button
|
|
240
266
|
ref={setTriggerRef}
|
|
@@ -253,7 +279,7 @@ export const Select = forwardRef<HTMLButtonElement, SelectProps>(function Select
|
|
|
253
279
|
onKeyDown={handleKeyDown}
|
|
254
280
|
{...ariaProps}
|
|
255
281
|
>
|
|
256
|
-
<span className={cn("sia-select__value", !selectedOption && "sia-select__placeholder")}>{selectedOption?.label ?? placeholder}</span>
|
|
282
|
+
<span className={cn("sia-select__value", !selectedOption && "sia-select__placeholder")}>{renderValue ? renderValue(selectedOption) : (selectedOption?.label ?? placeholder)}</span>
|
|
257
283
|
<span className="sia-select__actions">
|
|
258
284
|
{clearable && currentValue && !disabled && (
|
|
259
285
|
<span
|
|
@@ -56,3 +56,21 @@
|
|
|
56
56
|
transform-origin: top center;
|
|
57
57
|
box-shadow: var(--sia-elevation-overlay);
|
|
58
58
|
}
|
|
59
|
+
|
|
60
|
+
/* ── Logé dans un autre contrôle ───────────────────────────────────────────
|
|
61
|
+
Le déclencheur prend la largeur de son contenu et laisse bordure et fond au
|
|
62
|
+
contrôle hôte. Le panneau, lui, reste celui du Select : `matchAnchorWidth`
|
|
63
|
+
n'impose qu'une largeur minimale, il s'élargit à ses options. */
|
|
64
|
+
.sia-select--embedded { width: auto; }
|
|
65
|
+
.sia-select--embedded .sia-select__trigger {
|
|
66
|
+
width: auto;
|
|
67
|
+
min-height: 0;
|
|
68
|
+
gap: .35rem;
|
|
69
|
+
padding: 0 .35rem;
|
|
70
|
+
border-color: transparent;
|
|
71
|
+
background: transparent;
|
|
72
|
+
}
|
|
73
|
+
.sia-select--embedded .sia-select__trigger:hover:not(:disabled) {
|
|
74
|
+
border-color: transparent;
|
|
75
|
+
background: var(--sia-surface);
|
|
76
|
+
}
|
|
@@ -358,7 +358,8 @@ function Ligne({
|
|
|
358
358
|
aDesEnfants: boolean;
|
|
359
359
|
}) {
|
|
360
360
|
const porteUnePastille =
|
|
361
|
-
collapsed &&
|
|
361
|
+
collapsed &&
|
|
362
|
+
navSome(item, (e) => e.badge !== undefined && e.badge !== null);
|
|
362
363
|
|
|
363
364
|
return (
|
|
364
365
|
<>
|
|
@@ -370,7 +371,16 @@ function Ligne({
|
|
|
370
371
|
<span className="sia-sidebar__label">{item.label}</span>
|
|
371
372
|
|
|
372
373
|
{item.badge !== undefined && !collapsed && (
|
|
373
|
-
<span
|
|
374
|
+
<span
|
|
375
|
+
className="sia-sidebar__badge"
|
|
376
|
+
data-sia-badge={
|
|
377
|
+
typeof item.badge === "string" || typeof item.badge === "number"
|
|
378
|
+
? String(item.badge)
|
|
379
|
+
: undefined
|
|
380
|
+
}
|
|
381
|
+
>
|
|
382
|
+
{item.badge}
|
|
383
|
+
</span>
|
|
374
384
|
)}
|
|
375
385
|
|
|
376
386
|
{/* Le compte exact ne tient pas sur un rail : il devient un point, et se
|
|
@@ -571,7 +581,17 @@ function NoeudRail(props: NoeudProps) {
|
|
|
571
581
|
<div className="sia-sidebar-flyout__title">
|
|
572
582
|
{item.label}
|
|
573
583
|
{item.badge !== undefined && (
|
|
574
|
-
<span
|
|
584
|
+
<span
|
|
585
|
+
className="sia-sidebar__badge"
|
|
586
|
+
data-sia-badge={
|
|
587
|
+
typeof item.badge === "string" ||
|
|
588
|
+
typeof item.badge === "number"
|
|
589
|
+
? String(item.badge)
|
|
590
|
+
: undefined
|
|
591
|
+
}
|
|
592
|
+
>
|
|
593
|
+
{item.badge}
|
|
594
|
+
</span>
|
|
575
595
|
)}
|
|
576
596
|
</div>
|
|
577
597
|
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
font-size: .875rem;
|
|
9
9
|
}
|
|
10
10
|
|
|
11
|
-
.sia-sidebar__body { flex:
|
|
11
|
+
.sia-sidebar__body { flex:1; min-height:0; padding-inline-end:.375rem; overflow-y:auto; display:grid; gap:.125rem; align-content:start; scrollbar-gutter:stable; }
|
|
12
12
|
/* Rognés plutôt que débordants : en mode rail, un en-tête ou un pied qui
|
|
13
13
|
n'a pas été adapté déborderait sur le contenu. Le `data-collapsed` posé
|
|
14
14
|
par `AppShell` permet de l'adapter; ceci garantit que ne pas l'adapter
|
|
@@ -130,17 +130,7 @@
|
|
|
130
130
|
padding-block: .375rem;
|
|
131
131
|
font-size: .825rem;
|
|
132
132
|
}
|
|
133
|
-
/* Le filet qui relie les sous-entrées à leur parent. */
|
|
134
133
|
.sia-sidebar__sub { position: relative; }
|
|
135
|
-
.sia-sidebar__sub::before {
|
|
136
|
-
content: "";
|
|
137
|
-
position: absolute;
|
|
138
|
-
top: .375rem;
|
|
139
|
-
bottom: .375rem;
|
|
140
|
-
left: 1.25rem;
|
|
141
|
-
width: 1px;
|
|
142
|
-
background: var(--sia-border);
|
|
143
|
-
}
|
|
144
134
|
|
|
145
135
|
/* ── Mode réduit ───────────────────────────────────────────────────────── */
|
|
146
136
|
|
|
@@ -197,8 +187,6 @@
|
|
|
197
187
|
font-weight: 650;
|
|
198
188
|
}
|
|
199
189
|
.sia-sidebar-flyout__body { display: grid; gap: .125rem; }
|
|
200
|
-
/* Dans le volet, le filet vertical du parent n'a plus de parent à relier. */
|
|
201
|
-
.sia-sidebar-flyout .sia-sidebar__sub::before { display: none; }
|
|
202
190
|
.sia-sidebar-flyout .sia-sidebar__item--child { padding-left: calc(.625rem + var(--sia-sidebar-depth, 1) * .6rem); }
|
|
203
191
|
|
|
204
192
|
/* ── Variantes ─────────────────────────────────────────────────────────── */
|
|
@@ -13,4 +13,4 @@ export function formatDuration(milliseconds: number, showMilliseconds = false) {
|
|
|
13
13
|
* dixième de seconde redessine quarante fois par seconde pour un chiffre
|
|
14
14
|
* que personne ne lit.
|
|
15
15
|
*/
|
|
16
|
-
export function Timer({ duration = 60000, initialValue = 0, direction = "down", running = true, interval = 250, onChange, onFinish, format = formatDuration, className }: TimerProps) { const initial = direction === "down" ? duration : initialValue; const [value, setValue] = useState(initial); const finishCalled = useRef(false); useEffect(() => { setValue(direction === "down" ? duration : initialValue); finishCalled.current = false; }, [direction, duration, initialValue]); useEffect(() => { if (!running) return; const startedAt = Date.now(); const base = value; const update = () => { const elapsed = Date.now() - startedAt; const next = direction === "down" ? Math.max(0, base - elapsed) : base + elapsed; setValue(next); onChange?.(next); if (direction === "down" && next === 0 && !finishCalled.current) { finishCalled.current = true; onFinish?.(); } }; update(); const id = window.setInterval(update, interval); return () => window.clearInterval(id); }, [running, direction, interval]); return <time className={cn("sia-timer", className)} aria-live="polite">{format(value)}</time>; }
|
|
16
|
+
export function Timer({ duration = 60000, initialValue = 0, direction = "down", running = true, interval = 250, onChange, onFinish, format = formatDuration, className }: TimerProps) { const initial = direction === "down" ? duration : initialValue; const [value, setValue] = useState(initial); const finishCalled = useRef(false); /* Lus au démarrage et à chaque tic, sans relancer l'intervalle : la valeur de reprise et des rappels souvent écrits en ligne. */ const latest = useRef({ value, onChange, onFinish }); latest.current = { value, onChange, onFinish }; useEffect(() => { setValue(direction === "down" ? duration : initialValue); finishCalled.current = false; }, [direction, duration, initialValue]); useEffect(() => { if (!running) return; const startedAt = Date.now(); const base = latest.current.value; const update = () => { const elapsed = Date.now() - startedAt; const next = direction === "down" ? Math.max(0, base - elapsed) : base + elapsed; setValue(next); latest.current.onChange?.(next); if (direction === "down" && next === 0 && !finishCalled.current) { finishCalled.current = true; latest.current.onFinish?.(); } }; update(); const id = window.setInterval(update, interval); return () => window.clearInterval(id); }, [running, direction, interval]); return <time className={cn("sia-timer", className)} aria-live="polite">{format(value)}</time>; }
|
|
@@ -63,9 +63,10 @@
|
|
|
63
63
|
--sia-toggle-color: var(--sia-primary);
|
|
64
64
|
display: inline-flex;
|
|
65
65
|
padding: .2rem;
|
|
66
|
-
border: 1px solid var(--sia-border);
|
|
66
|
+
border: 1px solid color-mix(in srgb, var(--sia-muted) 28%, var(--sia-border));
|
|
67
67
|
border-radius: calc(var(--sia-radius) + 2px);
|
|
68
|
-
background: var(--sia-
|
|
68
|
+
background: color-mix(in srgb, var(--sia-muted) 12%, var(--sia-background));
|
|
69
|
+
box-shadow: inset 0 1px 2px rgb(15 23 42 / .05);
|
|
69
70
|
}
|
|
70
71
|
|
|
71
72
|
.sia-toggle-group__item {
|
|
@@ -22,11 +22,21 @@ import {
|
|
|
22
22
|
type UseFormProps,
|
|
23
23
|
type UseFormReturn,
|
|
24
24
|
} from "react-hook-form";
|
|
25
|
-
import
|
|
25
|
+
import {
|
|
26
|
+
hasFormErrors,
|
|
27
|
+
type FieldBindOptions,
|
|
28
|
+
type FieldBinding,
|
|
29
|
+
type FormAdapter,
|
|
30
|
+
type FormErrors,
|
|
31
|
+
type FormState,
|
|
32
|
+
type FormSubmitResult,
|
|
33
|
+
} from "@sia-ui/headless";
|
|
26
34
|
|
|
27
35
|
export interface RhfAdapterOptions<TValues extends FieldValues>
|
|
28
36
|
extends UseFormProps<TValues> {
|
|
29
|
-
onSubmit?: (
|
|
37
|
+
onSubmit?: (
|
|
38
|
+
values: TValues,
|
|
39
|
+
) => FormSubmitResult<TValues> | Promise<FormSubmitResult<TValues>>;
|
|
30
40
|
}
|
|
31
41
|
|
|
32
42
|
export interface RhfAdapter<TValues extends FieldValues>
|
|
@@ -94,6 +104,12 @@ export function useRhfFormAdapter<TValues extends FieldValues>(
|
|
|
94
104
|
[formOptions.mode, formState.errors, setValue, trigger, watch],
|
|
95
105
|
);
|
|
96
106
|
|
|
107
|
+
const poserErreurs = (next: FormErrors<TValues>) => {
|
|
108
|
+
for (const [key, message] of Object.entries(next)) {
|
|
109
|
+
if (message) setError(key as Path<TValues>, { message: String(message) });
|
|
110
|
+
}
|
|
111
|
+
};
|
|
112
|
+
|
|
97
113
|
const state: FormState<TValues> = {
|
|
98
114
|
values: values as TValues,
|
|
99
115
|
errors,
|
|
@@ -123,17 +139,14 @@ export function useRhfFormAdapter<TValues extends FieldValues>(
|
|
|
123
139
|
if (message) setError(name as Path<TValues>, { message });
|
|
124
140
|
else clearErrors(name as Path<TValues>);
|
|
125
141
|
},
|
|
126
|
-
// Ce que rend `HttpError.
|
|
127
|
-
// les erreurs du serveur se posent sous les champs, pas dans un bandeau.
|
|
128
|
-
setErrors:
|
|
129
|
-
for (const [key, message] of Object.entries(next)) {
|
|
130
|
-
if (message) setError(key as Path<TValues>, { message: String(message) });
|
|
131
|
-
}
|
|
132
|
-
},
|
|
142
|
+
// Ce que rend `HttpError.toFormErrors()` de `@sia-ui/api` entre directement
|
|
143
|
+
// ici : les erreurs du serveur se posent sous les champs, pas dans un bandeau.
|
|
144
|
+
setErrors: poserErreurs,
|
|
133
145
|
reset: (next) => reset(next as TValues | undefined),
|
|
134
146
|
submit: async () => {
|
|
135
147
|
await handleSubmit(async (formValues) => {
|
|
136
|
-
await onSubmit?.(formValues);
|
|
148
|
+
const result = await onSubmit?.(formValues);
|
|
149
|
+
if (hasFormErrors(result)) poserErreurs(result);
|
|
137
150
|
})();
|
|
138
151
|
},
|
|
139
152
|
validate: () => trigger(),
|