@zevra/ui 0.3.2 → 0.5.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 (143) hide show
  1. package/DECISIONS.md +25 -0
  2. package/README.md +83 -11
  3. package/dist/actions.d.ts +41 -0
  4. package/dist/actions.d.ts.map +1 -0
  5. package/dist/actions.js +24 -0
  6. package/dist/actions.js.map +1 -0
  7. package/dist/catalogue.d.ts +30 -0
  8. package/dist/catalogue.d.ts.map +1 -0
  9. package/dist/catalogue.js +830 -0
  10. package/dist/catalogue.js.map +1 -0
  11. package/dist/content.d.ts +135 -0
  12. package/dist/content.d.ts.map +1 -0
  13. package/dist/content.js +85 -0
  14. package/dist/content.js.map +1 -0
  15. package/dist/css-inline.d.ts +11 -0
  16. package/dist/css-inline.d.ts.map +1 -0
  17. package/dist/css-inline.js +13 -0
  18. package/dist/css-inline.js.map +1 -0
  19. package/dist/cx.d.ts +16 -0
  20. package/dist/cx.d.ts.map +1 -0
  21. package/dist/cx.js +20 -0
  22. package/dist/cx.js.map +1 -0
  23. package/dist/data.d.ts +136 -0
  24. package/dist/data.d.ts.map +1 -0
  25. package/dist/data.js +98 -0
  26. package/dist/data.js.map +1 -0
  27. package/dist/feedback.d.ts +88 -0
  28. package/dist/feedback.d.ts.map +1 -0
  29. package/dist/feedback.js +38 -0
  30. package/dist/feedback.js.map +1 -0
  31. package/dist/forms.d.ts +120 -0
  32. package/dist/forms.d.ts.map +1 -0
  33. package/dist/forms.js +60 -0
  34. package/dist/forms.js.map +1 -0
  35. package/dist/icon-data.d.ts +6 -0
  36. package/dist/icon-data.d.ts.map +1 -0
  37. package/dist/icon-data.js +77 -0
  38. package/dist/icon-data.js.map +1 -0
  39. package/dist/icon.d.ts +33 -0
  40. package/dist/icon.d.ts.map +1 -0
  41. package/dist/icon.js +13 -0
  42. package/dist/icon.js.map +1 -0
  43. package/dist/index.d.ts +13 -0
  44. package/dist/index.d.ts.map +1 -0
  45. package/dist/index.js +41 -0
  46. package/dist/index.js.map +1 -0
  47. package/dist/layout.d.ts +166 -0
  48. package/dist/layout.d.ts.map +1 -0
  49. package/dist/layout.js +120 -0
  50. package/dist/layout.js.map +1 -0
  51. package/dist/navigation.d.ts +150 -0
  52. package/dist/navigation.d.ts.map +1 -0
  53. package/dist/navigation.js +104 -0
  54. package/dist/navigation.js.map +1 -0
  55. package/dist/overlays.d.ts +68 -0
  56. package/dist/overlays.d.ts.map +1 -0
  57. package/dist/overlays.js +114 -0
  58. package/dist/overlays.js.map +1 -0
  59. package/dist/surfaces.d.ts +61 -0
  60. package/dist/surfaces.d.ts.map +1 -0
  61. package/dist/surfaces.js +54 -0
  62. package/dist/surfaces.js.map +1 -0
  63. package/dist/typography.d.ts +89 -0
  64. package/dist/typography.d.ts.map +1 -0
  65. package/dist/typography.js +76 -0
  66. package/dist/typography.js.map +1 -0
  67. package/logos/index.json +10 -500
  68. package/package.json +36 -4
  69. package/react/actions.tsx +138 -0
  70. package/react/catalogue.tsx +1644 -0
  71. package/react/content.tsx +313 -0
  72. package/react/css-inline.ts +14 -0
  73. package/react/cx.ts +20 -0
  74. package/react/data.tsx +300 -0
  75. package/react/feedback.tsx +229 -0
  76. package/react/forms.tsx +383 -0
  77. package/react/icon-data.ts +114 -0
  78. package/react/icon.tsx +84 -0
  79. package/react/index.ts +42 -0
  80. package/react/layout.tsx +347 -0
  81. package/react/navigation.tsx +329 -0
  82. package/react/overlays.tsx +237 -0
  83. package/react/surfaces.tsx +161 -0
  84. package/react/typography.tsx +220 -0
  85. package/logos/png/campus-glyph-ink.png +0 -0
  86. package/logos/png/campus-glyph-white.png +0 -0
  87. package/logos/png/campus-lockup.png +0 -0
  88. package/logos/png/lexform-glyph-ink.png +0 -0
  89. package/logos/png/lexform-glyph-white.png +0 -0
  90. package/logos/png/lexform-lockup.png +0 -0
  91. package/logos/png/mcp-glyph-ink.png +0 -0
  92. package/logos/png/mcp-glyph-white.png +0 -0
  93. package/logos/png/mcp-lockup.png +0 -0
  94. package/logos/png/memoire-dark.png +0 -0
  95. package/logos/png/memoire-glyph-ink.png +0 -0
  96. package/logos/png/memoire-glyph-white.png +0 -0
  97. package/logos/png/memoire-lockup.png +0 -0
  98. package/logos/png/zevra-lockup-ink.png +0 -0
  99. package/logos/png/zevra-lockup-white.png +0 -0
  100. package/logos/png/zevra-lockup.png +0 -0
  101. package/logos/png/zevra-mark-ink.png +0 -0
  102. package/logos/png/zevra-mark-white.png +0 -0
  103. package/logos/png/zevra-mark.png +0 -0
  104. package/logos/svg/campus-glyph-ink.svg +0 -1
  105. package/logos/svg/campus-glyph-navy.svg +0 -1
  106. package/logos/svg/campus-glyph-orig.svg +0 -1
  107. package/logos/svg/campus-glyph-white.svg +0 -1
  108. package/logos/svg/campus-lockup-ink.svg +0 -1
  109. package/logos/svg/campus-lockup-navy.svg +0 -1
  110. package/logos/svg/campus-lockup-orig.svg +0 -1
  111. package/logos/svg/campus-lockup-white.svg +0 -1
  112. package/logos/svg/lexform-glyph-ink.svg +0 -1
  113. package/logos/svg/lexform-glyph-navy.svg +0 -1
  114. package/logos/svg/lexform-glyph-orig.svg +0 -1
  115. package/logos/svg/lexform-glyph-white.svg +0 -1
  116. package/logos/svg/lexform-lockup-ink.svg +0 -1
  117. package/logos/svg/lexform-lockup-navy.svg +0 -1
  118. package/logos/svg/lexform-lockup-orig.svg +0 -1
  119. package/logos/svg/lexform-lockup-white.svg +0 -1
  120. package/logos/svg/mcp-glyph-ink.svg +0 -1
  121. package/logos/svg/mcp-glyph-navy.svg +0 -1
  122. package/logos/svg/mcp-glyph-orig.svg +0 -1
  123. package/logos/svg/mcp-glyph-white.svg +0 -1
  124. package/logos/svg/mcp-lockup-ink.svg +0 -1
  125. package/logos/svg/mcp-lockup-navy.svg +0 -1
  126. package/logos/svg/mcp-lockup-orig.svg +0 -1
  127. package/logos/svg/mcp-lockup-white.svg +0 -1
  128. package/logos/svg/memoire-glyph-ink.svg +0 -1
  129. package/logos/svg/memoire-glyph-navy.svg +0 -1
  130. package/logos/svg/memoire-glyph-orig.svg +0 -1
  131. package/logos/svg/memoire-glyph-white.svg +0 -1
  132. package/logos/svg/memoire-lockup-ink.svg +0 -1
  133. package/logos/svg/memoire-lockup-navy.svg +0 -1
  134. package/logos/svg/memoire-lockup-orig.svg +0 -1
  135. package/logos/svg/memoire-lockup-white.svg +0 -1
  136. package/logos/svg/zevra-lockup-ink.svg +0 -1
  137. package/logos/svg/zevra-lockup-navy.svg +0 -1
  138. package/logos/svg/zevra-lockup-orig.svg +0 -1
  139. package/logos/svg/zevra-lockup-white.svg +0 -1
  140. package/logos/svg/zevra-mark-ink.svg +0 -1
  141. package/logos/svg/zevra-mark-navy.svg +0 -1
  142. package/logos/svg/zevra-mark-orig.svg +0 -1
  143. package/logos/svg/zevra-mark-white.svg +0 -1
