@zevra/ui 0.8.0 → 0.10.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 (66) hide show
  1. package/DECISIONS.md +238 -0
  2. package/dist/actions.d.ts +13 -3
  3. package/dist/actions.d.ts.map +1 -1
  4. package/dist/actions.js +9 -2
  5. package/dist/actions.js.map +1 -1
  6. package/dist/catalogue.d.ts.map +1 -1
  7. package/dist/catalogue.js +163 -13
  8. package/dist/catalogue.js.map +1 -1
  9. package/dist/css-inline.d.ts.map +1 -1
  10. package/dist/css-inline.js +1 -1
  11. package/dist/css-inline.js.map +1 -1
  12. package/dist/data.d.ts +34 -1
  13. package/dist/data.d.ts.map +1 -1
  14. package/dist/data.js +27 -0
  15. package/dist/data.js.map +1 -1
  16. package/dist/feedback.d.ts +49 -0
  17. package/dist/feedback.d.ts.map +1 -1
  18. package/dist/feedback.js +24 -0
  19. package/dist/feedback.js.map +1 -1
  20. package/dist/forms.d.ts +36 -0
  21. package/dist/forms.d.ts.map +1 -1
  22. package/dist/forms.js +24 -0
  23. package/dist/forms.js.map +1 -1
  24. package/dist/icon-data.d.ts +1 -1
  25. package/dist/icon-data.d.ts.map +1 -1
  26. package/dist/icon-data.js +42 -0
  27. package/dist/icon-data.js.map +1 -1
  28. package/dist/icon.d.ts +1 -1
  29. package/dist/icon.js +1 -1
  30. package/dist/layout.d.ts +21 -0
  31. package/dist/layout.d.ts.map +1 -1
  32. package/dist/layout.js +18 -0
  33. package/dist/layout.js.map +1 -1
  34. package/dist/navigation.d.ts +45 -7
  35. package/dist/navigation.d.ts.map +1 -1
  36. package/dist/navigation.js +54 -7
  37. package/dist/navigation.js.map +1 -1
  38. package/dist/surfaces.d.ts +28 -6
  39. package/dist/surfaces.d.ts.map +1 -1
  40. package/dist/surfaces.js +18 -6
  41. package/dist/surfaces.js.map +1 -1
  42. package/icons/index.json +126 -0
  43. package/icons/sprite.svg +21 -0
  44. package/package.json +4 -3
  45. package/react/actions.tsx +28 -2
  46. package/react/catalogue.tsx +348 -18
  47. package/react/css-inline.ts +1 -1
  48. package/react/data.tsx +87 -1
  49. package/react/feedback.tsx +98 -0
  50. package/react/forms.tsx +90 -0
  51. package/react/icon-data.ts +63 -0
  52. package/react/icon.tsx +2 -2
  53. package/react/layout.tsx +39 -0
  54. package/react/navigation.tsx +108 -6
  55. package/react/surfaces.tsx +57 -7
  56. package/src/base.css +16 -0
  57. package/src/components.css +135 -3
  58. package/src/data.css +143 -1
  59. package/src/feedback.css +196 -37
  60. package/src/forms.css +137 -1
  61. package/src/layout.css +72 -0
  62. package/src/motion.css +23 -1
  63. package/src/navigation.css +207 -12
  64. package/src/surfaces.css +76 -7
  65. package/src/tokens.css +6 -0
  66. package/tokens/index.js +2 -0
package/react/data.tsx CHANGED
@@ -5,7 +5,13 @@
5
5
  // et en carte empilée en dessous, sans dupliquer le balisage. `Unwrap` est la
6
6
  // primitive qui rend cela possible.
7
7
 
8
- import type { AnchorHTMLAttributes, CSSProperties, HTMLAttributes, ReactNode } from 'react'
8
+ import type {
9
+ AnchorHTMLAttributes,
10
+ CSSProperties,
11
+ DetailsHTMLAttributes,
12
+ HTMLAttributes,
13
+ ReactNode,
14
+ } from 'react'
9
15
  import { cx } from './cx.js'
10
16
 
11
17
  type Div = Omit<HTMLAttributes<HTMLDivElement>, 'className'>
@@ -298,3 +304,83 @@ export function FileType({ format, className, ...reste }: FileTypeProps) {
298
304
  </span>
299
305
  )
300
306
  }
