@zevra/ui 0.16.0 → 0.20.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 (70) hide show
  1. package/DECISIONS.md +279 -0
  2. package/dist/catalogue.d.ts.map +1 -1
  3. package/dist/catalogue.js +76 -2
  4. package/dist/catalogue.js.map +1 -1
  5. package/dist/content.d.ts +103 -0
  6. package/dist/content.d.ts.map +1 -1
  7. package/dist/content.js +80 -0
  8. package/dist/content.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/forms.d.ts +22 -0
  13. package/dist/forms.d.ts.map +1 -1
  14. package/dist/forms.js +18 -0
  15. package/dist/forms.js.map +1 -1
  16. package/dist/icon-data.d.ts +1 -1
  17. package/dist/icon-data.d.ts.map +1 -1
  18. package/dist/icon-data.js +30 -0
  19. package/dist/icon-data.js.map +1 -1
  20. package/dist/layout.d.ts +11 -5
  21. package/dist/layout.d.ts.map +1 -1
  22. package/dist/layout.js.map +1 -1
  23. package/dist/typography.d.ts +8 -3
  24. package/dist/typography.d.ts.map +1 -1
  25. package/dist/typography.js +6 -0
  26. package/dist/typography.js.map +1 -1
  27. package/icons/index.json +90 -0
  28. package/icons/sprite.svg +15 -0
  29. package/logos/favicon/campus/apple-touch-icon.png +0 -0
  30. package/logos/favicon/campus/favicon.ico +0 -0
  31. package/logos/favicon/campus/icon-192.png +0 -0
  32. package/logos/favicon/campus/icon-512.png +0 -0
  33. package/logos/favicon/lexform/apple-touch-icon.png +0 -0
  34. package/logos/favicon/lexform/favicon.ico +0 -0
  35. package/logos/favicon/lexform/icon-192.png +0 -0
  36. package/logos/favicon/lexform/icon-512.png +0 -0
  37. package/logos/favicon/mcp/apple-touch-icon.png +0 -0
  38. package/logos/favicon/mcp/favicon.ico +0 -0
  39. package/logos/favicon/mcp/icon-192.png +0 -0
  40. package/logos/favicon/mcp/icon-512.png +0 -0
  41. package/logos/favicon/memoire/apple-touch-icon.png +0 -0
  42. package/logos/favicon/memoire/favicon.ico +0 -0
  43. package/logos/favicon/memoire/icon-192.png +0 -0
  44. package/logos/favicon/memoire/icon-512.png +0 -0
  45. package/logos/favicon/plume/apple-touch-icon.png +0 -0
  46. package/logos/favicon/plume/favicon.ico +0 -0
  47. package/logos/favicon/plume/icon-192.png +0 -0
  48. package/logos/favicon/plume/icon-512.png +0 -0
  49. package/logos/favicon/supernovia/apple-touch-icon.png +0 -0
  50. package/logos/favicon/supernovia/favicon.ico +0 -0
  51. package/logos/favicon/supernovia/icon-192.png +0 -0
  52. package/logos/favicon/supernovia/icon-512.png +0 -0
  53. package/logos/favicon/zevra/apple-touch-icon.png +0 -0
  54. package/logos/favicon/zevra/favicon.ico +0 -0
  55. package/logos/favicon/zevra/icon-192.png +0 -0
  56. package/logos/favicon/zevra/icon-512.png +0 -0
  57. package/package.json +1 -1
  58. package/react/catalogue.tsx +109 -3
  59. package/react/content.tsx +272 -1
  60. package/react/css-inline.ts +1 -1
  61. package/react/forms.tsx +53 -0
  62. package/react/icon-data.ts +45 -0
  63. package/react/layout.tsx +11 -4
  64. package/react/typography.tsx +13 -3
  65. package/src/base.css +7 -0
  66. package/src/components.css +294 -8
  67. package/src/forms.css +65 -2
  68. package/src/icons.css +20 -0
  69. package/src/layout.css +16 -1
  70. package/src/navigation.css +20 -0
package/react/forms.tsx CHANGED
@@ -21,6 +21,7 @@ import type {
21
21
  TextareaHTMLAttributes,
22
22
  } from 'react'
23
23
  import { cx } from './cx.js'
24
+ import { Icon } from './icon.js'
24
25
 
25
26
  /* ─── Groupe de champ ──────────────────────────────────────────────── */
26
27
 
@@ -471,3 +472,55 @@ export function Range({ className, style, min, max, value, defaultValue, ...rest
471
472
  />
472
473
  )
473
474
  }