@@ -0,0 +1,383 @@
1
+ // Saisie — champs, sélection, bascules, puces de filtre, zone de dépôt.
2
+ //
3
+ // Aucun composant de ce fichier ne porte « use client » : ils sont tous sans
4
+ // état. Ceux qui prennent un gestionnaire d'événement (Switch, Seg, Drop)
5
+ // doivent être rendus DEPUIS un composant client, qui détient l'état — le
6
+ // paquet ne le fait pas à ta place, sinon un simple champ de recherche
7
+ // tirerait React côté navigateur sur une page vitrine.
8
+ //
9
+ // ACCESSIBILITÉ — le liage intitulé ↔ contrôle est EXPLICITE. Field ne
10
+ // fabrique pas d'identifiant (il faudrait useId, donc un composant client) :
11
+ // passe le même `htmlFor` à Field et `id` au contrôle. Field pose l'erreur
12
+ // sur `${htmlFor}-err` pour que tu puisses la citer dans aria-describedby.
13
+
14
+ import type {
15
+ ButtonHTMLAttributes,
16
+ HTMLAttributes,
17
+ InputHTMLAttributes,
18
+ ReactNode,
19
+ SelectHTMLAttributes,
20
+ TextareaHTMLAttributes,
21
+ } from 'react'
22
+ import { cx } from './cx.js'
23
+
24
+ /* ─── Groupe de champ ──────────────────────────────────────────────── */
25
+
26
+ export interface FieldProps
27
+ extends Omit<HTMLAttributes<HTMLDivElement>, 'className'> {
28
+ label?: ReactNode
29
+ /** Identifiant du contrôle enveloppé — à répéter en `id` sur le contrôle. */
30
+ htmlFor?: string
31
+ /** Posé à droite de l'intitulé, sur la même ligne de base
32
+ * (« Mot de passe oublié ? »). */
33
+ action?: ReactNode
34
+ /** Aide permanente. Effacée par `erreur` : les deux au même endroit se
35
+ * chevaucheraient. */
36
+ help?: ReactNode
37
+ /** Message d'erreur. Sa présence passe aussi le groupe en `--err`, ce qui
38
+ * repeint le filet du contrôle. */
39
+ erreur?: ReactNode
40
+ className?: string
41
+ children?: ReactNode
42
+ }
43
+
44
+ export function Field({
45
+ label,
46
+ htmlFor,
47
+ action,
48
+ help,
49
+ erreur,
50
+ className,
51
+ children,
52
+ ...reste
53
+ }: FieldProps) {
54
+ return (
55
+ <div className={cx('zv-field', !!erreur && 'zv-field--err', className)} {...reste}>
56
+ {(label || action) &&
57
+ (action ? (
58
+ <div className="zv-field__head">
59
+ <label className="zv-field__label" htmlFor={htmlFor}>
60
+ {label}
61
+ </label>
62
+ {action}
63
+ </div>
64
+ ) : (
65
+ <label className="zv-field__label" htmlFor={htmlFor}>
66
+ {label}
67
+ </label>
68
+ ))}
69
+ {children}
70
+ {erreur ? (
71
+ <span className="zv-field__err" id={htmlFor ? `${htmlFor}-err` : undefined}>
72
+ {erreur}
73
+ </span>
74
+ ) : (
75
+ help && <span className="zv-field__help">{help}</span>
76
+ )}
77
+ </div>
78
+ )
79
+ }
80
+
81
+ /* ─── Champ de saisie ──────────────────────────────────────────────── */
82
+
83
+ export interface InputProps
84
+ extends Omit<InputHTMLAttributes<HTMLInputElement>, 'className'> {
85
+ /** Chiffres et tirets alignés (identifiant d'organisation, jeton). */
86
+ mono?: boolean
87
+ /** Ni fond ni filet : c'est le CADRE qui l'entoure qui fait le champ.
88
+ * Lui laisser sa bordure dessinerait deux cadres imbriqués. */
89
+ nu?: boolean
90
+ /** Pose `aria-invalid` — c'est lui qui rend l'erreur audible, et le CSS le
91
+ * suit autant que la classe. */
92
+ erreur?: boolean
93
+ className?: string
94
+ }
95
+
96
+ export function Input({ mono, nu, erreur, className, ...reste }: InputProps) {
97
+ return (
98
+ <input
99
+ className={cx('zv-input', mono && 'zv-input--mono', nu && 'zv-input--bare', className)}
100
+ aria-invalid={erreur || undefined}
101
+ {...reste}
102
+ />
103
+ )
104
+ }
105
+
106
+ export interface TextareaProps
107
+ extends Omit<TextareaHTMLAttributes<HTMLTextAreaElement>, 'className'> {
108
+ erreur?: boolean
109
+ className?: string
110
+ }
111
+
112
+ export function Textarea({ erreur, className, ...reste }: TextareaProps) {
113
+ return (
114
+ <textarea
115
+ className={cx('zv-textarea', className)}
116
+ aria-invalid={erreur || undefined}
117
+ {...reste}
118
+ />
119
+ )
120
+ }
121
+
122
+ /** <select> NATIF habillé comme le champ : la flèche, l'ouverture au clavier
123
+ * et l'annonce viennent du navigateur. C'est le motif de choix par défaut —
124
+ * `SelectTrigger` est un menu maison, à réserver aux filtres. */
125
+ export function Select({
126
+ className,
127
+ ...reste
128
+ }: Omit<SelectHTMLAttributes<HTMLSelectElement>, 'className'> & { className?: string }) {
129
+ return <select className={cx('zv-input', 'zv-input--select', className)} {...reste} />
130
+ }
131
+
132
+ /* ─── Recherche ────────────────────────────────────────────────────── */
133
+
134
+ export interface SearchProps
135
+ extends Omit<InputHTMLAttributes<HTMLInputElement>, 'className'> {
136
+ /** Enveloppe : c'est elle qui porte le cadre et la largeur. */
137
+ className?: string
138
+ }
139
+
140
+ /** Cadre + loupe + champ nu. La loupe est dans le balisage (et non un fond
141
+ * CSS) pour suivre `currentColor` et rester nette en zoom. */
142
+ export function Search({ className, ...reste }: SearchProps) {
143
+ return (
144
+ <div className={cx('zv-search', className)}>
145
+ <svg
146
+ className="zv-search__icon"
147
+ viewBox="0 0 13 13"
148
+ fill="none"
149
+ strokeWidth="1.5"
150
+ aria-hidden="true"
151
+ >
152
+ <circle cx="5.5" cy="5.5" r="4" />
153
+ <line x1="8.6" y1="8.6" x2="12" y2="12" />
154
+ </svg>
155
+ <input type="search" className="zv-search__input" {...reste} />
156
+ </div>
157
+ )
158
+ }
159
+
160
+ /* ─── Déclencheur de sélection (menu maison) ───────────────────────── */
161
+
162
+ export function SelectAnchor({
163
+ className,
164
+ ...reste
165
+ }: Omit<HTMLAttributes<HTMLDivElement>, 'className'> & { className?: string }) {
166
+ return <div className={cx('zv-select-anchor', className)} {...reste} />
167
+ }
168
+
169
+ export interface SelectTriggerProps
170
+ extends Omit<ButtonHTMLAttributes<HTMLButtonElement>, 'className' | 'value'> {
171
+ /** L'intitulé, en capitales (« PÉRIMÈTRE »). */
172
+ label?: ReactNode
173
+ /** La valeur courante, seule partie mise en avant. */
174
+ value?: ReactNode
175
+ className?: string
176
+ }
177
+
178
+ /** Bouton qui ouvre un `Menu`. Pour un vrai choix dans une liste, préférer
179
+ * `Select` (natif). À poser dans un `SelectAnchor` : c'est l'enveloppe qui
180
+ * ancre le menu, pas le déclencheur. */
181
+ export function SelectTrigger({
182
+ label,
183
+ value,
184
+ className,
185
+ ...reste
186
+ }: SelectTriggerProps) {
187
+ return (
188
+ <button type="button" className={cx('zv-select', className)} {...reste}>
189
+ {label}
190
+ {value != null && <strong>{value}</strong>}
191
+ <span className="zv-select__chevron" aria-hidden="true">
192
+ ▾
193
+ </span>
194
+ </button>
195
+ )
196
+ }
197
+
198
+ /* ─── Case, radio ──────────────────────────────────────────────────── */
199
+
200
+ export function Checkbox({
201
+ className,
202
+ ...reste
203
+ }: Omit<InputHTMLAttributes<HTMLInputElement>, 'className' | 'type'> & {
204
+ className?: string
205
+ }) {
206
+ return <input type="checkbox" className={cx('zv-check-box', className)} {...reste} />
207
+ }
208
+
209
+ export function Radio({
210
+ className,
211
+ ...reste
212
+ }: Omit<InputHTMLAttributes<HTMLInputElement>, 'className' | 'type'> & {
213
+ className?: string
214
+ }) {
215
+ return <input type="radio" className={cx('zv-radio', className)} {...reste} />
216
+ }
217
+
218
+ /* ─── Interrupteur ─────────────────────────────────────────────────── */
219
+
220
+ export interface SwitchProps
221
+ extends Omit<ButtonHTMLAttributes<HTMLButtonElement>, 'className' | 'onChange'> {
222
+ /** État courant. Composant CONTRÔLÉ : il ne mémorise rien. */
223
+ on: boolean
224
+ /** Libellé accolé — c'est lui qui porte le sens, la piste seule ne dit pas
225
+ * ce qui est activé. */
226
+ texte?: ReactNode
227
+ className?: string
228
+ }
229
+
230
+ export function Switch({ on, texte, className, ...reste }: SwitchProps) {
231
+ return (
232
+ <button
233
+ type="button"
234
+ role="switch"
235
+ aria-checked={on}
236
+ className={cx('zv-switch-hit', className)}
237
+ {...reste}
238
+ >
239
+ <span className={cx('zv-switch', on && 'zv-switch--on')}>
240
+ <span className="zv-switch__knob" />
241
+ </span>
242
+ {texte && (
243
+ <span className={cx('zv-switch__text', on && 'zv-switch__text--on')}>{texte}</span>
244
+ )}
245
+ </button>
246
+ )
247
+ }
248
+
249
+ /* ─── Puces de filtre ──────────────────────────────────────────────── */
250
+
251
+ export interface SegGroupProps
252
+ extends Omit<HTMLAttributes<HTMLDivElement>, 'className'> {
253
+ /** Rangée qui défile au lieu de passer à la ligne. À poser explicitement :
254
+ * par défaut les puces s'enroulent. */
255
+ rail?: boolean
256
+ className?: string
257
+ }
258
+
259
+ export function SegGroup({ rail, className, ...reste }: SegGroupProps) {
260
+ return (
261
+ <div
262
+ className={cx('zv-seg-group', rail && 'zv-seg-group--rail', className)}
263
+ {...reste}
264
+ />
265
+ )
266
+ }
267
+
268
+ export interface SegProps
269
+ extends Omit<ButtonHTMLAttributes<HTMLButtonElement>, 'className'> {
270
+ on?: boolean
271
+ /** Sélection PLEINE au lieu de douce. ⚠️ Un seul groupe plein par page, et
272
+ * jamais sur une page qui porte déjà un bouton plein. */
273
+ plein?: boolean
274
+ /** Libellés de longueur inégale centrés (INFO, WARN, ERROR). */
275
+ mono?: boolean
276
+ /** Compteur accolé (« Contrats 12 »). */
277
+ compte?: ReactNode
278
+ className?: string
279
+ children?: ReactNode
280
+ }
281
+
282
+ export function Seg({
283
+ on,
284
+ plein,
285
+ mono,
286
+ compte,
287
+ className,
288
+ children,
289
+ ...reste
290
+ }: SegProps) {
291
+ return (
292
+ <button
293
+ type="button"
294
+ aria-pressed={on ?? false}
295
+ className={cx(
296
+ 'zv-seg',
297
+ mono && 'zv-seg--mono',
298
+ on && (plein ? 'zv-seg--on-solid' : 'zv-seg--on'),
299
+ className,
300
+ )}
301
+ {...reste}
302
+ >
303
+ {children}
304
+ {compte != null && <span className="zv-seg__count">{compte}</span>}
305
+ </button>
306
+ )
307
+ }
308
+
309
+ /* ─── Zone de dépôt ────────────────────────────────────────────────── */
310
+
311
+ export interface DropProps
312
+ extends Omit<HTMLAttributes<HTMLDivElement>, 'className' | 'title'> {
313
+ titre?: ReactNode
314
+ /** Précision sous le titre (« PDF, DOCX · 25 Mo max »). */
315
+ spec?: ReactNode
316
+ /** Alternative tactile : sous le point de rupture, le lien souligné cède
317
+ * la place à cette cible. Le CSS échange les deux, les deux nœuds restent
318
+ * posés en permanence. */
319
+ parcourir?: ReactNode
320
+ /** Un fichier est en cours de survol : même rendu que le survol souris. */
321
+ survol?: boolean
322
+ desactive?: boolean
323
+ className?: string
324
+ children?: ReactNode
325
+ }
326
+
327
+ /** ⚠️ `role="button"` + `tabIndex` sont posés ici et non laissés au bon
328
+ * vouloir de l'appelant : une zone de dépôt sans eux rend la page
329
+ * inatteignable au clavier. Penser à remettre `value=''` sur l'input après
330
+ * chaque sélection, sinon redéposer le même fichier ne déclenche rien. */
331
+ export function Drop({
332
+ titre,
333
+ spec,
334
+ parcourir,
335
+ survol,
336
+ desactive,
337
+ className,
338
+ children,
339
+ ...reste
340
+ }: DropProps) {
341
+ return (
342
+ <div
343
+ role="button"
344
+ tabIndex={desactive ? -1 : 0}
345
+ aria-disabled={desactive || undefined}
346
+ className={cx(
347
+ 'zv-drop',
348
+ survol && 'is-dragging',
349
+ desactive && 'zv-drop--disabled',
350
+ className,
351
+ )}
352
+ {...reste}
353
+ >
354
+ <svg
355
+ viewBox="0 0 30 30"
356
+ width="30"
357
+ height="30"
358
+ fill="none"
359
+ stroke="var(--accent)"
360
+ strokeWidth="1.5"
361
+ strokeLinecap="round"
362
+ aria-hidden="true"
363
+ >
364
+ <line x1="15" y1="4" x2="15" y2="18" />
365
+ <polyline points="9.5,12.5 15,18 20.5,12.5" />
366
+ <path d="M5 21 v3 a2 2 0 0 0 2 2 h16 a2 2 0 0 0 2-2 v-3" />
367
+ </svg>
368
+ {titre && <span className="zv-drop__title">{titre}</span>}
369
+ {parcourir && <span className="zv-drop__browse">{parcourir}</span>}
370
+ {spec && <span className="zv-drop__spec">{spec}</span>}
371
+ {children}
372
+ </div>
373
+ )
374
+ }
375
+
376
+ export function DropInput({
377
+ className,
378
+ ...reste
379
+ }: Omit<InputHTMLAttributes<HTMLInputElement>, 'className' | 'type'> & {
380
+ className?: string
381
+ }) {
382
+ return <input type="file" className={cx('zv-drop__input', className)} {...reste} />
383
+ }
@@ -0,0 +1,114 @@
1
+ // GÉNÉRÉ par scripts/build-icon-react.mjs — ne pas éditer à la main.
2
+ // Source de vérité : icons/index.json.
3
+
4
+ export type IconId =
5
+ | 'accueil'
6
+ | 'activite'
7
+ | 'app-couches'
8
+ | 'archiver'
9
+ | 'base-de-donnees'
10
+ | 'bouclier-verifie'
11
+ | 'cadenas'
12
+ | 'cadrer'
13
+ | 'chevron-bas'
14
+ | 'chevron-droit'
15
+ | 'chevron-gauche'
16
+ | 'coche'
17
+ | 'copier'
18
+ | 'corbeille'
19
+ | 'cube'
20
+ | 'deconnexion'
21
+ | 'depot-fichier'
22
+ | 'dossier'
23
+ | 'eclair'
24
+ | 'equipe'
25
+ | 'etincelles'
26
+ | 'fermer'
27
+ | 'horloge'
28
+ | 'lien-externe'
29
+ | 'menu'
30
+ | 'moins'
31
+ | 'oeil'
32
+ | 'plus'
33
+ | 'recherche'
34
+ | 'registre'
35
+ | 'reglages'
36
+ | 'reseau'
37
+ | 'taches'
38
+ | 'trois-points'
39
+
40
+ export const CORPS: Record<IconId, string> = {
41
+ '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
+ 'activite': "<path d=\"M3.33 12 H8 L10.67 5.33 L14 18.67 L16 12 H20.67\" />",
43
+ '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
+ '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\" />",
45
+ '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
+ '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
+ '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
+ '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\" />",
49
+ 'chevron-bas': "<path d=\"M6.67 9.33 L12 14.67 L17.33 9.33\" />",
50
+ 'chevron-droit': "<path d=\"M9.33 4 L17.33 12 L9.33 20\" />",
51
+ 'chevron-gauche': "<path d=\"M14.67 4 L6.67 12 L14.67 20\" />",
52
+ 'coche': "<path d=\"M5 12.4 L9.4 16.8 L19 6.8\" />",
53
+ '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
+ '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\" />",
55
+ '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
+ '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
+ '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\" />",
58
+ '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
+ 'eclair': "<path d=\"M13.33 2.67 L5.33 13.33 H12 L10.67 21.33 L18.67 10.67 H12 Z\" />",
60
+ '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
+ '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\" />",
62
+ 'fermer': "<path d=\"M5.33 5.33 L18.67 18.67\" /><path d=\"M18.67 5.33 L5.33 18.67\" />",
63
+ 'horloge': "<circle cx=\"12\" cy=\"12\" r=\"8\" /><path d=\"M12 6.67 V12 L15.33 14\" />",
64
+ '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\" />",
65
+ 'menu': "<path d=\"M0 5.33 H24\" /><path d=\"M0 12 H24\" /><path d=\"M0 18.67 H24\" />",
66
+ 'moins': "<path d=\"M4 12 H20\" />",
67
+ '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\" />",
68
+ 'plus': "<path d=\"M12 4 V20\" /><path d=\"M4 12 H20\" />",
69
+ 'recherche': "<circle cx=\"10.67\" cy=\"10.67\" r=\"6\" /><path d=\"M15.33 15.33 L20 20\" />",
70
+ 'registre': "<rect x=\"3.33\" y=\"5.33\" width=\"17.33\" height=\"5.33\" rx=\"1.33\" /><rect x=\"3.33\" y=\"13.33\" width=\"17.33\" height=\"5.33\" rx=\"1.33\" /><circle cx=\"6.67\" cy=\"8\" r=\"0.8\" fill=\"currentColor\" stroke=\"none\" /><circle cx=\"6.67\" cy=\"16\" r=\"0.8\" fill=\"currentColor\" stroke=\"none\" />",
71
+ '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
+ '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
+ '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\" />",
74
+ '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\" />",
75
+ }
76
+
77
+ /** Nom lisible de chaque icône — sert de titre accessible par défaut à
78
+ * `IconButton`, et de libellé dans la page de consultation de l'admin. */
79
+ export const NOMS: Record<IconId, string> = {
80
+ 'accueil': "Accueil",
81
+ 'activite': "Activité (pouls)",
82
+ 'app-couches': "Application (couches)",
83
+ 'archiver': "Archiver",
84
+ 'base-de-donnees': "Base de données",
85
+ 'bouclier-verifie': "Bouclier vérifié (authentification)",
86
+ 'cadenas': "Cadenas (secret)",
87
+ 'cadrer': "Cadrer (ajuster à la vue)",
88
+ 'chevron-bas': "Chevron bas",
89
+ 'chevron-droit': "Chevron droit",
90
+ 'chevron-gauche': "Chevron gauche",
91
+ 'coche': "Coche",
92
+ 'copier': "Copier",
93
+ 'corbeille': "Corbeille (supprimer)",
94
+ 'cube': "Cube (stockage objet)",
95
+ 'deconnexion': "Déconnexion",
96
+ 'depot-fichier': "Dépôt de fichier (zone de glisser-déposer)",
97
+ 'dossier': "Dossier",
98
+ 'eclair': "Éclair (action rapide)",
99
+ 'equipe': "Équipe (membres)",
100
+ 'etincelles': "Étincelles (IA)",
101
+ 'fermer': "Fermer",
102
+ 'horloge': "Horloge (intégration continue)",
103
+ 'lien-externe': "Lien externe",
104
+ 'menu': "Menu (hamburger)",
105
+ 'moins': "Moins",
106
+ 'oeil': "Œil (afficher)",
107
+ 'plus': "Plus",
108
+ 'recherche': "Recherche (loupe)",
109
+ 'registre': "Registre (serveurs empilés)",
110
+ 'reglages': "Réglages",
111
+ 'reseau': "Réseau (entrée / globe)",
112
+ 'taches': "Tâches (liste à cocher)",
113
+ 'trois-points': "Trois points (menu contextuel)",
114
+ }
package/react/icon.tsx ADDED
@@ -0,0 +1,84 @@
1
+ // Icônes — jeu de 34, tracées, sur grille de 24.
2
+ //
3
+ // Le corps du tracé est INLINÉ (voir scripts/build-icon-react.mjs) : pas de
4
+ // <use href="…sprite.svg">, donc rien à résoudre côté bundler et rien à
5
+ // autoriser côté CSP. L'icône hérite de la couleur du texte : elle fonctionne
6
+ // sur papier comme sur la bande manifeste sans déclinaison.
7
+
8
+ import type { ButtonHTMLAttributes, ReactNode, SVGAttributes } from 'react'
9
+ import { cx } from './cx.js'
10
+ import { CORPS, NOMS, type IconId } from './icon-data.js'
11
+
12
+ export type { IconId }
13
+ export { NOMS as NOMS_ICONES }
14
+
15
+ /** Les 34 identifiants, triés — pour construire une planche ou un sélecteur. */
16
+ export const ICONES = Object.keys(CORPS) as IconId[]
17
+
18
+ export interface IconProps
19
+ extends Omit<SVGAttributes<SVGSVGElement>, 'className' | 'children'> {
20
+ nom: IconId
21
+ /** `sm` compense l'épaississement optique du trait, `xl` l'affine.
22
+ * `lg` = navigation et boutons de chrome. */
23
+ taille?: 'sm' | 'normal' | 'lg' | 'xl'
24
+ discret?: boolean
25
+ /** Titre accessible. Sans lui l'icône est DÉCORATIVE et masquée aux
26
+ * lecteurs d'écran — ce qui est le bon défaut : une icône accolée à un
27
+ * libellé qui la répète crée une annonce double. */
28
+ titre?: string
29
+ className?: string
30
+ }
31
+
32
+ export function Icon({ nom, taille = 'normal', discret, titre, className, ...reste }: IconProps) {
33
+ return (
34
+ <svg
35
+ viewBox="0 0 24 24"
36
+ role={titre ? 'img' : undefined}
37
+ aria-hidden={titre ? undefined : true}
38
+ aria-label={titre}
39
+ className={cx(
40
+ 'zv-icon',
41
+ taille !== 'normal' && `zv-icon--${taille}`,
42
+ discret && 'zv-icon--quiet',
43
+ className,
44
+ )}
45
+ dangerouslySetInnerHTML={{ __html: CORPS[nom] }}
46
+ {...reste}
47
+ />
48
+ )
49
+ }
50
+
51
+ export interface IconButtonProps
52
+ extends Omit<ButtonHTMLAttributes<HTMLButtonElement>, 'className'> {
53
+ /** Icône du jeu. À défaut, passe le glyphe en `children` — mais préfère le
54
+ * jeu : trois tracés maison de la même « croix » ont déjà divergé. */
55
+ icone?: IconId
56
+ /** OBLIGATOIRE : une icône seule n'a pas de libellé pour la nommer.
57
+ * L'écrire pour l'ACTION (« Fermer le panneau ») et non pour le dessin
58
+ * (« Croix ») — c'est ce que le lecteur d'écran annonce. */
59
+ label: string
60
+ taille?: IconProps['taille']
61
+ className?: string
62
+ children?: ReactNode
63
+ }
64
+
65
+ export function IconButton({
66
+ icone,
67
+ label,
68
+ taille = 'lg',
69
+ className,
70
+ children,
71
+ type = 'button',
72
+ ...reste
73
+ }: IconButtonProps) {
74
+ return (
75
+ <button
76
+ type={type}
77
+ aria-label={label}
78
+ className={cx('zv-icon-btn', className)}
79
+ {...reste}
80
+ >
81
+ {icone ? <Icon nom={icone} taille={taille} /> : children}
82
+ </button>
83
+ )
84
+ }
package/react/index.ts ADDED
@@ -0,0 +1,42 @@
1
+ // @zevra/ui/react — bibliothèque de composants du design system « Encre & Papier ».
2
+ //
3
+ // COMMENT C'EST ORGANISÉ. Le CSS reste la source de vérité : chaque composant
4
+ // n'est qu'une façade typée et documentée au-dessus de classes `zv-`. Rien
5
+ // n'est calculé au rendu, aucune valeur de charte n'est écrite en JavaScript.
6
+ // Un test (tests/react.test.js) vérifie que toute classe qu'un composant peut
7
+ // émettre existe bien dans le CSS — c'est ce qui empêche les deux de diverger.
8
+ //
9
+ // CE QU'IL FAUT CHARGER. Les composants ne portent PAS le CSS : il s'importe
10
+ // à part, une fois, dans la page ou la mise en page racine.
11
+ //
12
+ // import '@zevra/ui/tokens.css' // toujours
13
+ // import '@zevra/ui/fonts.css' // sauf si l'app charge déjà les polices
14
+ // import '@zevra/ui/base.css' // toujours
15
+ // … puis les modules employés (components, forms, feedback, data, …)
16
+ // ou '@zevra/ui/css' pour tout charger d'un coup.
17
+ //
18
+ // SERVEUR / CLIENT. Un seul module est marqué « use client » : `overlays`
19
+ // (tiroir, modale, notification) — eux seuls ont besoin du navigateur. Tout
20
+ // le reste se rend au serveur, y compris dans une île Astro à zéro JS.
21
+ // Les composants qui prennent un gestionnaire d'événement (Switch, Seg,
22
+ // Tabs…) restent sans état : c'est un composant client à toi qui détient
23
+ // l'état et les rend.
24
+ //
25
+ // ÉCRITURE DES PROPS. En français, comme le CSS et les commentaires. Chaque
26
+ // prop qui porte une décision de charte est documentée à l'endroit où on la
27
+ // lit — les avertissements ⚠️ signalent ce qui casse le système si on le
28
+ // contourne, pas de simples préférences.
29
+
30
+ export { cx, mod } from './cx.js'
31
+
32
+ export * from './actions.js'
33
+ export * from './content.js'
34
+ export * from './data.js'
35
+ export * from './feedback.js'
36
+ export * from './forms.js'
37
+ export * from './icon.js'
38
+ export * from './layout.js'
39
+ export * from './navigation.js'
40
+ export * from './overlays.js'
41
+ export * from './surfaces.js'
42
+ export * from './typography.js'