307
+
308
+ /* ─── Rangée dépliante ─────────────────────────────────────────────── */
309
+
310
+ export interface FoldProps
311
+ extends Omit<DetailsHTMLAttributes<HTMLDetailsElement>, 'className' | 'open'> {
312
+ titre: ReactNode
313
+ /** Ouverte au premier rendu. L'état vit ensuite dans le NAVIGATEUR
314
+ * (<details> natif) : pas de prop contrôlée, pas de JavaScript — le
315
+ * clavier et l'annonce viennent avec. `open` est retiré des attributs :
316
+ * deux canaux pour le même état, et c'est le silencieux qui gagnerait. */
317
+ ouvert?: boolean
318
+ className?: string
319
+ children?: ReactNode
320
+ }
321
+
322
+ /** Le pliage de l'applicatif : options avancées, détail d'une ligne de
323
+ * journal. La FAQ (contenu) reste le pliage de la VITRINE — question en
324
+ * 14px, respiration éditoriale ; celui-ci est un chrome dense. Le chevron
325
+ * fait partie du composant, comme la flèche de LinkArrow. */
326
+ export function Fold({ titre, ouvert, className, children, ...reste }: FoldProps) {
327
+ return (
328
+ <details className={cx('zv-fold', className)} open={ouvert} {...reste}>
329
+ <summary className="zv-fold__titre">
330
+ {titre}
331
+ <svg
332
+ className="zv-fold__chevron"
333
+ viewBox="0 0 24 24"
334
+ fill="none"
335
+ stroke="currentColor"
336
+ strokeWidth="2"
337
+ strokeLinecap="round"
338
+ strokeLinejoin="round"
339
+ aria-hidden="true"
340
+ >
341
+ <path d="M6.67 9.33 L12 14.67 L17.33 9.33" />
342
+ </svg>
343
+ </summary>
344
+ <div className="zv-fold__corps">{children}</div>
345
+ </details>
346
+ )
347
+ }
348
+
349
+ /* ─── Touche de clavier ────────────────────────────────────────────── */
350
+
351
+ /** Une touche, une seule : « ⌘K » est DEUX touches, donc deux `Kbd` — les
352
+ * fondre dans une seule dessine une touche qui n'existe sur aucun
353
+ * clavier. */
354
+ export function Kbd({
355
+ className,
356
+ ...reste
357
+ }: Omit<HTMLAttributes<HTMLElement>, 'className'> & { className?: string }) {
358
+ return <kbd className={cx('zv-kbd', className)} {...reste} />
359
+ }
360
+
361
+ /* ─── Code ─────────────────────────────────────────────────────────── */
362
+
363
+ /** Code EN LIGNE, dans une phrase : `pnpm build`. Taille relative au texte
364
+ * qui l'accueille. Pour un extrait de plusieurs lignes, `CodeBlock`. */
365
+ export function Code({
366
+ className,
367
+ ...reste
368
+ }: Omit<HTMLAttributes<HTMLElement>, 'className'> & { className?: string }) {
369
+ return <code className={cx('zv-code', className)} {...reste} />
370
+ }
371
+
372
+ /** Bloc de code. Chasse fixe RÉELLE (--font-code) : --font-mono est
373
+ * proportionnelle et n'aligne pas du code. Aucune coloration syntaxique —
374
+ * elle imposerait une palette entière que la charte n'a pas. Le bloc
375
+ * défile en son sein : une ligne longue ne pousse jamais la page. */
376
+ export function CodeBlock({
377
+ className,
378
+ children,
379
+ ...reste
380
+ }: Omit<HTMLAttributes<HTMLPreElement>, 'className'> & { className?: string }) {
381
+ return (
382
+ <pre className={cx('zv-codeblock', className)} {...reste}>
383
+ <code>{children}</code>
384
+ </pre>
385
+ )
386
+ }
@@ -227,3 +227,101 @@ export function Progress({ valeur, label, className, ...reste }: ProgressProps)
227
227
  </div>
228
228
  )
229
229
  }
