@sia-ui/cli 0.6.0 → 0.7.1

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 CHANGED
@@ -1,5 +1,17 @@
1
1
  # @sia-ui/cli
2
2
 
3
+ ## 0.7.1
4
+
5
+ ### Patch Changes
6
+
7
+ - Aligne les packages publics de SIA UI sur la version 0.7.1 afin que les huit packages restent sur une version commune.
8
+
9
+ ## 0.7.0
10
+
11
+ ### Minor Changes
12
+
13
+ - Aligne les packages publics de SIA UI sur la version 0.7.0 afin que les huit packages restent sur une version commune.
14
+
3
15
  ## 0.6.0
4
16
 
5
17
  ### Minor Changes
package/README.md CHANGED
@@ -68,6 +68,18 @@ Le contenu ci-dessous reprend intégralement `CHANGELOG.md` pour rester visible
68
68
  <!-- sia:changelog:start -->
69
69
  # @sia-ui/cli
70
70
 
71
+ ## 0.7.1
72
+
73
+ ### Patch Changes
74
+
75
+ - Aligne les packages publics de SIA UI sur la version 0.7.1 afin que les huit packages restent sur une version commune.
76
+
77
+ ## 0.7.0
78
+
79
+ ### Minor Changes
80
+
81
+ - Aligne les packages publics de SIA UI sur la version 0.7.0 afin que les huit packages restent sur une version commune.
82
+
71
83
  ## 0.6.0
72
84
 
73
85
  ### Minor Changes
@@ -19,8 +19,10 @@ import { renduCellule, valeurDe } from "./values";
19
19
  import "./styles.css";
20
20
 
21
21
  export {
22
+ createTanStackRouterStorage,
22
23
  createUrlTableStorage,
23
24
  useDataTableQuery,
25
+ type TanStackRouterLike,
24
26
  type UseDataTableQueryOptions,
25
27
  } from "./query";
26
28
 
@@ -33,6 +33,74 @@ export function createUrlTableStorage(
33
33
  };
34
34
  }
35
35
 