475
+
476
+ export interface InputPasswordProps
477
+ extends Omit<InputHTMLAttributes<HTMLInputElement>, 'className' | 'type'> {
478
+ /** Le mot de passe est-il en clair ? L'état est TENU PAR L'APPELANT. */
479
+ visible?: boolean
480
+ /** Bascule demandée. Sans lui, le bouton n'est pas rendu du tout. */
481
+ onBascule?: () => void
482
+ className?: string
483
+ }
484
+
485
+ /**
486
+ * Champ de saisie masquée, avec sa bascule d'affichage.
487
+ *
488
+ * C'est le seul endroit de l'interface où l'on tape sans voir ce qu'on écrit :
489
+ * sur un mot de passe long, saisi au clavier d'un téléphone, la bascule évite
490
+ * l'échec silencieux et la réinitialisation qui suit.
491
+ *
492
+ * ⚠️ Sans état, comme tout ce fichier : `visible` et `onBascule` viennent de
493
+ * l'appelant, qui est déjà un composant client puisqu'il tient la valeur du
494
+ * champ. Le paquet ne tire pas React côté navigateur pour un champ.
495
+ *
496
+ * Le libellé du bouton change avec l'état — une icône seule ne dit pas ce qui
497
+ * va se passer — et `aria-pressed` l'annonce aux lecteurs d'écran.
498
+ */
499
+ export function InputPassword({
500
+ visible,
501
+ onBascule,
502
+ className,
503
+ ...reste
504
+ }: InputPasswordProps) {
505
+ return (
506
+ <div className={cx('zv-affix', className)}>
507
+ <input
508
+ type={visible ? 'text' : 'password'}
509
+ className="zv-input zv-input--bare"
510
+ {...reste}
511
+ />
512
+ {onBascule && (
513
+ <button
514
+ type="button"
515
+ className="zv-affix__btn"
516
+ onClick={onBascule}
517
+ aria-pressed={visible ?? false}
518
+ aria-label={visible ? 'Masquer le mot de passe' : 'Afficher le mot de passe'}
519
+ title={visible ? 'Masquer le mot de passe' : 'Afficher le mot de passe'}
520
+ >
521
+ <Icon nom={visible ? 'oeil-barre' : 'oeil'} taille="sm" />
522
+ </button>
523
+ )}
524
+ </div>
525
+ )
526
+ }
@@ -8,6 +8,7 @@ export type IconId =
8
8
  | 'app-couches'
9
9
  | 'archiver'
10
10
  | 'attention'
11
+ | 'avancer'
11
12
  | 'balance'
12
13
  | 'base-de-donnees'
13
14
  | 'bouclier-verifie'
@@ -18,6 +19,7 @@ export type IconId =
18
19
  | 'chevron-droit'
19
20
  | 'chevron-gauche'
20
21
  | 'chevron-haut'
22
+ | 'citation'
21
23
  | 'coche'
22
24
  | 'copier'
23
25
  | 'corbeille'
@@ -38,25 +40,38 @@ export type IconId =
38
40
  | 'filtre'
39
41
  | 'globe'
40
42
  | 'graphique'
43
+ | 'gras'
41
44
  | 'horloge'
45
+ | 'image'
42
46
  | 'imprimante'
43
47
  | 'info'
48
+ | 'italique'
49
+ | 'lecture'
44
50
  | 'lien-externe'
51
+ | 'liste-a-puces'
45
52
  | 'livre-ouvert'
46
53
  | 'menu'
47
54
  | 'moins'
55
+ | 'muet'
48
56
  | 'oeil'
57
+ | 'oeil-barre'
58
+ | 'pause'
59
+ | 'plein-ecran'
49
60
  | 'plus'
50
61
  | 'recherche'
51
62
  | 'registre'
52
63
  | 'reglages'
64
+ | 'rembobiner'
53
65
  | 'reseau'
66
+ | 'signet'
67
+ | 'son'
54
68
  | 'taches'
55
69
  | 'telecharger'
56
70
  | 'telephone'
57
71
  | 'tri'
58
72
  | 'trois-points'
59
73
  | 'utilisateur'
74
+ | 'video'
60
75
 