230
+
231
+ /* ─── Roue ─────────────────────────────────────────────────────────── */
232
+
233
+ export interface SpinnerProps extends Span {
234
+ /** En tête d'un panneau d'attente : la roue seule, plus grande. */
235
+ grand?: boolean
236
+ /** Ce qu'on attend, annoncé au lecteur d'écran. Sans lui la roue est
237
+ * DÉCORATIVE et masquée — le bon défaut quand une phrase voisine dit
238
+ * déjà le travail (`Loading`, ou le libellé d'un bouton `occupe`). */
239
+ label?: string
240
+ className?: string
241
+ }
242
+
243
+ /** Attente COURTE ET LOCALE : un bouton, une cellule, un panneau sans la
244
+ * place d'une phrase. Pour l'attente d'un écran, préférer `Loading` — la
245
+ * phrase qui dit le travail réel reste le motif par défaut du système.
246
+ * Le libellé est un TEXTE masqué visuellement, pas un aria-label : sur un
247
+ * span vide, l'aria-label n'est pas annoncé par tous les lecteurs. */
248
+ export function Spinner({ grand, label, className, ...reste }: SpinnerProps) {
249
+ return (
250
+ <span
251
+ role={label ? 'status' : undefined}
252
+ aria-hidden={label ? undefined : true}
253
+ className={cx('zv-spinner', grand && 'zv-spinner--lg', className)}
254
+ {...reste}
255
+ >
256
+ {label && <span className="zv-sr-only">{label}</span>}
257
+ </span>
258
+ )
259
+ }
260
+
261
+ /* ─── Squelette ────────────────────────────────────────────────────── */
262
+
263
+ export interface SkeletonProps extends Div {
264
+ /** Une ligne de texte (hauteur d'un cadratin) plutôt qu'un bloc libre. */
265
+ texte?: boolean
266
+ /** La géométrie vient du CONSOMMATEUR : un squelette qui invente ses
267
+ * dimensions ne ressemble jamais au contenu qu'il annonce. */
268
+ largeur?: number | string
269
+ hauteur?: number | string
270
+ className?: string
271
+ }
272
+
273
+ /** Il ANNONCE une structure connue (la carte, la ligne qui va venir) — pour
274
+ * un contenu dont la forme est inconnue, c'est `Loading` qui dit ce cas.
275
+ * Le squelette est masqué aux lecteurs d'écran : c'est la zone qui charge
276
+ * qui porte aria-busy, pas chaque os. */
277
+ export function Skeleton({ texte, largeur, hauteur, className, style, ...reste }: SkeletonProps) {
278
+ return (
279
+ <div
280
+ aria-hidden="true"
281
+ className={cx('zv-skeleton', texte && 'zv-skeleton--text', className)}
282
+ style={{ width: largeur, height: hauteur, ...style }}
283
+ {...reste}
284
+ />
285
+ )
286
+ }
287
+
288
+ /* ─── Infobulle ────────────────────────────────────────────────────── */
289
+
290
+ export interface TooltipProps extends Span {
291
+ /** Le complément — un raccourci, une date exacte, une unité. JAMAIS le
292
+ * nom de la commande : lui vit sur la cible (libellé ou aria-label), une
293
+ * infobulle n'existe pas au doigt. */
294
+ contenu: ReactNode
295
+ /** Identifiant EXPLICITE, à répéter en `aria-describedby` sur la cible —
296
+ * le contrat du couple htmlFor/id de Field, pour la même raison : pas de
297
+ * useId, le composant reste rendu au serveur. */
298
+ id?: string
299
+ /** Sous la cible, pour les rares cas où le haut est occupé. */
300
+ bas?: boolean
301
+ className?: string
302
+ /** La cible. Elle doit être FOCALISABLE (un bouton, un lien) : c'est son
303
+ * focus qui montre l'infobulle au clavier. */
304
+ children?: ReactNode
305
+ }
306
+
307
+ /** Échap ferme la bulle jusqu'au blur (WCAG 1.4.13) : la classe est posée
308
+ * sur le nœud sans état React — le composant reste rendu au serveur, et
309
+ * sans script la bulle reste simplement au comportement survol/focus. */
310
+ export function Tooltip({ contenu, id, bas, className, children, ...reste }: TooltipProps) {
311
+ return (
312
+ <span
313
+ className={cx('zv-tooltip-hit', className)}
314
+ onKeyDown={(e) => {
315
+ if (e.key === 'Escape') e.currentTarget.classList.add('zv-tooltip-hit--off')
316
+ }}
317
+ onBlur={(e) => e.currentTarget.classList.remove('zv-tooltip-hit--off')}
318
+ onMouseLeave={(e) => e.currentTarget.classList.remove('zv-tooltip-hit--off')}
319
+ {...reste}
320
+ >
321
+ {children}
322
+ <span role="tooltip" id={id} className={cx('zv-tooltip', bas && 'zv-tooltip--bas')}>
323
+ {contenu}
324
+ </span>
325
+ </span>
326
+ )
327
+ }
package/react/forms.tsx CHANGED
@@ -13,6 +13,7 @@
13
13
 