36
+ /**
37
+ * Ce que l'adaptateur demande d'un routeur : la forme du `router` de
38
+ * TanStack Router, sans en dépendre. Méthodes écrites en syntaxe de méthode :
39
+ * leurs paramètres se comparent alors de façon bivariante, et le routeur typé
40
+ * de l'application s'y passe sans transtypage.
41
+ */
42
+ export interface TanStackRouterLike {
43
+ state: { location: { search: object } };
44
+ navigate(options: {
45
+ to: string;
46
+ search: Record<string, unknown>;
47
+ replace: boolean;
48
+ }): unknown;
49
+ subscribe(event: "onResolved", listener: () => void): () => void;
50
+ }
51
+
52
+ /** `"2"` → `2`, mais jamais `"007"`, `"1e5"` ni `""` : la conversion est sans perte. */
53
+ function nombreCanonique(valeur: string): string | number {
54
+ const nombre = Number(valeur);
55
+ return valeur !== "" && Number.isFinite(nombre) && String(nombre) === valeur
56
+ ? nombre
57
+ : valeur;
58
+ }
59
+
60
+ /**
61
+ * L'état du tableau dans les paramètres de TanStack Router.
62
+ *
63
+ * Deux pièges de la recette qu'on écrivait à la main :
64
+ *
65
+ * - TanStack Router sérialise les paramètres en JSON. Une chaîne qui a l'air
66
+ * d'un nombre y est mise entre guillemets — `page=2` devenait
67
+ * `page=%222%22`. L'adaptateur passe donc de vrais nombres ; les autres
68
+ * valeurs restent des chaînes, que le routeur relit à l'identique.
69
+ * - La lecture part de l'objet déjà décodé par le routeur, jamais de la chaîne
70
+ * brute, qui porte ces guillemets.
71
+ *
72
+ * Les paramètres étrangers au tableau sont gardés par `useTableQuery`, qui
73
+ * n'écrit que ses propres clés.
74
+ */
75
+ export function createTanStackRouterStorage(
76
+ router: TanStackRouterLike,
77
+ mode: "replace" | "push" = "replace",
78
+ ): TableQueryStorage {
79
+ return {
80
+ read: () => {
81
+ const params = new URLSearchParams();
82
+ for (const [cle, valeur] of Object.entries(router.state.location.search)) {
83
+ if (valeur === undefined || valeur === null) continue;
84
+ for (const v of Array.isArray(valeur) ? valeur : [valeur]) {
85
+ params.append(cle, typeof v === "object" ? JSON.stringify(v) : String(v));
86
+ }
87
+ }
88
+ return params.toString();
89
+ },
90
+ write: (value) => {
91
+ const search: Record<string, unknown> = {};
92
+ for (const [cle, brut] of new URLSearchParams(value)) {
93
+ const valeur = nombreCanonique(brut);
94
+ const deja = search[cle];
95
+ search[cle] =
96
+ deja === undefined ? valeur : [...(Array.isArray(deja) ? deja : [deja]), valeur];
97
+ }
98
+ router.navigate({ to: ".", search, replace: mode === "replace" });
99
+ },
100
+ subscribe: (listener) => router.subscribe("onResolved", listener),
101
+ };
102
+ }
103
+
36
104
  export interface UseDataTableQueryOptions {
37
105
  defaultState?: Partial<TableQueryState>;
38
106
  /** `false` garde l'état en mémoire — utile dans une modale. */
@@ -50,6 +118,11 @@ export interface UseDataTableQueryOptions {
50
118
  * remplace celui de l'URL, et `syncUrl` / `history` sont ignorés.
51
119
  */
52
120
  storage?: TableQueryStorage;
121
+ /**
122
+ * Le routeur TanStack Router de l'application. Raccourci de
123
+ * `storage: createTanStackRouterStorage(router)` ; `history` s'applique.
124
+ */
125
+ router?: TanStackRouterLike;
53
126
  /** Les noms des paramètres serveur — `limit`, `sort` + `order`… */
54
127
  serverParams?: TableParamsMapping;
55
128
  }
@@ -69,14 +142,21 @@ export function useDataTableQuery(
69
142
  history = "replace",
70
143
  prefix,
71
144
  storage: fourni,
145
+ router,
72
146
  serverParams,
73
147
  } = options;
74
148
 
75
149
  // Créé une fois : un objet reconstruit à chaque rendu réabonnerait
76
150
  // l'écouteur `popstate` en boucle.
77
151
  const storage = useMemo(
78
- () => fourni ?? (syncUrl ? createUrlTableStorage(history) : undefined),
79
- [fourni, history, syncUrl],
152
+ () =>
153
+ fourni ??
154
+ (router
155
+ ? createTanStackRouterStorage(router, history)
156
+ : syncUrl
157
+ ? createUrlTableStorage(history)
158
+ : undefined),
159
+ [fourni, history, router, syncUrl],
80
160
  );
81
161
 
82
162
  return useTableQuery({
@@ -191,3 +191,56 @@ export function SecretFields({
191
191
  </div>
192
192
  );
193
193
  }
194
+
195
+ /** Ce qu'il faut envoyer au serveur après une saisie dans `SecretFields`. */
196
+ export interface SecretChanges {
197
+ /** À créer ou à remplacer (rotation) : nom → nouvelle valeur. */
198
+ set: Record<string, string>;
199
+ /** Noms enregistrés retirés de la liste : à supprimer. */
200
+ remove: string[];
201
+ /**
202
+ * Noms nouveaux restés sans valeur. Rien à envoyer pour eux — mais les
203
+ * ignorer sans le dire perdrait une saisie : à signaler avant l'envoi.
204
+ */
205
+ missing: string[];
206
+ }
207
+
208
+ /**
209
+ * Ce qui change, calculé une fois pour tous les projets.
210
+ *
211
+ * Un nom enregistré laissé vide est inchangé ; saisi, il est remplacé. Un nom
212
+ * enregistré absent de la liste est à supprimer — y compris quand il a été
213
+ * renommé : l'ancien part, le nouveau arrive avec sa valeur. Les lignes sans
214
+ * nom sont ignorées ; un nom en double garde sa dernière valeur saisie.
215
+ *
216
+ * ```ts
217
+ * const { set, remove, missing } = secretChanges(secrets, enregistres);
218
+ * if (missing.length) return; // à compléter
219
+ * await api.patch(`/projets/${id}/secrets`, { body: { set, remove } });
220
+ * setSecrets(clearSecretValues(secrets));
221
+ * ```
222
+ */
223
+ export function secretChanges(
224
+ entries: SecretEntry[],
225
+ storedKeys: string[] = [],
226
+ ): SecretChanges {
227
+ const enregistres = new Set(storedKeys);
228
+ const presents = new Set<string>();
229
+ const set: Record<string, string> = {};
230
+ const missing: string[] = [];
231
+
232
+ for (const { key, value } of entries) {
233
+ const nom = key.trim();
234
+ if (!nom) continue;
235
+ presents.add(nom);
236
+ if (value !== "") set[nom] = value;
237
+ else if (!enregistres.has(nom) && !missing.includes(nom)) missing.push(nom);
238
+ }
239
+
240
+ // Un nom saisi plus loin avec une valeur n'est plus manquant.
241
+ return {
242
+ set,
243
+ remove: storedKeys.filter((nom) => !presents.has(nom)),
244
+ missing: missing.filter((nom) => !(nom in set)),
245
+ };
246
+ }
@@ -191,6 +191,16 @@ export const Select = forwardRef<HTMLButtonElement, SelectProps>(function Select
191
191
  if (searchable || fetcher) window.setTimeout(() => searchRef.current?.focus(), 0);
192
192
  }, [currentValue, fetcher, open, searchable, visibleOptions]);
193
193
 
194
+ // L'option active reste visible : au clavier, descendre au-delà du bas de
195
+ // la liste la laissait hors champ, et l'on choisissait à l'aveugle.
196
+ // `nearest` ne fait défiler que si elle sort de la zone visible.
197
+ useEffect(() => {
198
+ if (!open || activeIndex < 0) return;
199
+ document
200
+ .getElementById(`${listboxId}-${activeIndex}`)
201
+ ?.scrollIntoView?.({ block: "nearest" });
202
+ }, [activeIndex, listboxId, open]);
203
+
194
204
  const setTriggerRef = (node: HTMLButtonElement | null) => {
195
205
  triggerRef.current = node;
196
206
  if (typeof forwardedRef === "function") forwardedRef(node);
@@ -12,10 +12,13 @@
12
12
  .sia-select__clear:hover { color:var(--sia-foreground); background:var(--sia-surface); }
13
13
  .sia-select__chevron { transform:translateY(-.5rem); font-size:1.1rem; }
14
14
  .sia-select__overlay { overflow:hidden; border:1px solid var(--sia-border); border-radius:var(--sia-radius); color:var(--sia-foreground); background:var(--sia-background); box-shadow:0 14px 36px rgb(15 23 42 / .16); }
15
- .sia-select__panel { display:grid; min-width:12rem; max-height:min(22rem,calc(100vh - 1rem)); }
16
- .sia-select__search-wrap { display:flex; align-items:center; gap:.5rem; padding:.55rem .65rem; color:var(--sia-muted); border-bottom:1px solid var(--sia-border); }
15
+ /* Colonne flex, pas grille : un enfant de grille garde une hauteur minimale
16
+ égale à son contenu, la liste débordait alors du panneau au lieu de
17
+ défiler — et l'overlay, en overflow:hidden, la coupait. */
18
+ .sia-select__panel { display:flex; flex-direction:column; min-width:12rem; max-height:min(22rem,calc(100vh - 1rem)); }
19
+ .sia-select__search-wrap { flex:none; display:flex; align-items:center; gap:.5rem; padding:.55rem .65rem; color:var(--sia-muted); border-bottom:1px solid var(--sia-border); }
17
20
  .sia-select__search { min-width:0; width:100%; padding:.2rem 0; border:0; outline:0; color:var(--sia-foreground); background:transparent; font:inherit; }
18
- .sia-select__options { overflow-y:auto; padding:.35rem; }
21
+ .sia-select__options { flex:1 1 auto; min-height:0; overflow-y:auto; overscroll-behavior:contain; padding:.35rem; }
19
22
  .sia-select__option { display:flex; align-items:center; justify-content:space-between; gap:.75rem; width:100%; padding:.55rem .65rem; border:0; border-radius:calc(var(--sia-radius) * .7); color:inherit; background:transparent; font:inherit; text-align:left; cursor:pointer; }
20
23
  .sia-select__option:hover:not(:disabled),.sia-select__option--active:not(:disabled) { background:var(--sia-surface); }
21
24
  .sia-select__option[aria-selected="true"] { color:var(--sia-primary); background:color-mix(in srgb,var(--sia-primary) 10%,transparent); }
@@ -452,7 +452,7 @@
452
452
  "name": "data-table",
453
453
  "description": "Data table: row actions with permissions and confirmation, selection, card view, density and sorting.",
454
454
  "type": "component",
455
- "version": "0.4.0",
455
+ "version": "0.5.0",
456
456
  "status": "stable",
457
457
  "source": "package",
458
458
  "files": [
@@ -1415,7 +1415,7 @@
1415
1415
  "name": "secret-fields",
1416
1416
  "description": "Write-only key/value secrets editor: stored names shown, values never read back.",
1417
1417
  "type": "component",
1418
- "version": "0.3.0",
1418
+ "version": "0.4.0",
1419
1419
  "status": "experimental",
1420
1420
  "source": "package",
1421
1421
  "files": [
@@ -1437,7 +1437,7 @@
1437
1437
  "name": "select",
1438
1438
  "description": "Accessible searchable select with positioned overlay and async option loading.",
1439
1439
  "type": "component",
1440
- "version": "0.3.0",
1440
+ "version": "0.3.1",
1441
1441
  "status": "stable",
1442
1442
  "source": "package",
1443
1443
  "files": [
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sia-ui/cli",
3
- "version": "0.6.0",
3
+ "version": "0.7.1",
4
4
  "description": "Install SIA UI components progressively into an application.",
5
5
  "keywords": [
6
6
  "sia-ui",
@@ -39,10 +39,10 @@
39
39
  "react-dom": "^19.2.3",
40
40
  "typescript": "^5.9.3",
41
41
  "vitest": "^4.0.16",
42
- "@sia-ui/headless": "0.6.0",
43
- "@sia-ui/react": "0.6.0",
44
- "@sia-ui/utils": "0.6.0",
45
- "@sia-ui/tokens": "0.6.0"
42
+ "@sia-ui/headless": "0.7.1",
43
+ "@sia-ui/react": "0.7.1",
44
+ "@sia-ui/tokens": "0.7.1",
45
+ "@sia-ui/utils": "0.7.1"
46
46
  },
47
47
  "scripts": {
48
48
  "build": "node scripts/build.mjs",