61
76
  export const CORPS: Record<IconId, string> = {
62
77
  'accueil': "<path d=\"M4 10.67 L12 4 L20 10.67 V20 H4 Z\" /><path d=\"M9.33 20 V13.33 H14.67 V20\" />",
@@ -65,6 +80,7 @@ export const CORPS: Record<IconId, string> = {
65
80
  '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\" />",
66
81
  'archiver': "<rect x=\"3.33\" y=\"4\" width=\"17.33\" height=\"4.67\" /><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
82
  '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\" /><rect x=\"11.2\" y=\"15.73\" width=\"1.6\" height=\"1.6\" fill=\"currentColor\" stroke=\"none\" />",
83
+ 'avancer': "<path d=\"M4 6 L11 12 L4 18 Z\" fill=\"currentColor\" stroke=\"none\" /><path d=\"M13 6 L20 12 L13 18 Z\" fill=\"currentColor\" stroke=\"none\" />",
68
84
  '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\" />",
69
85
  '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\" />",
70
86
  '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\" />",
@@ -75,6 +91,7 @@ export const CORPS: Record<IconId, string> = {
75
91
  'chevron-droit': "<path d=\"M9.33 4 L17.33 12 L9.33 20\" />",
76
92
  'chevron-gauche': "<path d=\"M14.67 4 L6.67 12 L14.67 20\" />",
77
93
  'chevron-haut': "<path d=\"M6.67 14.67 L12 9.33 L17.33 14.67\" />",
94
+ 'citation': "<path d=\"M4 14 V10 C4 7.8 5.8 6 8 6 V9 C6.9 9 6 9.9 6 11 H9 V14 Z\" /><path d=\"M14 14 V10 C14 7.8 15.8 6 18 6 V9 C16.9 9 16 9.9 16 11 H19 V14 Z\" />",
78
95
  'coche': "<path d=\"M5 12.4 L9.4 16.8 L19 6.8\" />",
79
96
  'copier': "<rect x=\"4\" y=\"4\" width=\"12\" height=\"12\" /><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\" />",
80
97
  '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\" />",
@@ -95,25 +112,38 @@ export const CORPS: Record<IconId, string> = {
95
112
  'filtre': "<path d=\"M2.67 4 H21.33 L14.67 12.27 V18.67 L9.33 21.33 V12.27 Z\" />",
96
113
  '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
114
  '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\" />",
115
+ 'gras': "<path d=\"M7 4 H13 C15.2 4 16.5 5.3 16.5 7.5 C16.5 9.7 15.2 11 13 11 H7 Z\" /><path d=\"M7 11 H14 C16.2 11 17.5 12.5 17.5 15 C17.5 17.5 16.2 19 14 19 H7 Z\" />",
98
116
  'horloge': "<circle cx=\"12\" cy=\"12\" r=\"8\" /><path d=\"M12 6.67 V12 L15.33 14\" />",
117
+ 'image': "<path d=\"M3.5 5 H20.5 V19 H3.5 Z\" /><path d=\"M3.5 16 L9 10.5 L13.5 15 L16 12.5 L20.5 17\" /><path d=\"M15 8.5 H15.01\" />",
99
118
  '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\" />",
100
119
  'info': "<circle cx=\"12\" cy=\"12\" r=\"8.67\" /><path d=\"M12 11.33 V16\" /><rect x=\"11.2\" y=\"7.2\" width=\"1.6\" height=\"1.6\" fill=\"currentColor\" stroke=\"none\" />",
120
+ 'italique': "<path d=\"M10 5 H18\" /><path d=\"M6 19 H14\" /><path d=\"M14.5 5 L9.5 19\" />",
121
+ 'lecture': "<path d=\"M8 5 L19 12 L8 19 Z\" fill=\"currentColor\" stroke=\"none\" />",
101
122
  '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\" />",
123
+ 'liste-a-puces': "<path d=\"M9 6.5 H20\" /><path d=\"M9 12 H20\" /><path d=\"M9 17.5 H20\" /><path d=\"M4 5.5 H6 V7.5 H4 Z\" fill=\"currentColor\" stroke=\"none\" /><path d=\"M4 11 H6 V13 H4 Z\" fill=\"currentColor\" stroke=\"none\" /><path d=\"M4 16.5 H6 V18.5 H4 Z\" fill=\"currentColor\" stroke=\"none\" />",
102
124
  '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\" />",
103
125
  'menu': "<path d=\"M0 5.33 H24\" /><path d=\"M0 12 H24\" /><path d=\"M0 18.67 H24\" />",
104
126
  'moins': "<path d=\"M4 12 H20\" />",
127
+ 'muet': "<path d=\"M4 9.5 H7.5 L12 5.5 V18.5 L7.5 14.5 H4 Z\" /><path d=\"M16 10 L21 14\" /><path d=\"M21 10 L16 14\" />",
105
128
  '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\" />",
129
+ 'oeil-barre': "<path d=\"M3 12 C5.5 7.5 8.6 5.5 12 5.5 C15.4 5.5 18.5 7.5 21 12 C18.5 16.5 15.4 18.5 12 18.5 C8.6 18.5 5.5 16.5 3 12 Z\" /><path d=\"M9.5 12 A2.5 2.5 0 0 1 14.5 12 A2.5 2.5 0 0 1 9.5 12 Z\" /><path d=\"M4 20 L20 4\" />",
130
+ 'pause': "<path d=\"M9 5 H11 V19 H9 Z\" fill=\"currentColor\" stroke=\"none\" /><path d=\"M13 5 H15 V19 H13 Z\" fill=\"currentColor\" stroke=\"none\" />",
131
+ 'plein-ecran': "<path d=\"M4 9 V4 H9\" /><path d=\"M20 9 V4 H15\" /><path d=\"M4 15 V20 H9\" /><path d=\"M20 15 V20 H15\" />",
106
132
  'plus': "<path d=\"M12 4 V20\" /><path d=\"M4 12 H20\" />",
107
133
  'recherche': "<circle cx=\"10.67\" cy=\"10.67\" r=\"6\" /><path d=\"M15.33 15.33 L20 20\" />",
108
134
  'registre': "<rect x=\"3.33\" y=\"5.33\" width=\"17.33\" height=\"5.33\" /><rect x=\"3.33\" y=\"13.33\" width=\"17.33\" height=\"5.33\" /><rect x=\"5.87\" y=\"7.2\" width=\"1.6\" height=\"1.6\" fill=\"currentColor\" stroke=\"none\" /><rect x=\"5.87\" y=\"15.2\" width=\"1.6\" height=\"1.6\" fill=\"currentColor\" stroke=\"none\" />",
109
135
  '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\" />",
136
+ 'rembobiner': "<path d=\"M11 6 L4 12 L11 18 Z\" fill=\"currentColor\" stroke=\"none\" /><path d=\"M20 6 L13 12 L20 18 Z\" fill=\"currentColor\" stroke=\"none\" />",
110
137
  '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\" />",
138
+ 'signet': "<path d=\"M6 4 H18 V20 L12 15.5 L6 20 Z\" />",
139
+ 'son': "<path d=\"M4 9.5 H7.5 L12 5.5 V18.5 L7.5 14.5 H4 Z\" /><path d=\"M16 9 C17.5 10.2 17.5 13.8 16 15\" /><path d=\"M18.8 6.6 C21.4 9 21.4 15 18.8 17.4\" />",
111
140
  '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
141
  '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
142
  '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
143
  '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\" />",
115
144
  'trois-points': "<rect x=\"4\" y=\"10.67\" width=\"2.66\" height=\"2.66\" fill=\"currentColor\" stroke=\"none\" /><rect x=\"10.67\" y=\"10.67\" width=\"2.66\" height=\"2.66\" fill=\"currentColor\" stroke=\"none\" /><rect x=\"17.34\" y=\"10.67\" width=\"2.66\" height=\"2.66\" fill=\"currentColor\" stroke=\"none\" />",
116
145
  '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\" />",
146
+ 'video': "<path d=\"M3.5 6 H15 V18 H3.5 Z\" /><path d=\"M15 10 L20.5 7 V17 L15 14 Z\" />",
117
147
  }
118
148
 
119
149
  /** Nom lisible de chaque icône — sert de titre accessible par défaut à
@@ -125,6 +155,7 @@ export const NOMS: Record<IconId, string> = {
125
155
  'app-couches': "Application (couches)",
126
156
  'archiver': "Archiver",
127
157
  'attention': "Attention (avertissement)",
158
+ 'avancer': "Avancer",
128
159
  'balance': "Balance (justice)",
129
160
  'base-de-donnees': "Base de données",
130
161
  'bouclier-verifie': "Bouclier vérifié (authentification)",
@@ -135,6 +166,7 @@ export const NOMS: Record<IconId, string> = {
135
166
  'chevron-droit': "Chevron droit",
136
167
  'chevron-gauche': "Chevron gauche",
137
168
  'chevron-haut': "Chevron haut",
169
+ 'citation': "Citation",
138
170
  'coche': "Coche",
139
171
  'copier': "Copier",
140
172
  'corbeille': "Corbeille (supprimer)",
@@ -155,23 +187,36 @@ export const NOMS: Record<IconId, string> = {
155
187
  'filtre': "Filtre",
156
188
  'globe': "Globe (international)",
157
189
  'graphique': "Graphique (barres)",
190
+ 'gras': "Gras",
158
191
  'horloge': "Horloge (intégration continue)",
192
+ 'image': "Image",
159
193
  'imprimante': "Imprimante",
160
194
  'info': "Information",
195
+ 'italique': "Italique",
196
+ 'lecture': "Lecture",
161
197
  'lien-externe': "Lien externe",
198
+ 'liste-a-puces': "Liste à puces",
162
199
  'livre-ouvert': "Livre ouvert (formation)",
163
200
  'menu': "Menu (hamburger)",
164
201
  'moins': "Moins",
202
+ 'muet': "Muet",
165
203
  'oeil': "Œil (afficher)",
204
+ 'oeil-barre': "Œil barré",
205
+ 'pause': "Pause",
206
+ 'plein-ecran': "Plein écran",
166
207
  'plus': "Plus",
167
208
  'recherche': "Recherche (loupe)",
168
209
  'registre': "Registre (serveurs empilés)",
169
210
  'reglages': "Réglages",
211
+ 'rembobiner': "Rembobiner",
170
212
  'reseau': "Réseau (entrée / globe)",
213
+ 'signet': "Signet",
214
+ 'son': "Son",
171
215
  'taches': "Tâches (liste à cocher)",
172
216
  'telecharger': "Télécharger",
173
217
  'telephone': "Téléphone",
174
218
  'tri': "Tri",
175
219
  'trois-points': "Trois points (menu contextuel)",
176
220
  'utilisateur': "Utilisateur",
221
+ 'video': "Vidéo",
177
222
  }
package/react/layout.tsx CHANGED
@@ -5,6 +5,7 @@
5
5
  // Tout y est sans état.
6
6
 
7
7
  import type {
8
+ AnchorHTMLAttributes,
8
9
  ButtonHTMLAttributes,
9
10
  CSSProperties,
10
11
  ElementType,
@@ -162,8 +163,14 @@ export function NavLinks({ className, ...reste }: Div & { className?: string })
162
163
  return <div className={cx('zv-nav__links', className)} {...reste} />
163
164
  }
164
165
 
166
+ /** Les composants qui rendent un `<a>` sont typés sur
167
+ * `AnchorHTMLAttributes` et non `HTMLAttributes` : sans quoi ils refusent
168
+ * `target`, `rel`, `download` et `hreflang`. Un lien sortant ne peut alors
169
+ * pas porter `rel="noopener"`, et un lien qui doit s'ouvrir à côté ne le
170
+ * peut pas non plus — une app consommatrice a dû écrire la classe à la main
171
+ * pour cette seule raison. (0.19.0) */
165
172
  export interface NavLinkProps
166
- extends Omit<HTMLAttributes<HTMLAnchorElement>, 'className'> {
173
+ extends Omit<AnchorHTMLAttributes<HTMLAnchorElement>, 'className'> {
167
174
  /** Lien secondaire : connexion, mentions. */
168
175
  discret?: boolean
169
176
  href?: string
@@ -200,7 +207,7 @@ export function NavDrawerLink({
200
207
  courant,
201
208
  className,
202
209
  ...reste
203
- }: Omit<HTMLAttributes<HTMLAnchorElement>, 'className'> & {
210
+ }: Omit<AnchorHTMLAttributes<HTMLAnchorElement>, 'className'> & {
204
211
  courant?: boolean
205
212
  href?: string
206
213
  className?: string
@@ -287,7 +294,7 @@ export function Langue({
287
294
  courante,
288
295
  className,
289
296
  ...reste
290
- }: Omit<HTMLAttributes<HTMLAnchorElement>, 'className'> & {
297
+ }: Omit<AnchorHTMLAttributes<HTMLAnchorElement>, 'className'> & {
291
298
  courante?: boolean
292
299
  href?: string
293
300
  className?: string
@@ -389,7 +396,7 @@ export function RailItem({
389
396
  className,
390
397
  children,
391
398
  ...reste
392
- }: Omit<HTMLAttributes<HTMLAnchorElement>, 'className'> & {
399
+ }: Omit<AnchorHTMLAttributes<HTMLAnchorElement>, 'className'> & {
393
400
  courant?: boolean
394
401
  href: string
395
402
  className?: string
@@ -6,7 +6,12 @@
6
6
  // balise un choix de structure. Les confondre casse le plan du document pour
7
7
  // un lecteur d'écran sans que rien ne se voie à l'écran.
8
8
 
9
- import type { ElementType, HTMLAttributes, ReactNode } from 'react'
9
+ import type {
10
+ AnchorHTMLAttributes,
11
+ ElementType,
12
+ HTMLAttributes,
13
+ ReactNode,
14
+ } from 'react'
10
15
  import { cx } from './cx.js'
11
16
 
12
17
  type Div = Omit<HTMLAttributes<HTMLElement>, 'className'>
@@ -122,12 +127,17 @@ export function Em({
122
127
  )
123
128
  }
124
129
 
130
+ /** Les composants qui rendent un `<a>` sont typés sur
131
+ * `AnchorHTMLAttributes` et non `HTMLAttributes` : sans quoi ils refusent
132
+ * `target`, `rel`, `download` et `hreflang`. Un lien sortant ne peut alors
133
+ * pas porter `rel="noopener"`, et un lien qui doit s'ouvrir à côté ne le
134
+ * peut pas non plus — une app consommatrice a dû écrire la classe à la main
135
+ * pour cette seule raison. (0.19.0) */
125
136
  export function Link({
126
137
  className,
127
138
  ...reste
128
- }: Omit<HTMLAttributes<HTMLAnchorElement>, 'className'> & {
139
+ }: Omit<AnchorHTMLAttributes<HTMLAnchorElement>, 'className'> & {
129
140
  className?: string
130
- href?: string
131
141
  }) {
132
142
  return <a className={cx('zv-link', className)} {...reste} />
133
143
  }
package/src/base.css CHANGED
@@ -160,6 +160,13 @@ a:not([class]):hover {
160
160
  .zv-em {
161
161
  color: var(--accent-deep);
162
162
  font-weight: 400;
163
+ /* Le composant `Em` rend un vrai <em>, que le navigateur penche par
164
+ * défaut — et AUCUNE italique n'est chargée : Outfit n'en a pas, il en
165
+ * fabrique donc une oblique de synthèse. Sur un titre de 76px, ça se voit.
166
+ * La charte ne demande à cette classe qu'un changement de couleur et de
167
+ * graisse ; on remet donc la droite. (`.zv-inset em` portait déjà cette
168
+ * règle : elle vit désormais là où le besoin est structurel.) */
169
+ font-style: normal;
163
170
  }
164
171
 
165
172
  /* Sur fond sombre l'accent ne change pas de couleur mais de graisse. */
@@ -41,10 +41,34 @@
41
41
  box-shadow 160ms cubic-bezier(0.2, 0.7, 0.2, 1);
42
42
  }
43
43
 
44
- .zv-btn {
45
- /* Le sommet du dégradé, relevé dans Main.html et SANS token : écrit une
46
- * seule fois ici, les états de survol le dérivent par color-mix. */
47
- --zv-btn-haut: #2b2bb4;
44
+ /* ─── Le plein ne s'applique QUE si aucune variante n'est posée ────────
45
+ *
46
+ * `zv-btn`, `zv-btn--ghost`, `zv-btn--ink` et `zv-btn--danger` sont quatre
47
+ * classes AUTONOMES et mutuellement exclusives : le socle commun ci-dessus
48
+ * les groupe dans un même sélecteur, ce qui leur donne exactement l'air
49
+ * d'une base et de ses modificateurs. `Button` n'en rend jamais qu'une —
50
+ * mais une app qui écrit ses classes à la main lit du BEM et compose les
51
+ * deux. C'est arrivé (mcp-factory, 0.19.0), et le résultat était vicieux :
52
+ * `.zv-btn:hover` (0,2,0) l'emportait sur `.zv-btn--ghost` (0,1,0), d'où un
53
+ * fond indigo sous un texte encre — illisible, et seulement AU SURVOL, donc
54
+ * invisible en relecture comme en capture.
55
+ *
56
+ * Le `:not(:where(…))` dit ce que le système voulait déjà dire : ces quatre
57
+ * classes s'excluent. `:where()` a une spécificité NULLE — la garde ne
58
+ * change donc aucun arbitrage existant, et une app qui surcharge `.zv-btn`
59
+ * continue de gagner exactement comme avant.
60
+ *
61
+ * ⚠️ À reporter sur toute règle future qui peint le bouton plein. */
62
+
63
+ .zv-btn:not(:where(.zv-btn--ghost, .zv-btn--ink, .zv-btn--danger)) {
64
+ /* Le sommet du dégradé. RELEVÉ dans Main.html à #2b2bb4, mais DÉRIVÉ de
65
+ * --accent depuis la 0.19.0 : la déclaration étant portée par le bouton
66
+ * lui-même, une redéfinition de marque héritée d'un conteneur ne
67
+ * l'atteignait pas — un cabinet en marque blanche obtenait un dégradé
68
+ * allant de l'indigo Zevra à SA couleur. Pour la charte par défaut,
69
+ * l'écart avec la valeur relevée est d'un point de bleu, invisible.
70
+ * Les états de survol le dérivent ensuite par color-mix. */
71
+ --zv-btn-haut: var(--accent);
48
72
  background: linear-gradient(180deg, var(--zv-btn-haut) 0%, var(--accent-deep) 100%);
49
73
  color: var(--white);
50
74
  /* Le liseré interne fait tout le galbe du bouton : il est dans Main.html
@@ -88,7 +112,7 @@
88
112
  * chose.
89
113
  * Déclarés AVANT [disabled], qui doit l'emporter à spécificité égale. */
90
114
 
91
- .zv-btn:hover {
115
+ .zv-btn:not(:where(.zv-btn--ghost, .zv-btn--ink, .zv-btn--danger)):hover {
92
116
  background: linear-gradient(
93
117
  180deg,
94
118
  color-mix(in oklab, var(--zv-btn-haut) 86%, var(--ink)) 0%,
@@ -96,7 +120,7 @@
96
120
  );
97
121
  }
98
122
 
99
- .zv-btn:active {
123
+ .zv-btn:not(:where(.zv-btn--ghost, .zv-btn--ink, .zv-btn--danger)):active {
100
124
  background: linear-gradient(
101
125
  180deg,
102
126
  color-mix(in oklab, var(--zv-btn-haut) 72%, var(--ink)) 0%,
@@ -177,8 +201,8 @@
177
201
  * désactivé OU occupé (dont le clic est avalé par la façade) : sans ces
178
202
  * règles, les états ci-dessus repeignent le fond et le bouton a l'air
179
203
  * actif. Même précaution que les contrôles désactivés de forms.css. */
180
- .zv-btn[disabled]:is(:hover, :active),
181
- .zv-btn[aria-busy='true']:is(:hover, :active) {
204
+ .zv-btn:not(:where(.zv-btn--ghost, .zv-btn--ink, .zv-btn--danger))[disabled]:is(:hover, :active),
205
+ .zv-btn:not(:where(.zv-btn--ghost, .zv-btn--ink, .zv-btn--danger))[aria-busy='true']:is(:hover, :active) {
182
206
  background: linear-gradient(180deg, var(--zv-btn-haut) 0%, var(--accent-deep) 100%);
183
207
  }
184
208
 
@@ -911,6 +935,13 @@
911
935
 
912
936
 
913
937
  @media (max-width: 767px) {
938
+
939
+ /* Le `min-width: 260px` de la valeur ne tient pas sous le seuil : dans une
940
+ * carte de 215px — un écran de 320px — elle débordait de 45px. La CLÉ
941
+ * était déjà libérée (flex-basis 100%), il manquait la valeur. */
942
+ .zv-rows__val {
943
+ min-width: 0;
944
+ }
914
945
  .zv-btn--sm {
915
946
  height: 36px;
916
947
  padding: 0 14px;
@@ -1029,3 +1060,258 @@
1029
1060
  .zv-lockup--marque-seule .zv-lockup__word {
1030
1061
  display: none;
1031
1062
  }
1063
+
1064
+ /* ─── Cadre média ───────────────────────────────────────────────────────
1065
+ * HORS RELEVÉ (27/08/2026, 0.18.0). Aucune des dix planches ne montre
1066
+ * d'image de contenu — elles sont composées, pas photographiées. Le manque
1067
+ * était pourtant total : pas un `aspect-ratio`, pas un `object-fit` dans le
1068
+ * paquet, et quatre réécritures du même cadre chez un seul consommateur.
1069
+ *
1070
+ * ⚠️ Le rapport vit sur le CADRE, jamais sur l'image. Posé sur un enfant de
1071
+ * grille aligné au centre, il ne se résout pas et la colonne s'effondre à
1072
+ * zéro — c'est un correctif de production, pas une précaution théorique.
1073
+ *
1074
+ * Le fond est posé et non laissé transparent : une image qui tarde, échoue
1075
+ * ou porte de la transparence laisserait voir la surface du dessous, soit un
1076
+ * trou blanc sur une carte, soit un trou sombre sur une bande. */
1077
+
1078
+ .zv-media {
1079
+ position: relative;
1080
+ display: block;
1081
+ width: 100%;
1082
+ overflow: hidden;
1083
+ background: var(--hairline);
1084
+ /* Angle vif comme le reste ; aucune ombre, l'image est dans le flux. */
1085
+ border-radius: 0;
1086
+ }
1087
+
1088
+ /* Les quatre rapports du système. Un cinquième se demande, il ne s'invente
1089
+ * pas : c'est ainsi qu'une bibliothèque finit avec dix-sept rapports dont
1090
+ * trois servent. */
1091
+ .zv-media--16-9 {
1092
+ aspect-ratio: 16 / 9;
1093
+ }
1094
+
1095
+ .zv-media--4-3 {
1096
+ aspect-ratio: 4 / 3;
1097
+ }
1098
+
1099
+ .zv-media--1-1 {
1100
+ aspect-ratio: 1 / 1;
1101
+ }
1102
+
1103
+ .zv-media--3-2 {
1104
+ aspect-ratio: 3 / 2;
1105
+ }
1106
+
1107
+ /* L'image REMPLIT le cadre : c'est le seul comportement qui le garantisse
1108
+ * plein quelle que soit la source, et une grille dont les couvertures ne
1109
+ * s'alignent pas se voit aussitôt. En absolu, pour que le cadre garde la
1110
+ * main sur sa hauteur. */
1111
+ .zv-media__img {
1112
+ position: absolute;
1113
+ inset: 0;
1114
+ width: 100%;
1115
+ height: 100%;
1116
+ object-fit: cover;
1117
+ display: block;
1118
+ }
1119
+
1120
+ /* Le cas inverse — un logo, un schéma, une pièce qu'on ne peut pas rogner.
1121
+ * Le papier passe derrière : il restera du vide, autant qu'il soit choisi. */
1122
+ .zv-media--contient {
1123
+ background: var(--paper);
1124
+ }
1125
+
1126
+ .zv-media--contient .zv-media__img {
1127
+ object-fit: contain;
1128
+ }
1129
+
1130
+ /* Le coin d'un cadre : durée d'une vidéo, nombre de pages, mention de
1131
+ * réserve. Encre diluée et non un gris opaque — le fond d'image est
1132
+ * imprévisible, et l'encre de charte s'y pose sans jurer. */
1133
+ .zv-media__coin {
1134
+ position: absolute;
1135
+ right: 8px;
1136
+ bottom: 8px;
1137
+ z-index: 1;
1138
+ display: inline-flex;
1139
+ align-items: center;
1140
+ gap: 4px;
1141
+ padding: 3px 7px;
1142
+ background: color-mix(in srgb, var(--ink) 72%, transparent);
1143
+ color: var(--white);
1144
+ font-family: var(--font-mono);
1145
+ font-size: var(--mono);
1146
+ font-weight: 500;
1147
+ letter-spacing: 0.08em;
1148
+ }
1149
+
1150
+ .zv-media__coin--gauche {
1151
+ right: auto;
1152
+ left: 8px;
1153
+ }
1154
+
1155
+ .zv-media__coin--haut {
1156
+ bottom: auto;
1157
+ top: 8px;
1158
+ }
1159
+
1160
+ /* Voile de lecture : un titre posé SUR une image n'est lisible qu'au-dessus
1161
+ * d'un dégradé. Il part du bas, là où le texte se pose, et n'assombrit pas
1162
+ * le sujet. Il ne capte pas le pointeur — le cadre reste cliquable. */
1163
+ .zv-media__voile {
1164
+ position: absolute;
1165
+ inset: 0;
1166
+ z-index: 1;
1167
+ pointer-events: none;
1168
+ background: linear-gradient(
1169
+ 180deg,
1170
+ transparent 40%,
1171
+ color-mix(in srgb, var(--ink) 78%, transparent) 100%
1172
+ );
1173
+ }
1174
+
1175
+ /* ─── Page légale ───────────────────────────────────────────────────────
1176
+ * HORS RELEVÉ (27/08/2026, voir DECISIONS.md). La forme d'un document
1177
+ * contractuel — CGVU, mentions légales, DPA. Le TEXTE, lui, n'est pas ici :
1178
+ * il vit dans @zevra/legal, versionné et archivé. Ce module ne porte que la
1179
+ * mise en page ; c'est ce qui permet au même texte de s'afficher sur la
1180
+ * vitrine et dans une application sans exister en deux exemplaires.
1181
+ *
1182
+ * Presque tout se compose de pièces existantes : le sommaire est .zv-rail,
1183
+ * les définitions .zv-rows, les tableaux .zv-table, les encarts .zv-callout.
1184
+ * Ne restent ici que la colonne de lecture et ce que les listes de texte
1185
+ * courant n'avaient jamais eu. */
1186
+
1187
+ .zv-legal {
1188
+ display: flex;
1189
+ flex-direction: column;
1190
+ gap: 48px;
1191
+ /* La MESURE, et c'est la seule règle qui compte vraiment pour un contrat :
1192
+ * au-delà d'environ 80 signes par ligne, l'œil perd le début de la ligne
1193
+ * suivante — sur un texte qu'on lit déjà à reculons, c'est rédhibitoire. */
1194
+ max-width: 72ch;
1195
+ }
1196
+
1197
+ .zv-legal__section {
1198
+ display: flex;
1199
+ flex-direction: column;
1200
+ gap: 14px;
1201
+ /* Un lien vers un article précis ne doit pas déposer son titre sous la
1202
+ * barre fixe. Sans cette réserve, cliquer « Art. 7 » dans le sommaire
1203
+ * affiche le corps de l'article et cache son intitulé. */
1204
+ scroll-margin-top: 88px;
1205
+ }
1206
+
1207
+ /* Les listes du texte courant. Le paquet n'en avait aucune : ses listes
1208
+ * étaient toutes des objets (étapes, sommaires numérotés, rangées à filets),
1209
+ * jamais l'énumération d'une clause. */
1210
+ .zv-legal__liste {
1211
+ margin: 0;
1212
+ padding-left: 22px;
1213
+ display: flex;
1214
+ flex-direction: column;
1215
+ gap: 8px;
1216
+ font-size: var(--body);
1217
+ line-height: 1.6;
1218
+ color: var(--muted);
1219
+ }
1220
+
1221
+ .zv-legal__liste li::marker {
1222
+ color: var(--accent-line);
1223
+ }
1224
+
1225
+ /* Le renvoi vers un autre document contractuel, dans le fil d'une clause :
1226
+ * souligné, parce qu'à l'intérieur d'un paragraphe la couleur seule ne
1227
+ * suffit pas à annoncer qu'on change de contrat. */
1228
+ .zv-legal a {
1229
+ color: var(--accent);
1230
+ text-decoration: underline;
1231
+ text-underline-offset: 2px;
1232
+ }
1233
+
1234
+ /* Le tableau d'un contrat (durées de conservation, sous-traitants). Il n'
1235
+ * emprunte PAS .zv-table de data.css : celui-là est une surface de travail
1236
+ * dont les rangées se survolent et se cliquent, ce qu'un tableau de contrat
1237
+ * ne fait jamais. Filets, chiffres alignés, et rien de plus. */
1238
+ .zv-legal__table {
1239
+ width: 100%;
1240
+ border-collapse: collapse;
1241
+ font-size: var(--app);
1242
+ line-height: 1.55;
1243
+ color: var(--ink-2);
1244
+ }
1245
+
1246
+ .zv-legal__table th {
1247
+ border-bottom: 1px solid var(--line);
1248
+ padding: 8px 12px 8px 0;
1249
+ text-align: left;
1250
+ font-family: var(--font-mono);
1251
+ font-size: var(--mono);
1252
+ font-weight: 600;
1253
+ letter-spacing: 0.14em;
1254
+ text-transform: uppercase;
1255
+ color: var(--label);
1256
+ }
1257
+
1258
+ .zv-legal__table td {
1259
+ border-bottom: 1px solid var(--hairline);
1260
+ padding: 10px 12px 10px 0;
1261
+ vertical-align: top;
1262
+ font-variant-numeric: tabular-nums;
1263
+ }
1264
+
1265
+ /* L'en-tête : titre, date d'entrée en vigueur, identifiant de version.
1266
+ * ⚠️ La VERSION est affichée, et ce n'est pas décoratif : c'est ce que
1267
+ * l'utilisateur cite quand il conteste, et ce que le support lui demande. */
1268
+ .zv-legal__tete {
1269
+ display: flex;
1270
+ flex-direction: column;
1271
+ gap: 10px;
1272
+ border-bottom: 1px solid var(--line);
1273
+ padding-bottom: 24px;
1274
+ }
1275
+
1276
+ @media (max-width: 767px) {
1277
+ .zv-legal {
1278
+ gap: 36px;
1279
+ }
1280
+
1281
+ .zv-legal__section {
1282
+ scroll-margin-top: calc(var(--topbar-h) + 12px);
1283
+ }
1284
+ }
1285
+
1286
+ /* Encart d'accompagnement d'une clause : la reformulation en langage courant
1287
+ * (« En clair ») ou le point à ne pas manquer. Il COMMENTE la clause et ne
1288
+ * la remplace pas — d'où une surface d'accompagnement, et non l'aplat d'une
1289
+ * alerte, qui annoncerait un incident.
1290
+ * Il vit ici et non dans feedback.css : une page légale de vitrine n'a pas à
1291
+ * charger la couche applicative pour afficher une glose. */
1292
+ .zv-legal__encart {
1293
+ display: flex;
1294
+ flex-direction: column;
1295
+ gap: 6px;
1296
+ border-left: 2px solid var(--accent-line);
1297
+ background: var(--panel);
1298
+ padding: 14px 18px;
1299
+ }
1300
+
1301
+ .zv-legal__encart--important {
1302
+ border-left-color: var(--warn);
1303
+ background: var(--warn-soft);
1304
+ }
1305
+
1306
+ .zv-legal__encart-label {
1307
+ font-family: var(--font-mono);
1308
+ font-size: var(--mono);
1309
+ font-weight: 700;
1310
+ letter-spacing: 0.14em;
1311
+ text-transform: uppercase;
1312
+ color: var(--label);
1313
+ }
1314
+
1315
+ .zv-legal__encart--important .zv-legal__encart-label {
1316
+ color: var(--warn-ink);
1317
+ }