@sia-ui/cli 0.4.0 → 0.6.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 (37) hide show
  1. package/CHANGELOG.md +58 -0
  2. package/README.md +58 -0
  3. package/dist/index.js +30 -11
  4. package/dist/lifecycle.mjs +189 -4
  5. package/dist/lock.mjs +16 -2
  6. package/dist/registry/package/components/ActivityLog/index.tsx +241 -0
  7. package/dist/registry/package/components/ActivityLog/styles.css +31 -0
  8. package/dist/registry/package/components/Badge/index.tsx +58 -0
  9. package/dist/registry/package/components/Calendar/index.tsx +4 -4
  10. package/dist/registry/package/components/CrudPage/dialogs.tsx +35 -22
  11. package/dist/registry/package/components/CrudPage/index.tsx +58 -26
  12. package/dist/registry/package/components/CrudPage/styles.css +7 -6
  13. package/dist/registry/package/components/DataTable/index.tsx +5 -1
  14. package/dist/registry/package/components/DataTable/query.ts +23 -3
  15. package/dist/registry/package/components/DataTable/row-actions.tsx +1 -1
  16. package/dist/registry/package/components/DataTable/types.ts +8 -0
  17. package/dist/registry/package/components/Drawer/index.tsx +9 -3
  18. package/dist/registry/package/components/Form/index.tsx +69 -3
  19. package/dist/registry/package/components/Icons/index.tsx +88 -4
  20. package/dist/registry/package/components/PageHeader/index.tsx +28 -4
  21. package/dist/registry/package/components/PageHeader/styles.css +14 -2
  22. package/dist/registry/package/components/Pagination/index.tsx +168 -61
  23. package/dist/registry/package/components/Pagination/styles.css +133 -38
  24. package/dist/registry/package/components/PhoneInput/index.tsx +46 -29
  25. package/dist/registry/package/components/PhoneInput/styles.css +7 -21
  26. package/dist/registry/package/components/Resource/index.tsx +69 -19
  27. package/dist/registry/package/components/SecretFields/index.tsx +193 -0
  28. package/dist/registry/package/components/SecretFields/styles.css +84 -0
  29. package/dist/registry/package/components/Select/index.tsx +34 -8
  30. package/dist/registry/package/components/Select/styles.css +18 -0
  31. package/dist/registry/package/components/Timer/index.tsx +1 -1
  32. package/dist/registry/package/lib/form-adapter-rhf.tsx +23 -10
  33. package/dist/registry/registry.json +71 -16
  34. package/dist/registry/styles/sia-ui.css +7 -1
  35. package/dist/source.mjs +10 -0
  36. package/dist/styles.mjs +111 -0
  37. package/package.json +5 -5
@@ -1,7 +1,15 @@
1
- import { useEffect, useId, useState, type FormEvent } from "react";
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
- if (totalPages <= 1) return null;
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
- const items = paginationItems(current, totalPages, siblingCount);
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
- <button
88
- type="button"
89
- className="sia-pagination__step"
90
- disabled={current === 1}
91
- aria-label={previousLabel}
92
- onClick={() => onPageChange(current - 1)}
93
- >
94
- <ChevronLeftIcon />
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
- key={item}
103
- className="sia-pagination__page"
104
- aria-current={item === current ? "page" : undefined}
105
- onClick={() => onPageChange(item)}
162
+ className="sia-pagination__step"
163
+ disabled={current === 1}
164
+ aria-label={previousLabel}
165
+ onClick={() => onPageChange(current - 1)}
106
166
  >
107
- {item}
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
- <button
117
- type="button"
118
- className="sia-pagination__step"
119
- disabled={current === totalPages}
120
- aria-label={nextLabel}
121
- onClick={() => onPageChange(current + 1)}
122
- >
123
- {!compact && <span>{nextLabel}</span>}
124
- <ChevronRightIcon />
125
- </button>
126
-
127
- {showTotal && (
128
- <span className="sia-pagination__total">
129
- {locale.page} {current} / {totalPages}
130
- </span>
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
- <form className="sia-pagination__jump" onSubmit={jump}>
135
- <label htmlFor={jumpId}>{locale.goToPage}</label>
136
- <input
137
- id={jumpId}
138
- // `text` plutôt que `number` : le champ numérique natif accepte
139
- // « 1e5 » et « -- », et vide sa valeur sans prévenir quand elle
140
- // ne lui plaît pas.
141
- type="text"
142
- inputMode="numeric"
143
- value={draft}
144
- aria-label={locale.goToPage}
145
- onChange={(event) => setDraft(event.target.value.replace(/\D/g, ""))}
146
- onBlur={jump}
147
- />
148
- <span className="sia-pagination__jump-total" aria-hidden="true">
149
- / {totalPages}
150
- </span>
151
- </form>
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
  );
@@ -1,19 +1,58 @@
1
- .sia-pagination { display: flex; align-items: center; gap: .3rem; flex-wrap: wrap; }
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-pagination button {
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: 2.25rem;
9
- min-height: 2.25rem;
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
- .sia-pagination button:hover:not(:disabled) { background: var(--sia-surface); }
25
- .sia-pagination button:active:not(:disabled) { transform: scale(.94); }
26
- .sia-pagination button:disabled { cursor: not-allowed; opacity: .5; }
27
- .sia-pagination button:focus-visible {
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-pagination button[aria-current="page"] {
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 { padding: 0 .25rem; color: var(--sia-muted); }
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
- @media (max-width: 480px) {
53
- /* Sur un écran étroit, seuls les pas et la page courante tiennent. */
54
- .sia-pagination__step span { display: none; }
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
- /* ── Saut direct ───────────────────────────────────────────────────────── */
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: .4rem;
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
- width: 3.25rem;
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
- @media (max-width: 480px) {
100
- /* Le saut direct vaut mieux que la liste des numéros sur un écran étroit :
101
- il passe à la ligne entier plutôt que d'être tronqué. */
102
- .sia-pagination__jump { width: 100%; margin-left: 0; justify-content: flex-end; }
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
- <span className="sia-phone__country">
154
- <span className="sia-phone__flag" aria-hidden="true">
155
- {country.flag}
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
- <span className="sia-phone__dial">{country.dial}</span>
158
- {!lockCountry && (
159
- <select
160
- className="sia-phone__select"
161
- aria-label={locale.countryCode}
162
- value={country.code}
163
- disabled={disabled}
164
- onChange={(event) => {
165
- const next = countries.find(
166
- (entry) => entry.code === event.target.value,
167
- );
168
- if (!next) return;
169
- setPicked(next.code);
170
- // Le numéro national suit le changement d'indicatif : il
171
- // est rare mais courant de corriger le pays après coup.
172
- commit(national ? `${next.dial}${national}` : "");
173
- }}
174
- >
175
- {countries.map((entry) => (
176
- <option key={entry.code} value={entry.code}>
177
- {entry.flag} {entry.label} ({entry.dial})
178
- </option>
179
- ))}
180
- </select>
181
- )}
182
- </span>
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: .6rem;
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:hover { background: var(--sia-surface); }
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); }