@sia-ui/cli 0.5.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.
@@ -29,7 +29,7 @@ import type { SelectOption } from "../Select";
29
29
  import type { FormEntry, FormFieldConfig, FormShape } from "../Form";
30
30
  import type { ColumnCardRole, DataTableColumn } from "../DataTable/types";
31
31
  import { AmountDisplay } from "../AmountDisplay";
32
- import { Badge } from "../Badge";
32
+ import { Badge, toneOf } from "../Badge";
33
33
 
34
34
  /**
35
35
  * Une propriété d'une ressource.
@@ -40,6 +40,9 @@ import { Badge } from "../Badge";
40
40
  * même client finissent toujours par diverger, et c'est le formulaire qui
41
41
  * oublie le champ que le tableau affiche.
42
42
  */
43
+ /** Le formulaire ouvert : une création, ou la modification d'une ligne. */
44
+ export type ResourceFormMode = "create" | "edit";
45
+
43
46
  export interface ResourceField<T = unknown> {
44
47
  /** Le contrôle de saisie et le rendu par défaut. `text` sans précision. */
45
48
  type?: FieldType;
@@ -49,7 +52,13 @@ export interface ResourceField<T = unknown> {
49
52
  helpText?: ReactNode;
50
53
  placeholder?: string;
51
54
 
52
- required?: boolean;
55
+ /**
56
+ * Obligatoire toujours, ou dans un seul mode.
57
+ *
58
+ * `"create"` sert au mot de passe initial : exigé à la création, facultatif
59
+ * ensuite — le laisser vide en modification veut dire « inchangé ».
60
+ */
61
+ required?: boolean | ResourceFormMode;
53
62
  /** Les règles en plus de `required`. Celles du type sont ajoutées seules. */
54
63
  rules?: Array<Rule<never>>;
55
64
 
@@ -71,8 +80,11 @@ export interface ResourceField<T = unknown> {
71
80
 
72
81
  /** Par défaut, tout sauf les types trop longs pour une cellule. */
73
82
  inTable?: boolean;
74
- /** Par défaut, tout sauf `readOnly`. */
75
- inForm?: boolean;
83
+ /**
84
+ * Par défaut, tout sauf `readOnly` et les valeurs calculées. `"create"` ou
85
+ * `"edit"` limitent le champ à l'un des deux formulaires.
86
+ */
87
+ inForm?: boolean | ResourceFormMode;
76
88
  /** Par défaut, tout. */
77
89
  inDetail?: boolean;
78
90
  /** Par défaut, les types textuels. */
@@ -105,6 +117,16 @@ export interface ResourceField<T = unknown> {
105
117
 
106
118
  /** Le rendu en tableau et en détail. Écrase celui déduit du type. */
107
119
  render?: (row: T, index: number) => ReactNode;
120
+
121
+ /**
122
+ * Une valeur calculée plutôt que lue sur la ligne.
123
+ *
124
+ * La colonne peut alors porter une clé qui n'est pas une propriété de `T`
125
+ * — un nom complet, un solde — sans élargir le type de la ligne. Le rendu
126
+ * par défaut, la carte et la recherche locale passent par elle. Une valeur
127
+ * calculée ne se saisit pas : elle sort du formulaire, sauf `inForm`.
128
+ */
129
+ value?: (row: T) => unknown;
108
130
  }
109
131
 
110
132
  export interface ResourceDefinition<T> {
@@ -221,13 +243,29 @@ function libelleParDefaut(cle: string): string {
221
243
  return mots.charAt(0).toUpperCase() + mots.slice(1);
222
244
  }
223
245
 
224
- function estVisible(champ: ResourceField<never>, ou: "table" | "form" | "detail") {
246
+ function estVisible(
247
+ champ: ResourceField<never>,
248
+ ou: "table" | "form" | "detail",
249
+ mode: ResourceFormMode = "create",
250
+ ) {
225
251
  const type = champ.type ?? "text";
226
252
  if (ou === "table") return champ.inTable ?? !HORS_TABLEAU.has(type);
227
- if (ou === "form") return champ.inForm ?? !champ.readOnly;
253
+ if (ou === "form") {
254
+ const inForm = champ.inForm ?? (!champ.readOnly && !champ.value);
255
+ return typeof inForm === "string" ? inForm === mode : inForm;
256
+ }
228
257
  return champ.inDetail ?? true;
229
258
  }
230
259
 
260
+ function estRequis(champ: ResourceField<never>, mode: ResourceFormMode) {
261
+ return champ.required === true || champ.required === mode;
262
+ }
263
+
264
+ /** Comment lire la valeur d'une propriété : calculée, ou sur la ligne. */
265
+ function lecteur<T>(cle: string, champ: ResourceField<T>): (row: T) => unknown {
266
+ return champ.value ?? ((row) => (row as Record<string, unknown>)[cle]);
267
+ }
268
+
231
269
  /** Les propriétés dans l'ordre de déclaration, avec leur clé. */
232
270
  function entrees<T>(resource: ResourceDefinition<T>) {
233
271
  return Object.entries(resource.fields) as Array<[string, ResourceField<T>]>;
@@ -247,10 +285,11 @@ function renduParDefaut<T>(
247
285
  champ: ResourceField<T>,
248
286
  ): ((row: T, index: number) => ReactNode) | undefined {
249
287
  const type = champ.type ?? "text";
288
+ const lire = lecteur(cle, champ);
250
289
 
251
290
  if (type === "currency") {
252
291
  return (row) => {
253
- const valeur = (row as Record<string, unknown>)[cle];
292
+ const valeur = lire(row);
254
293
  if (typeof valeur !== "number") return null;
255
294
  return (
256
295
  <AmountDisplay
@@ -263,23 +302,28 @@ function renduParDefaut<T>(
263
302
 
264
303
  if (type === "select" || type === "radio") {
265
304
  return (row) => {
266
- const valeur = (row as Record<string, unknown>)[cle];
305
+ const valeur = lire(row);
267
306
  if (valeur === null || valeur === undefined || valeur === "") return null;
268
307
  const brut = String(valeur);
269
308
  const option = champ.options?.find((o) => o.value === brut);
270
309
  const libelle = option?.label ?? brut;
271
- const ton = champ.tones?.[brut];
272
- return ton ? <Badge tone={ton}>{libelle}</Badge> : <>{libelle}</>;
310
+ // Sans ton déclaré pour cette valeur, un mot plutôt qu'une pastille
311
+ // neutre : la colonne n'a pas demandé à être lue comme un statut.
312
+ return champ.tones?.[brut] ? (
313
+ <Badge tone={toneOf(champ.tones, brut)}>{libelle}</Badge>
314
+ ) : (
315
+ <>{libelle}</>
316
+ );
273
317
  };
274
318
  }
275
319
 
276
320
  if (type === "checkbox" || type === "switch") {
277
- return (row) => ((row as Record<string, unknown>)[cle] ? "Oui" : "Non");
321
+ return (row) => (lire(row) ? "Oui" : "Non");
278
322
  }
279
323
 
280
324
  if (type === "date" || type === "datetime") {
281
325
  return (row) => {
282
- const valeur = (row as Record<string, unknown>)[cle];
326
+ const valeur = lire(row);
283
327
  if (typeof valeur !== "string" && !(valeur instanceof Date)) return null;
284
328
  return type === "date" ? formatDate(valeur) : formatDateTime(valeur);
285
329
  };
@@ -287,7 +331,7 @@ function renduParDefaut<T>(
287
331
 
288
332
  if (type === "number") {
289
333
  return (row) => {
290
- const valeur = (row as Record<string, unknown>)[cle];
334
+ const valeur = lire(row);
291
335
  if (typeof valeur !== "number") return null;
292
336
  return formatNumber(valeur, {
293
337
  ...(champ.decimals !== undefined
@@ -302,7 +346,7 @@ function renduParDefaut<T>(
302
346
 
303
347
  if (type === "multiselect" || type === "tags") {
304
348
  return (row) => {
305
- const valeur = (row as Record<string, unknown>)[cle];
349
+ const valeur = lire(row);
306
350
  if (!Array.isArray(valeur)) return null;
307
351
  return valeur
308
352
  .map((v) => {
@@ -345,6 +389,7 @@ export function resourceColumns<T>(
345
389
  colonnes.push({
346
390
  key: cle,
347
391
  header: champ.label ?? libelleParDefaut(cle),
392
+ ...(champ.value ? { accessor: champ.value } : {}),
348
393
  ...(rendu ? { cell: rendu } : {}),
349
394
  align: champ.align ?? (NUMERIQUES.has(type) ? "end" : "start"),
350
395
  ...(champ.width ? { width: champ.width } : {}),
@@ -376,6 +421,7 @@ export function resourceDetailColumns<T>(
376
421
  lignes.push({
377
422
  key: cle,
378
423
  header: champ.label ?? libelleParDefaut(cle),
424
+ ...(champ.value ? { accessor: champ.value } : {}),
379
425
  ...(rendu ? { cell: rendu } : {}),
380
426
  });
381
427
  }
@@ -386,18 +432,19 @@ export function resourceDetailColumns<T>(
386
432
  /** Les champs du formulaire, groupés si la déclaration les groupe. */
387
433
  export function resourceFormFields<T>(
388
434
  resource: ResourceDefinition<T>,
435
+ mode: ResourceFormMode = "create",
389
436
  ): FormEntry[] {
390
437
  const libres: FormFieldConfig[] = [];
391
438
  const groupes = new Map<string, FormFieldConfig[]>();
392
439
 
393
440
  for (const [cle, champ] of entrees(resource)) {
394
- if (!estVisible(champ as ResourceField<never>, "form")) continue;
441
+ if (!estVisible(champ as ResourceField<never>, "form", mode)) continue;
395
442
 
396
443
  const config: FormFieldConfig = {
397
444
  name: cle,
398
445
  label: champ.label ?? libelleParDefaut(cle),
399
446
  ...(champ.type ? { type: champ.type } : {}),
400
- ...(champ.required ? { required: champ.required } : {}),
447
+ ...(estRequis(champ as ResourceField<never>, mode) ? { required: true } : {}),
401
448
  ...(champ.placeholder ? { placeholder: champ.placeholder } : {}),
402
449
  ...(champ.helpText ? { helpText: champ.helpText } : {}),
403
450
  ...(champ.description ? { description: champ.description } : {}),
@@ -428,14 +475,17 @@ export function resourceFormFields<T>(
428
475
  */
429
476
  export function resourceValidator<T>(
430
477
  resource: ResourceDefinition<T>,
478
+ mode: ResourceFormMode = "create",
431
479
  ): FormValidator<FormShape> | undefined {
432
480
  const schema: Record<string, Array<Rule<never>>> = {};
433
481
 
434
482
  for (const [cle, champ] of entrees(resource)) {
435
- if (!estVisible(champ as ResourceField<never>, "form")) continue;
483
+ if (!estVisible(champ as ResourceField<never>, "form", mode)) continue;
436
484
 
437
485
  const regles: Array<Rule<never>> = [];
438
- if (champ.required) regles.push(regleRequise() as Rule<never>);
486
+ if (estRequis(champ as ResourceField<never>, mode)) {
487
+ regles.push(regleRequise() as Rule<never>);
488
+ }
439
489
 
440
490
  const duType = REGLE_DU_TYPE[champ.type ?? "text"];
441
491
  if (duType) regles.push(duType());
@@ -459,7 +509,7 @@ export function resourceSearchKeys<T>(
459
509
  .map(([cle]) => cle);
460
510
  }
461
511
 
462
- /** Les valeurs de départ d'une création. */
512
+ /** Les valeurs de départ d'une création : les champs du mode `create`. */
463
513
  export function resourceDefaults<T>(resource: ResourceDefinition<T>): FormShape {
464
514
  const valeurs: FormShape = {};
465
515
  for (const [cle, champ] of entrees(resource)) {
@@ -0,0 +1,193 @@
1
+ import { useId, useMemo, useState, type ReactNode } from "react";
2
+ import { cn } from "@sia-ui/utils";
3
+ import { Button } from "../Button";
4
+ import { IconButton } from "../IconButton";
5
+ import { Input } from "../Input";
6
+ import { PasswordInput } from "../PasswordInput";
7
+ import { PlusIcon, TrashIcon } from "../Icons";
8
+ import "./styles.css";
9
+
10
+ /** Une paire nom / valeur. La valeur n'est jamais relue du serveur. */
11
+ export interface SecretEntry {
12
+ key: string;
13
+ value: string;
14
+ }
15
+
16
+ export interface SecretFieldsProps {
17
+ value?: SecretEntry[];
18
+ defaultValue?: SecretEntry[];
19
+ onValueChange?: (entries: SecretEntry[]) => void;
20
+ /**
21
+ * Les noms déjà enregistrés côté serveur.
22
+ *
23
+ * Affichés, mais leur valeur reste vide : un secret ne revient jamais au
24
+ * navigateur. Une valeur laissée vide sur l'un d'eux veut dire « inchangé » ;
25
+ * en saisir une, c'est le remplacer — une rotation.
26
+ */
27
+ storedKeys?: string[];
28
+ /** Des noms proposés d'un clic : `DATABASE_URL`, `API_KEY`… */
29
+ templates?: string[];
30
+ /** La forme d'un nom valide. Par défaut, aucune contrainte. */
31
+ keyPattern?: RegExp;
32
+ keyPatternMessage?: string;
33
+ keyLabel?: string;
34
+ valueLabel?: string;
35
+ addLabel?: ReactNode;
36
+ disabled?: boolean;
37
+ className?: string;
38
+ }
39
+
40
+ /** Les valeurs effacées, les noms gardés — ce qu'on affiche après l'envoi. */
41
+ export function clearSecretValues(entries: SecretEntry[]): SecretEntry[] {
42
+ return entries.map((entry) => ({ ...entry, value: "" }));
43
+ }
44
+
45
+ /**
46
+ * Des secrets en écriture seule : les noms se lisent, les valeurs jamais.
47
+ *
48
+ * Chaque ligne est un nom et une valeur masquée. Les noms déjà enregistrés
49
+ * s'affichent sans leur valeur — le serveur ne la renvoie pas, et ne doit pas
50
+ * la renvoyer. Les champs refusent l'autocomplétion du navigateur, qui
51
+ * proposerait sinon le mot de passe du compte à la place d'une clé d'API.
52
+ *
53
+ * Après l'envoi, `clearSecretValues` vide les valeurs : un secret ne reste pas
54
+ * affiché dans un formulaire ouvert.
55
+ */
56
+ export function SecretFields({
57
+ value,
58
+ defaultValue,
59
+ onValueChange,
60
+ storedKeys = [],
61
+ templates = [],
62
+ keyPattern,
63
+ keyPatternMessage = "Nom invalide",
64
+ keyLabel = "Nom",
65
+ valueLabel = "Valeur",
66
+ addLabel = "Ajouter un secret",
67
+ disabled = false,
68
+ className,
69
+ }: SecretFieldsProps) {
70
+ const id = useId();
71
+ const [interne, setInterne] = useState<SecretEntry[]>(
72
+ () => defaultValue ?? storedKeys.map((key) => ({ key, value: "" })),
73
+ );
74
+ const lignes = value ?? interne;
75
+ const enregistres = useMemo(() => new Set(storedKeys), [storedKeys]);
76
+
77
+ const changer = (suivantes: SecretEntry[]) => {
78
+ if (value === undefined) setInterne(suivantes);
79
+ onValueChange?.(suivantes);
80
+ };
81
+
82
+ /** Un doublon ou un nom mal formé, ligne par ligne. */
83
+ const erreurs = useMemo(() => {
84
+ const vus = new Map<string, number>();
85
+ for (const ligne of lignes) {
86
+ const nom = ligne.key.trim();
87
+ if (nom) vus.set(nom, (vus.get(nom) ?? 0) + 1);
88
+ }
89
+ return lignes.map((ligne) => {
90
+ const nom = ligne.key.trim();
91
+ if (!nom) return undefined;
92
+ if ((vus.get(nom) ?? 0) > 1) return "Ce nom est déjà utilisé";
93
+ if (keyPattern && !keyPattern.test(nom)) return keyPatternMessage;
94
+ return undefined;
95
+ });
96
+ }, [keyPattern, keyPatternMessage, lignes]);
97
+
98
+ const presents = new Set(lignes.map((ligne) => ligne.key.trim()));
99
+ const proposes = templates.filter((nom) => !presents.has(nom));
100
+
101
+ const modifier = (index: number, patch: Partial<SecretEntry>) =>
102
+ changer(lignes.map((ligne, i) => (i === index ? { ...ligne, ...patch } : ligne)));
103
+
104
+ return (
105
+ <div className={cn("sia-secrets", className)}>
106
+ {lignes.length > 0 && (
107
+ <div className="sia-secrets__head" aria-hidden="true">
108
+ <span>{keyLabel}</span>
109
+ <span>{valueLabel}</span>
110
+ </div>
111
+ )}
112
+
113
+ {lignes.map((ligne, index) => {
114
+ const erreur = erreurs[index];
115
+ const garde = enregistres.has(ligne.key.trim());
116
+ const idErreur = `${id}-erreur-${index}`;
117
+
118
+ return (
119
+ <div className="sia-secrets__row" key={index}>
120
+ <div className="sia-secrets__key">
121
+ <Input
122
+ value={ligne.key}
123
+ aria-label={`${keyLabel} ${index + 1}`}
124
+ placeholder="NOM_DU_SECRET"
125
+ autoComplete="off"
126
+ spellCheck={false}
127
+ disabled={disabled}
128
+ invalid={Boolean(erreur)}
129
+ {...(erreur ? { "aria-describedby": idErreur } : {})}
130
+ onChange={(event) => modifier(index, { key: event.target.value })}
131
+ />
132
+ {erreur && (
133
+ <span id={idErreur} className="sia-secrets__error" role="alert">
134
+ {erreur}
135
+ </span>
136
+ )}
137
+ </div>
138
+
139
+ <PasswordInput
140
+ value={ligne.value}
141
+ aria-label={`${valueLabel} ${ligne.key || index + 1}`}
142
+ placeholder={garde ? "Inchangée si vide" : "Valeur"}
143
+ // `new-password` : le seul réglage que les navigateurs
144
+ // respectent pour ne pas proposer un mot de passe enregistré.
145
+ autoComplete="new-password"
146
+ spellCheck={false}
147
+ disabled={disabled}
148
+ onChange={(event) => modifier(index, { value: event.target.value })}
149
+ />
150
+
151
+ <IconButton
152
+ label={`Retirer ${ligne.key || "la ligne"}`}
153
+ icon={<TrashIcon />}
154
+ variant="ghost"
155
+ disabled={disabled}
156
+ onClick={() => changer(lignes.filter((_, i) => i !== index))}
157
+ />
158
+ </div>
159
+ );
160
+ })}
161
+
162
+ <div className="sia-secrets__footer">
163
+ <Button
164
+ type="button"
165
+ variant="outline"
166
+ size="sm"
167
+ disabled={disabled}
168
+ leftIcon={<PlusIcon />}
169
+ onClick={() => changer([...lignes, { key: "", value: "" }])}
170
+ >
171
+ {addLabel}
172
+ </Button>
173
+
174
+ {proposes.length > 0 && (
175
+ <div className="sia-secrets__templates">
176
+ {proposes.map((nom) => (
177
+ <button
178
+ key={nom}
179
+ type="button"
180
+ className="sia-secrets__template"
181
+ disabled={disabled}
182
+ onClick={() => changer([...lignes, { key: nom, value: "" }])}
183
+ >
184
+ <PlusIcon aria-hidden="true" />
185
+ {nom}
186
+ </button>
187
+ ))}
188
+ </div>
189
+ )}
190
+ </div>
191
+ </div>
192
+ );
193
+ }
@@ -0,0 +1,84 @@
1
+ /* Trois colonnes alignées sur toutes les lignes : nom, valeur, retrait. Le
2
+ nom est en chasse fixe — MAJUSCULES_ET_SOULIGNÉS se relisent mieux, et une
3
+ faute de frappe s'y voit. */
4
+ .sia-secrets {
5
+ container: sia-secrets / inline-size;
6
+ display: grid;
7
+ gap: .5rem;
8
+ }
9
+
10
+ .sia-secrets__head,
11
+ .sia-secrets__row {
12
+ display: grid;
13
+ grid-template-columns: minmax(0, 1fr) minmax(0, 1.35fr) 2.5rem;
14
+ align-items: start;
15
+ gap: .5rem;
16
+ }
17
+
18
+ .sia-secrets__head {
19
+ color: var(--sia-muted);
20
+ font-size: .75rem;
21
+ font-weight: 600;
22
+ letter-spacing: .02em;
23
+ }
24
+
25
+ .sia-secrets__key { display: grid; gap: .25rem; min-width: 0; }
26
+ .sia-secrets__key input {
27
+ font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
28
+ font-size: .85rem;
29
+ }
30
+ .sia-secrets__error { color: var(--sia-danger); font-size: .78rem; }
31
+
32
+ /* Le bouton de retrait a la hauteur d'un champ, pour tomber en face. */
33
+ .sia-secrets__row > .sia-icon-button { width: 2.5rem; height: 2.5rem; }
34
+
35
+ .sia-secrets__footer {
36
+ display: flex;
37
+ flex-wrap: wrap;
38
+ align-items: center;
39
+ gap: .5rem .75rem;
40
+ margin-top: .25rem;
41
+ }
42
+
43
+ .sia-secrets__templates { display: flex; flex-wrap: wrap; gap: .375rem; }
44
+ .sia-secrets__template {
45
+ display: inline-flex;
46
+ align-items: center;
47
+ gap: .3rem;
48
+ height: 1.75rem;
49
+ padding: 0 .6rem;
50
+ border: 1px dashed var(--sia-border-strong);
51
+ border-radius: var(--sia-radius-full);
52
+ color: var(--sia-muted);
53
+ background: transparent;
54
+ font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
55
+ font-size: .75rem;
56
+ cursor: pointer;
57
+ transition:
58
+ color var(--sia-duration-fast) var(--sia-easing-standard),
59
+ border-color var(--sia-duration-fast) var(--sia-easing-standard),
60
+ background-color var(--sia-duration-fast) var(--sia-easing-standard);
61
+ }
62
+ .sia-secrets__template:hover:not(:disabled) {
63
+ border-color: var(--sia-primary);
64
+ color: var(--sia-primary);
65
+ background: color-mix(in srgb, var(--sia-primary) 6%, transparent);
66
+ }
67
+ .sia-secrets__template:focus-visible {
68
+ outline: 3px solid color-mix(in srgb, var(--sia-primary) 30%, transparent);
69
+ outline-offset: 2px;
70
+ }
71
+ .sia-secrets__template:disabled { cursor: not-allowed; opacity: .5; }
72
+
73
+ /* Étroit : le nom au-dessus, la valeur et le retrait en dessous. Chaque
74
+ ligne se sépare de la suivante par un filet, sans quoi les paires se
75
+ confondent. */
76
+ @container sia-secrets (max-width: 30rem) {
77
+ .sia-secrets__head { display: none; }
78
+ .sia-secrets__row {
79
+ grid-template-columns: minmax(0, 1fr) 2.5rem;
80
+ padding-bottom: .5rem;
81
+ border-bottom: 1px solid var(--sia-border);
82
+ }
83
+ .sia-secrets__key { grid-column: 1 / -1; }
84
+ }
@@ -22,6 +22,32 @@
22
22
  "icons"
23
23
  ]
24
24
  },
25
+ {
26
+ "name": "activity-log",
27
+ "description": "Server-filtered activity log: declared filters, debounced search, pagination and page size.",
28
+ "type": "pattern",
29
+ "version": "0.3.0",
30
+ "status": "experimental",
31
+ "source": "package",
32
+ "files": [
33
+ "components/ActivityLog/index.tsx",
34
+ "components/ActivityLog/styles.css"
35
+ ],
36
+ "dependencies": [
37
+ "@sia-ui/headless",
38
+ "@sia-ui/tokens",
39
+ "@sia-ui/utils"
40
+ ],
41
+ "registryDependencies": [
42
+ "badge",
43
+ "data-table",
44
+ "filters-bar",
45
+ "page-header",
46
+ "pagination",
47
+ "search-input",
48
+ "select"
49
+ ]
50
+ },
25
51
  {
26
52
  "name": "alert",
27
53
  "description": "Semantic feedback alert with tones, action and dismissal.",
@@ -179,7 +205,7 @@
179
205
  "name": "badge",
180
206
  "description": "Compact semantic status label with side content and loading state.",
181
207
  "type": "primitive",
182
- "version": "0.1.0",
208
+ "version": "0.3.0",
183
209
  "status": "stable",
184
210
  "source": "package",
185
211
  "files": [
@@ -383,7 +409,7 @@
383
409
  "name": "crud-page",
384
410
  "description": "List page for a declared resource: create, view, edit and delete open their own dialogs, each removable and permission-aware.",
385
411
  "type": "pattern",
386
- "version": "0.3.0",
412
+ "version": "0.4.0",
387
413
  "status": "stable",
388
414
  "source": "package",
389
415
  "files": [
@@ -426,7 +452,7 @@
426
452
  "name": "data-table",
427
453
  "description": "Data table: row actions with permissions and confirmation, selection, card view, density and sorting.",
428
454
  "type": "component",
429
- "version": "0.3.0",
455
+ "version": "0.4.0",
430
456
  "status": "stable",
431
457
  "source": "package",
432
458
  "files": [
@@ -846,7 +872,7 @@
846
872
  "name": "icons",
847
873
  "description": "Jeu d'icônes SVG sans dépendance, servi par le package.",
848
874
  "type": "primitive",
849
- "version": "0.1.0",
875
+ "version": "0.3.0",
850
876
  "status": "stable",
851
877
  "source": "package",
852
878
  "files": [
@@ -1080,7 +1106,7 @@
1080
1106
  "name": "page-header",
1081
1107
  "description": "Consistent page title, description and action area.",
1082
1108
  "type": "pattern",
1083
- "version": "0.1.0",
1109
+ "version": "0.3.0",
1084
1110
  "status": "stable",
1085
1111
  "source": "package",
1086
1112
  "files": [
@@ -1316,7 +1342,7 @@
1316
1342
  "name": "resource",
1317
1343
  "description": "Declare a resource once: table columns, form fields, detail view, validation, search keys and permissions are all derived from it.",
1318
1344
  "type": "pattern",
1319
- "version": "0.2.0",
1345
+ "version": "0.3.0",
1320
1346
  "status": "stable",
1321
1347
  "source": "package",
1322
1348
  "files": [
@@ -1385,6 +1411,28 @@
1385
1411
  "icons"
1386
1412
  ]
1387
1413
  },
1414
+ {
1415
+ "name": "secret-fields",
1416
+ "description": "Write-only key/value secrets editor: stored names shown, values never read back.",
1417
+ "type": "component",
1418
+ "version": "0.3.0",
1419
+ "status": "experimental",
1420
+ "source": "package",
1421
+ "files": [
1422
+ "components/SecretFields/index.tsx",
1423
+ "components/SecretFields/styles.css"
1424
+ ],
1425
+ "dependencies": [
1426
+ "@sia-ui/utils"
1427
+ ],
1428
+ "registryDependencies": [
1429
+ "button",
1430
+ "icon-button",
1431
+ "icons",
1432
+ "input",
1433
+ "password-input"
1434
+ ]
1435
+ },
1388
1436
  {
1389
1437
  "name": "select",
1390
1438
  "description": "Accessible searchable select with positioned overlay and async option loading.",
@@ -209,7 +209,7 @@
209
209
  /* Wave 2 business components */
210
210
  @media (max-width:900px) { .sia-kpi-grid--3,.sia-kpi-grid--4 { grid-template-columns:repeat(2,minmax(0,1fr)); } }
211
211
  @media (max-width:560px) { .sia-kpi-grid--2,.sia-kpi-grid--3,.sia-kpi-grid--4 { grid-template-columns:1fr; } .sia-filters-bar { align-items:stretch; }.sia-filters-bar__main,.sia-filters-bar__actions { width:100%; } }
212
- @media (max-width:640px) { .sia-page-header { flex-direction:column; } .sia-page-header__actions { width:100%; } .sia-entity-meta__grid { grid-template-columns:1fr; } }
212
+ @media (max-width:640px) { .sia-entity-meta__grid { grid-template-columns:1fr; } }
213
213
  .sia-month-picker,.sia-year-picker,.sia-week-picker { min-height:2.5rem; padding:0 .75rem; border:1px solid var(--sia-border); border-radius:var(--sia-radius); color:var(--sia-foreground); background:var(--sia-background); font:inherit; }
214
214
  .sia-picker--invalid { border-color:var(--sia-danger); }
215
215
  .sia-tone--neutral { --sia-accent:var(--sia-muted); --sia-accent-foreground:var(--sia-background); }
package/dist/source.mjs CHANGED
@@ -40,6 +40,7 @@ export async function sourceLocale(racine) {
40
40
  origine: "embarqué",
41
41
  registre,
42
42
  lire: (fichier) => readFile(join(racine, "package", fichier)),
43
+ lireStyles: () => readFile(join(racine, "styles/sia-ui.css")),
43
44
  };
44
45
  }
45
46
 
@@ -78,6 +79,15 @@ export async function sourceDistante(base) {
78
79
  }
79
80
  return Buffer.from(await r.arrayBuffer());
80
81
  },
82
+ // La feuille vient du même registre que les composants : servis par une
83
+ // URL, ils ne doivent pas dépendre de la feuille figée dans la CLI.
84
+ lireStyles: async () => {
85
+ const r = await fetch(new URL("styles/sia-ui.css", racine));
86
+ if (!r.ok) {
87
+ throw new Error(`styles/sia-ui.css a répondu ${r.status} sur ${racine}`);
88
+ }
89
+ return Buffer.from(await r.arrayBuffer());
90
+ },
81
91
  };
82
92
  }
83
93