14
14
  import type {
15
15
  ButtonHTMLAttributes,
16
+ CSSProperties,
16
17
  HTMLAttributes,
17
18
  InputHTMLAttributes,
18
19
  ReactNode,
@@ -381,3 +382,92 @@ export function DropInput({
381
382
  }) {
382
383
  return <input type="file" className={cx('zv-drop__input', className)} {...reste} />
383
384
  }
385
+
386
+ /* ─── Champ à affixe ───────────────────────────────────────────────── */
387
+
388
+ export interface InputAffixProps
389
+ extends Omit<HTMLAttributes<HTMLDivElement>, 'className'> {
390
+ /** Préfixe, en mono label : « € », « HT ». Une unité est un libellé, pas
391
+ * une donnée — elle ne se saisit pas, elle qualifie ce qui est saisi. */
392
+ avant?: ReactNode
393
+ /** Suffixe : « JOURS », « / MOIS », « .zevra.tech ». */
394
+ apres?: ReactNode
395
+ /** Identifiants EXPLICITES des affixes, à citer dans `aria-describedby`
396
+ * sur le champ intérieur — le contrat htmlFor/id de `Field`, pour la
397
+ * même raison : pas de useId, le composant reste rendu au serveur.
398
+ * ⚠️ Sans ce câblage, un lecteur d'écran en mode formulaire entend
399
+ * « 1200 » sans jamais apprendre qu'il s'agit d'euros HT — exactement le
400
+ * défaut que le composant existe pour corriger. */
401
+ idAvant?: string
402
+ idApres?: string
403
+ className?: string
404
+ /** Le champ lui-même — un `<Input nu>` : c'est le CADRE qui fait le champ,
405
+ * un `<Input>` bordé dessinerait deux cadres imbriqués. */
406
+ children?: ReactNode
407
+ }
408
+
409
+ /** Cadre à affixe : « 30 », seul dans un champ, ne dit pas s'il s'agit de
410
+ * jours ou d'euros. Même grammaire que `Search` (cadre + champ nu) ; à ne
411
+ * pas employer quand l'unité est déjà dans l'intitulé du champ — deux fois
412
+ * la même unité se lisent comme deux informations. */
413
+ export function InputAffix({
414
+ avant,
415
+ apres,
416
+ idAvant,
417
+ idApres,
418
+ className,
419
+ children,
420
+ ...reste
421
+ }: InputAffixProps) {
422
+ return (
423
+ <div className={cx('zv-affix', className)} {...reste}>
424
+ {avant != null && (
425
+ <span className="zv-affix__add" id={idAvant}>
426
+ {avant}
427
+ </span>
428
+ )}
429
+ {children}
430
+ {apres != null && (
431
+ <span className="zv-affix__add" id={idApres}>
432
+ {apres}
433
+ </span>
434
+ )}
435
+ </div>
436
+ )
437
+ }
438
+
439
+ /* ─── Curseur ──────────────────────────────────────────────────────── */
440
+
441
+ export interface RangeProps
442
+ extends Omit<InputHTMLAttributes<HTMLInputElement>, 'className' | 'type'> {
443
+ className?: string
444
+ }
445
+
446
+ /** Curseur natif habillé (`<input type="range">`) : clavier, annonce et
447
+ * bornes viennent du navigateur. Le remplissage de la piste est calculé
448
+ * ici depuis value/min/max — une géométrie, comme la largeur de
449
+ * `Progress`, pas une valeur de charte. En NON contrôlé, il reflète la
450
+ * valeur initiale et ne suit plus le doigt : contrôler le champ dès que le
451
+ * remplissage doit suivre.
452
+ * ⚠️ Toujours accompagné d'un intitulé (`Field`) ET d'une valeur affichée
453
+ * en clair : un curseur seul ne dit ni ce qu'il règle, ni sur combien. */
454
+ export function Range({ className, style, min, max, value, defaultValue, ...reste }: RangeProps) {
455
+ const borneMin = Number(min ?? 0)
456
+ const borneMax = Number(max ?? 100)
457
+ const courant = Number(value ?? defaultValue ?? NaN)
458
+ const part = Number.isNaN(courant)
459
+ ? 0.5
460
+ : Math.min(1, Math.max(0, (courant - borneMin) / (borneMax - borneMin || 1)))
461
+ return (
462
+ <input
463
+ type="range"
464
+ min={min}
465
+ max={max}
466
+ value={value}
467
+ defaultValue={defaultValue}
468
+ className={cx('zv-range', className)}
469
+ style={{ ...style, '--zv-range-pos': `${part * 100}%` } as CSSProperties}
470
+ {...reste}
471
+ />
472
+ )
473
+ }
@@ -4,28 +4,45 @@
4
4
  export type IconId =
5
5
  | 'accueil'
6
6
  | 'activite'
7
+ | 'aide'
7
8
  | 'app-couches'
8
9
  | 'archiver'
10
+ | 'attention'
11
+ | 'balance'
9
12
  | 'base-de-donnees'
10
13
  | 'bouclier-verifie'
11
14
  | 'cadenas'
12
15
  | 'cadrer'
16
+ | 'calendrier'
13
17
  | 'chevron-bas'
14
18
  | 'chevron-droit'
15
19
  | 'chevron-gauche'
20
+ | 'chevron-haut'
16
21
  | 'coche'
17
22
  | 'copier'
18
23
  | 'corbeille'
24
+ | 'courriel'
19
25
  | 'cube'
20
26
  | 'deconnexion'
21
27
  | 'depot-fichier'
28
+ | 'diplome'
22
29
  | 'dossier'
23
30
  | 'eclair'
31
+ | 'envoyer'
32
+ | 'epingle'
24
33
  | 'equipe'
25
34
  | 'etincelles'
35
+ | 'etoile'
26
36
  | 'fermer'
37
+ | 'fichier'
38
+ | 'filtre'
39
+ | 'globe'
40
+ | 'graphique'
27
41
  | 'horloge'
42
+ | 'imprimante'
43
+ | 'info'
28
44
  | 'lien-externe'
45
+ | 'livre-ouvert'
29
46
  | 'menu'
30
47
  | 'moins'
31
48
  | 'oeil'
@@ -35,33 +52,54 @@ export type IconId =
35
52
  | 'reglages'
36
53
  | 'reseau'
37
54
  | 'taches'
55
+ | 'telecharger'
56
+ | 'telephone'
57
+ | 'tri'
38
58
  | 'trois-points'
59
+ | 'utilisateur'
39
60
 
40
61
  export const CORPS: Record<IconId, string> = {
41
62
  'accueil': "<path d=\"M4 10.67 L12 4 L20 10.67 V20 H4 Z\" /><path d=\"M9.33 20 V13.33 H14.67 V20\" />",
42
63
  'activite': "<path d=\"M3.33 12 H8 L10.67 5.33 L14 18.67 L16 12 H20.67\" />",
64
+ 'aide': "<circle cx=\"12\" cy=\"12\" r=\"8.67\" /><path d=\"M9.33 9.33 A2.67 2.67 0 0 1 14.53 10.13 C14.53 11.87 12 12.27 12 14\" /><circle cx=\"12\" cy=\"17.07\" r=\"0.8\" fill=\"currentColor\" stroke=\"none\" />",
43
65
  'app-couches': "<path d=\"M12 2.67 L20 6.67 L12 10.67 L4 6.67 Z\" /><path d=\"M4 12 L12 16 L20 12\" /><path d=\"M4 17.33 L12 21.33 L20 17.33\" />",
44
66
  'archiver': "<rect x=\"3.33\" y=\"4\" width=\"17.33\" height=\"4.67\" rx=\"1.07\" /><path d=\"M5.33 8.67 V18.93 A1.07 1.07 0 0 0 6.4 20 H17.6 A1.07 1.07 0 0 0 18.67 18.93 V8.67\" /><path d=\"M9.33 12.67 H14.67\" />",
67
+ 'attention': "<path d=\"M10.67 3.87 L2.53 17.87 A1.55 1.55 0 0 0 3.87 20.19 H20.13 A1.55 1.55 0 0 0 21.47 17.87 L13.33 3.87 A1.54 1.54 0 0 0 10.67 3.87 Z\" /><path d=\"M12 9.33 V13.33\" /><circle cx=\"12\" cy=\"16.53\" r=\"0.8\" fill=\"currentColor\" stroke=\"none\" />",
68
+ 'balance': "<path d=\"M12 3.33 V20.67\" /><path d=\"M8.67 20.67 H15.33\" /><path d=\"M5.33 6 H18.67\" /><path d=\"M6.67 6 L3.33 13.33\" /><path d=\"M6.67 6 L10 13.33\" /><path d=\"M3.33 13.33 A3.33 3.33 0 0 0 10 13.33\" /><path d=\"M17.33 6 L14 13.33\" /><path d=\"M17.33 6 L20.67 13.33\" /><path d=\"M14 13.33 A3.33 3.33 0 0 0 20.67 13.33\" />",
45
69
  'base-de-donnees': "<ellipse cx=\"12\" cy=\"6\" rx=\"7.33\" ry=\"2.67\" /><path d=\"M4.67 6 V12 C4.67 13.47 7.87 14.67 12 14.67 C16.13 14.67 19.33 13.47 19.33 12 V6\" /><path d=\"M4.67 12 V18 C4.67 19.47 7.87 20.67 12 20.67 C16.13 20.67 19.33 19.47 19.33 18 V12\" />",
46
70
  'bouclier-verifie': "<path d=\"M12 2.67 L20 6 V12 C20 16.67 16.67 19.73 12 21.33 C7.33 19.73 4 16.67 4 12 V6 Z\" /><path d=\"M8.67 12 L11.07 14.4 L15.33 10\" />",
47
71
  'cadenas': "<rect x=\"4.67\" y=\"10.67\" width=\"14.67\" height=\"9.33\" rx=\"1.6\" /><path d=\"M8 10.67 V7.33 A4 4 0 0 1 16 7.33 V10.67\" /><circle cx=\"12\" cy=\"15.33\" r=\"1.33\" />",
48
72
  'cadrer': "<path d=\"M4 8 V4 H8\" /><path d=\"M20 8 V4 H16\" /><path d=\"M4 16 V20 H8\" /><path d=\"M20 16 V20 H16\" />",
73
+ 'calendrier': "<rect x=\"3.33\" y=\"4.67\" width=\"17.33\" height=\"16\" rx=\"1.33\" /><path d=\"M3.33 9.33 H20.67\" /><path d=\"M8 2.67 V6.67\" /><path d=\"M16 2.67 V6.67\" />",
49
74
  'chevron-bas': "<path d=\"M6.67 9.33 L12 14.67 L17.33 9.33\" />",
50
75
  'chevron-droit': "<path d=\"M9.33 4 L17.33 12 L9.33 20\" />",
51
76
  'chevron-gauche': "<path d=\"M14.67 4 L6.67 12 L14.67 20\" />",
77
+ 'chevron-haut': "<path d=\"M6.67 14.67 L12 9.33 L17.33 14.67\" />",
52
78
  'coche': "<path d=\"M5 12.4 L9.4 16.8 L19 6.8\" />",
53
79
  'copier': "<rect x=\"4\" y=\"4\" width=\"12\" height=\"12\" rx=\"1.6\" /><path d=\"M8 16 V18.93 A1.07 1.07 0 0 0 9.07 20 H18.93 A1.07 1.07 0 0 0 20 18.93 V9.07 A1.07 1.07 0 0 0 18.93 8 H16\" />",
54
80
  'corbeille': "<path d=\"M4 6.67 H20\" /><path d=\"M9.33 6.67 V4.67 A1.33 1.33 0 0 1 10.67 3.33 H13.33 A1.33 1.33 0 0 1 14.67 4.67 V6.67\" /><path d=\"M6 6.67 L6.93 18.93 A1.33 1.33 0 0 0 8.27 20 H15.73 A1.33 1.33 0 0 0 17.07 18.93 L18 6.67\" /><path d=\"M10 10.67 V16.67\" /><path d=\"M14 10.67 V16.67\" />",
81
+ 'courriel': "<rect x=\"2.67\" y=\"5.33\" width=\"18.67\" height=\"13.33\" rx=\"1.33\" /><path d=\"M2.67 7.33 L12 13.33 L21.33 7.33\" />",
55
82
  'cube': "<path d=\"M3.33 8 L12 4 L20.67 8 V16 L12 20 L3.33 16 Z\" /><path d=\"M3.33 8 L12 12 L20.67 8\" /><path d=\"M12 12 V20\" />",
56
83
  'deconnexion': "<path d=\"M14.67 4 H5.33 V20 H14.67\" /><path d=\"M10.67 12 H20\" /><path d=\"M16.67 8 L20.67 12 L16.67 16\" />",
57
84
  'depot-fichier': "<path d=\"M12 3.2 V14.4\" /><polyline points=\"7.6,10 12,14.4 16.4,10\" /><path d=\"M4 16.8 v2.4 a1.6 1.6 0 0 0 1.6 1.6 h12.8 a1.6 1.6 0 0 0 1.6-1.6 v-2.4\" />",
85
+ 'diplome': "<path d=\"M2.67 9.33 L12 4.67 L21.33 9.33 L12 14 Z\" /><path d=\"M6.67 11.6 V16 C6.67 17.47 9.07 18.67 12 18.67 C14.93 18.67 17.33 17.47 17.33 16 V11.6\" /><path d=\"M21.33 9.33 V14.67\" />",
58
86
  'dossier': "<path d=\"M1.41 6.64a1.69 1.69 0 0 1 1.69-1.69h4.8l2.12 2.4h10.87a1.69 1.69 0 0 1 1.69 1.69v9.88a1.69 1.69 0 0 1-1.69 1.69H3.11A1.69 1.69 0 0 1 1.41 20.33V6.64Z\" />",
59
87
  'eclair': "<path d=\"M13.33 2.67 L5.33 13.33 H12 L10.67 21.33 L18.67 10.67 H12 Z\" />",
88
+ 'envoyer': "<path d=\"M21.33 2.67 L10.67 13.33\" /><path d=\"M21.33 2.67 L14.67 21.33 L10.67 13.33 L2.67 9.33 Z\" />",
89
+ 'epingle': "<path d=\"M12 21.33 C7.56 16.89 5.33 13.42 5.33 10.67 A6.67 6.67 0 0 1 18.67 10.67 C18.67 13.42 16.44 16.89 12 21.33 Z\" /><circle cx=\"12\" cy=\"10.67\" r=\"2.4\" />",
60
90
  'equipe': "<circle cx=\"9.33\" cy=\"8\" r=\"3.33\" /><path d=\"M3.33 20 C3.33 15.33 6 13.33 9.33 13.33 C12.67 13.33 15.33 15.33 15.33 20\" /><path d=\"M16 5.33 A2.93 2.93 0 0 1 16 11.07\" /><path d=\"M17.33 13.6 C19.73 14.27 20.67 16.4 20.67 20\" />",
61
91
  'etincelles': "<path d=\"M12 2.67 L13.87 8.8 L20 10.67 L13.87 12.53 L12 18.67 L10.13 12.53 L4 10.67 L10.13 8.8 Z\" /><path d=\"M19.33 16.67 L20 18.67 L22 19.33 L20 20 L19.33 22 L18.67 20 L16.67 19.33 L18.67 18.67 Z\" />",
92
+ 'etoile': "<path d=\"M12 3.33 L14.67 8.53 L20.4 9.37 L16.2 13.4 L17.2 19.07 L12 16.4 L6.8 19.07 L7.8 13.4 L3.6 9.37 L9.33 8.53 Z\" />",
62
93
  'fermer': "<path d=\"M5.33 5.33 L18.67 18.67\" /><path d=\"M18.67 5.33 L5.33 18.67\" />",
94
+ 'fichier': "<path d=\"M13.33 2.67 H6.67 A1.33 1.33 0 0 0 5.33 4 V20 A1.33 1.33 0 0 0 6.67 21.33 H17.33 A1.33 1.33 0 0 0 18.67 20 V8 Z\" /><path d=\"M13.33 2.67 V8 H18.67\" />",
95
+ 'filtre': "<path d=\"M2.67 4 H21.33 L14.67 12.27 V18.67 L9.33 21.33 V12.27 Z\" />",
96
+ 'globe': "<circle cx=\"12\" cy=\"12\" r=\"8.67\" /><path d=\"M3.33 12 H20.67\" /><path d=\"M12 3.33 C14.22 5.64 15.47 8.71 15.47 12 C15.47 15.29 14.22 18.36 12 20.67 C9.78 18.36 8.53 15.29 8.53 12 C8.53 8.71 9.78 5.64 12 3.33 Z\" />",
97
+ 'graphique': "<path d=\"M4 4 V20 H20\" /><path d=\"M8.67 20 V12.67\" /><path d=\"M12.67 20 V8\" /><path d=\"M16.67 20 V15.33\" />",
63
98
  'horloge': "<circle cx=\"12\" cy=\"12\" r=\"8\" /><path d=\"M12 6.67 V12 L15.33 14\" />",
99
+ 'imprimante': "<path d=\"M6.67 8 V3.33 H17.33 V8\" /><path d=\"M6.67 17.33 H4.67 A1.33 1.33 0 0 1 3.33 16 V10.67 A1.33 1.33 0 0 1 4.67 9.33 H19.33 A1.33 1.33 0 0 1 20.67 10.67 V16 A1.33 1.33 0 0 1 19.33 17.33 H17.33\" /><rect x=\"6.67\" y=\"13.33\" width=\"10.67\" height=\"7.33\" rx=\"0.8\" />",
100
+ 'info': "<circle cx=\"12\" cy=\"12\" r=\"8.67\" /><path d=\"M12 11.33 V16\" /><circle cx=\"12\" cy=\"8\" r=\"0.8\" fill=\"currentColor\" stroke=\"none\" />",
64
101
  'lien-externe': "<path d=\"M10.67 4 H4 V20 H20 V13.33\" /><path d=\"M14.67 4 H20 V9.33\" /><path d=\"M20 4 L12 12\" />",
102
+ 'livre-ouvert': "<path d=\"M2.67 5.33 C5.33 4.67 7.78 4.89 12 6.67 C16.22 4.89 18.67 4.67 21.33 5.33 V18 C18.67 17.33 16.22 17.56 12 19.33 C7.78 17.56 5.33 17.33 2.67 18 Z\" /><path d=\"M12 6.67 V19.33\" />",
65
103
  'menu': "<path d=\"M0 5.33 H24\" /><path d=\"M0 12 H24\" /><path d=\"M0 18.67 H24\" />",
66
104
  'moins': "<path d=\"M4 12 H20\" />",
67
105
  'oeil': "<path d=\"M2 12 C5.33 6.67 8.67 4.67 12 4.67 C15.33 4.67 18.67 6.67 22 12 C18.67 17.33 15.33 19.33 12 19.33 C8.67 19.33 5.33 17.33 2 12 Z\" /><circle cx=\"12\" cy=\"12\" r=\"2.67\" />",
@@ -71,7 +109,11 @@ export const CORPS: Record<IconId, string> = {
71
109
  'reglages': "<circle cx=\"12\" cy=\"12\" r=\"2.93\" /><path d=\"M12 2 V4.67 M12 19.33 V22 M2 12 H4.67 M19.33 12 H22 M4.8 4.8 L6.67 6.67 M17.33 17.33 L19.2 19.2 M19.2 4.8 L17.33 6.67 M6.67 17.33 L4.8 19.2\" />",
72
110
  'reseau': "<circle cx=\"12\" cy=\"12\" r=\"8\" /><path d=\"M4 12 H20\" /><path d=\"M12 4 C14.67 8 14.67 16 12 20\" /><path d=\"M12 4 C9.33 8 9.33 16 12 20\" />",
73
111
  'taches': "<path d=\"M4.67 6.67 L6.67 8.67 L10 5.33\" /><path d=\"M4.67 16 L6.67 18 L10 14.67\" /><path d=\"M13.33 7.33 H20\" /><path d=\"M13.33 16.67 H20\" />",
112
+ 'telecharger': "<path d=\"M12 3.33 V15.33\" /><polyline points=\"6.67,10 12,15.33 17.33,10\" /><path d=\"M4.67 20.67 H19.33\" />",
113
+ 'telephone': "<path d=\"M20.67 16.93 V19.33 A1.6 1.6 0 0 1 18.93 20.93 C16.13 20.67 13.47 19.67 11.2 18.13 C9.09 16.71 7.29 14.91 5.87 12.8 C4.33 10.52 3.33 7.85 3.07 5.05 A1.6 1.6 0 0 1 4.67 3.33 H7.07 A1.6 1.6 0 0 1 8.67 4.71 C8.79 5.83 9.04 6.93 9.4 7.97 A1.6 1.6 0 0 1 9.04 9.65 L8 10.69 C9.32 13.01 11.03 14.71 13.33 16 L14.37 14.96 A1.6 1.6 0 0 1 16.05 14.6 C17.09 14.96 18.19 15.21 19.31 15.33 A1.6 1.6 0 0 1 20.67 16.93 Z\" />",
114
+ 'tri': "<path d=\"M8 4 V20\" /><path d=\"M4.67 7.33 L8 4 L11.33 7.33\" /><path d=\"M16 20 V4\" /><path d=\"M12.67 16.67 L16 20 L19.33 16.67\" />",
74
115
  'trois-points': "<circle cx=\"5.33\" cy=\"12\" r=\"1.33\" fill=\"currentColor\" stroke=\"none\" /><circle cx=\"12\" cy=\"12\" r=\"1.33\" fill=\"currentColor\" stroke=\"none\" /><circle cx=\"18.67\" cy=\"12\" r=\"1.33\" fill=\"currentColor\" stroke=\"none\" />",
116
+ 'utilisateur': "<circle cx=\"12\" cy=\"8\" r=\"3.33\" /><path d=\"M5.33 20 C5.33 15.33 8.33 13.33 12 13.33 C15.67 13.33 18.67 15.33 18.67 20\" />",
75
117
  }
76
118
 
77
119
  /** Nom lisible de chaque icône — sert de titre accessible par défaut à
@@ -79,28 +121,45 @@ export const CORPS: Record<IconId, string> = {
79
121
  export const NOMS: Record<IconId, string> = {
80
122
  'accueil': "Accueil",
81
123
  'activite': "Activité (pouls)",
124
+ 'aide': "Aide",
82
125
  'app-couches': "Application (couches)",
83
126
  'archiver': "Archiver",
127
+ 'attention': "Attention (avertissement)",
128
+ 'balance': "Balance (justice)",
84
129
  'base-de-donnees': "Base de données",
85
130
  'bouclier-verifie': "Bouclier vérifié (authentification)",
86
131
  'cadenas': "Cadenas (secret)",
87
132
  'cadrer': "Cadrer (ajuster à la vue)",
133
+ 'calendrier': "Calendrier",
88
134
  'chevron-bas': "Chevron bas",
89
135
  'chevron-droit': "Chevron droit",
90
136
  'chevron-gauche': "Chevron gauche",
137
+ 'chevron-haut': "Chevron haut",
91
138
  'coche': "Coche",
92
139
  'copier': "Copier",
93
140
  'corbeille': "Corbeille (supprimer)",
141
+ 'courriel': "Courriel",
94
142
  'cube': "Cube (stockage objet)",
95
143
  'deconnexion': "Déconnexion",
96
144
  'depot-fichier': "Dépôt de fichier (zone de glisser-déposer)",
145
+ 'diplome': "Diplôme (Campus)",
97
146
  'dossier': "Dossier",
98
147
  'eclair': "Éclair (action rapide)",
148
+ 'envoyer': "Envoyer",
149
+ 'epingle': "Épingle (lieu)",
99
150
  'equipe': "Équipe (membres)",
100
151
  'etincelles': "Étincelles (IA)",
152
+ 'etoile': "Étoile (favori)",
101
153
  'fermer': "Fermer",
154
+ 'fichier': "Fichier (document)",
155
+ 'filtre': "Filtre",
156
+ 'globe': "Globe (international)",
157
+ 'graphique': "Graphique (barres)",
102
158
  'horloge': "Horloge (intégration continue)",
159
+ 'imprimante': "Imprimante",
160
+ 'info': "Information",
103
161
  'lien-externe': "Lien externe",
162
+ 'livre-ouvert': "Livre ouvert (formation)",
104
163
  'menu': "Menu (hamburger)",
105
164
  'moins': "Moins",
106
165
  'oeil': "Œil (afficher)",
@@ -110,5 +169,9 @@ export const NOMS: Record<IconId, string> = {
110
169
  'reglages': "Réglages",
111
170
  'reseau': "Réseau (entrée / globe)",
112
171
  'taches': "Tâches (liste à cocher)",
172
+ 'telecharger': "Télécharger",
173
+ 'telephone': "Téléphone",
174
+ 'tri': "Tri",
113
175
  'trois-points': "Trois points (menu contextuel)",
176
+ 'utilisateur': "Utilisateur",
114
177
  }
package/react/icon.tsx CHANGED
@@ -1,4 +1,4 @@
1
- // Icônes — jeu de 34, tracées, sur grille de 24.
1
+ // Icônes — jeu de 55, tracées, sur grille de 24.
2
2
  //
3
3
  // Le corps du tracé est INLINÉ (voir scripts/build-icon-react.mjs) : pas de
4
4
  // <use href="…sprite.svg">, donc rien à résoudre côté bundler et rien à
@@ -12,7 +12,7 @@ import { CORPS, NOMS, type IconId } from './icon-data.js'
12
12
  export type { IconId }
13
13
  export { NOMS as NOMS_ICONES }
14
14
 
15
- /** Les 34 identifiants, triés — pour construire une planche ou un sélecteur. */
15
+ /** Les 55 identifiants, triés — pour construire une planche ou un sélecteur. */
16
16
  export const ICONES = Object.keys(CORPS) as IconId[]
17
17
 
18
18
  export interface IconProps
package/react/layout.tsx CHANGED
@@ -346,3 +346,42 @@ export function OnlyDesktop({ className, ...reste }: Div & { className?: string
346
346
  export function OnlyMobile({ className, ...reste }: Div & { className?: string }) {
347
347
  return <div className={cx('zv-only-mobile', className)} {...reste} />
348
348
  }
349
+
350
+ /* ─── Bandeau d'annonce ────────────────────────────────────────────── */
351
+
352
+ /** La bande AU-DESSUS de la barre de navigation : annonce de version,
353
+ * information réglementaire. UN SEUL par page, et jamais pour du marketing
354
+ * permanent — un bandeau qui ne part pas devient un bourrelet de chrome. */
355
+ export function Banner({ className, ...reste }: Div & { className?: string }) {
356
+ return <div className={cx('zv-banner', className)} {...reste} />
357
+ }
358
+
359
+ /* ─── Carrousel ────────────────────────────────────────────────────── */
360
+
361
+ export interface CarouselProps extends Div {
362
+ /** Ce que la rangée contient (« Nos produits ») : une région qui défile
363
+ * sans nom est une énigme au lecteur d'écran. */
364
+ label: string
365
+ className?: string
366
+ }
367
+
368
+ /** Défilement natif scroll-snap, AUCUN JavaScript : pas de flèches, pas de
369
+ * points. Le conteneur est focalisable — c'est lui que les flèches du
370
+ * clavier font défiler. La largeur des éléments vient du consommateur.
371
+ * ⚠️ Un carrousel reste un aveu d'espace : la règle de variété lui préfère
372
+ * une grille tant que le contenu tient. */
373
+ export function Carousel({ label, className, ...reste }: CarouselProps) {
374
+ return (
375
+ <div
376
+ role="region"
377
+ aria-label={label}
378
+ tabIndex={0}
379
+ className={cx('zv-carousel', className)}
380
+ {...reste}
381
+ />
382
+ )
383
+ }
384
+
385
+ export function CarouselItem({ className, ...reste }: Div & { className?: string }) {
386
+ return <div className={cx('zv-carousel__item', className)} {...reste} />
387
+ }