@zevra/ui 0.4.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (86) hide show
  1. package/DECISIONS.md +24 -0
  2. package/README.md +64 -1
  3. package/demo/index.html +33 -0
  4. package/dist/actions.d.ts +41 -0
  5. package/dist/actions.d.ts.map +1 -0
  6. package/dist/actions.js +24 -0
  7. package/dist/actions.js.map +1 -0
  8. package/dist/catalogue.d.ts +30 -0
  9. package/dist/catalogue.d.ts.map +1 -0
  10. package/dist/catalogue.js +881 -0
  11. package/dist/catalogue.js.map +1 -0
  12. package/dist/content.d.ts +186 -0
  13. package/dist/content.d.ts.map +1 -0
  14. package/dist/content.js +122 -0
  15. package/dist/content.js.map +1 -0
  16. package/dist/css-inline.d.ts +11 -0
  17. package/dist/css-inline.d.ts.map +1 -0
  18. package/dist/css-inline.js +13 -0
  19. package/dist/css-inline.js.map +1 -0
  20. package/dist/cx.d.ts +16 -0
  21. package/dist/cx.d.ts.map +1 -0
  22. package/dist/cx.js +20 -0
  23. package/dist/cx.js.map +1 -0
  24. package/dist/data.d.ts +136 -0
  25. package/dist/data.d.ts.map +1 -0
  26. package/dist/data.js +98 -0
  27. package/dist/data.js.map +1 -0
  28. package/dist/feedback.d.ts +88 -0
  29. package/dist/feedback.d.ts.map +1 -0
  30. package/dist/feedback.js +38 -0
  31. package/dist/feedback.js.map +1 -0
  32. package/dist/forms.d.ts +120 -0
  33. package/dist/forms.d.ts.map +1 -0
  34. package/dist/forms.js +60 -0
  35. package/dist/forms.js.map +1 -0
  36. package/dist/icon-data.d.ts +6 -0
  37. package/dist/icon-data.d.ts.map +1 -0
  38. package/dist/icon-data.js +77 -0
  39. package/dist/icon-data.js.map +1 -0
  40. package/dist/icon.d.ts +33 -0
  41. package/dist/icon.d.ts.map +1 -0
  42. package/dist/icon.js +13 -0
  43. package/dist/icon.js.map +1 -0
  44. package/dist/index.d.ts +13 -0
  45. package/dist/index.d.ts.map +1 -0
  46. package/dist/index.js +41 -0
  47. package/dist/index.js.map +1 -0
  48. package/dist/layout.d.ts +167 -0
  49. package/dist/layout.d.ts.map +1 -0
  50. package/dist/layout.js +121 -0
  51. package/dist/layout.js.map +1 -0
  52. package/dist/navigation.d.ts +150 -0
  53. package/dist/navigation.d.ts.map +1 -0
  54. package/dist/navigation.js +104 -0
  55. package/dist/navigation.js.map +1 -0
  56. package/dist/overlays.d.ts +68 -0
  57. package/dist/overlays.d.ts.map +1 -0
  58. package/dist/overlays.js +114 -0
  59. package/dist/overlays.js.map +1 -0
  60. package/dist/surfaces.d.ts +61 -0
  61. package/dist/surfaces.d.ts.map +1 -0
  62. package/dist/surfaces.js +54 -0
  63. package/dist/surfaces.js.map +1 -0
  64. package/dist/typography.d.ts +89 -0
  65. package/dist/typography.d.ts.map +1 -0
  66. package/dist/typography.js +76 -0
  67. package/dist/typography.js.map +1 -0
  68. package/package.json +36 -4
  69. package/react/actions.tsx +138 -0
  70. package/react/catalogue.tsx +1752 -0
  71. package/react/content.tsx +417 -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 +348 -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/src/components.css +201 -0
  86. package/src/layout.css +4 -0
@@ -0,0 +1,417 @@
1
+ // Contenu — surfaces, badges et blocs éditoriaux. Sans état, donc sans
2
+ // « use client ».
3
+
4
+ import type { HTMLAttributes, ReactNode } from 'react'
5
+ import { cx } from './cx.js'
6
+
7
+ type Div = Omit<HTMLAttributes<HTMLDivElement>, 'className'>
8
+ type Span = Omit<HTMLAttributes<HTMLSpanElement>, 'className'>
9
+
10
+ export interface CardProps extends Div {
11
+ /** Retrait intérieur. Sans, la carte est nue — à toi de poser le tien. */
12
+ padding?: 'aucun' | 'normal' | 'large'
13
+ /** Filet coloré : un état porté par la bordure, pas par un fond. */
14
+ etat?: 'ok' | 'err'
15
+ /** Ombre portée, pour une carte qui flotte au-dessus d'une scène. */
16
+ flottante?: boolean
17
+ className?: string
18
+ }
19
+
20
+ export function Card({
21
+ padding = 'normal',
22
+ etat,
23
+ flottante,
24
+ className,
25
+ ...reste
26
+ }: CardProps) {
27
+ return (
28
+ <div
29
+ className={cx(
30
+ 'zv-card',
31
+ padding === 'normal' && 'zv-card--pad',
32
+ padding === 'large' && 'zv-card--pad-lg',
33
+ etat === 'ok' && 'zv-card--ok',
34
+ etat === 'err' && 'zv-card--err',
35
+ flottante && 'zv-card--float',
36
+ className,
37
+ )}
38
+ {...reste}
39
+ />
40
+ )
41
+ }
42
+
43
+ /** Surface semi-opaque. ⚠️ Ne fonctionne QUE devant un halo ou une scène :
44
+ * posée sur du papier nu, elle disparaît. */
45
+ export function Glass({ className, ...reste }: Div & { className?: string }) {
46
+ return <div className={cx('zv-glass', className)} {...reste} />
47
+ }
48
+
49
+ export interface InsetProps extends Div {
50
+ /** Variante document : fond panel, texte encre, très aérée. */
51
+ discret?: boolean
52
+ /** Poussé en bas de la carte qui l'accueille. */
53
+ enBas?: boolean
54
+ className?: string
55
+ }
56
+
57
+ /** Encart dans une carte. Deux objets distincts au relevé, longtemps
58
+ * confondus : la citation d'interface et l'extrait de document. */
59
+ export function Inset({ discret, enBas, className, ...reste }: InsetProps) {
60
+ return (
61
+ <div
62
+ className={cx(
63
+ 'zv-inset',
64
+ discret && 'zv-inset--quiet',
65
+ enBas && 'zv-inset--bottom',
66
+ className,
67
+ )}
68
+ {...reste}
69
+ />
70
+ )
71
+ }
72
+
73
+ export function Pill({ className, ...reste }: Span & { className?: string }) {
74
+ return <span className={cx('zv-pill', className)} {...reste} />
75
+ }
76
+
77
+ export interface SrcProps extends Span {
78
+ /** Le « + 11 » qui clôt une liste de sources. */
79
+ attenue?: boolean
80
+ className?: string
81
+ }
82
+
83
+ /** Puce de source — BOSS, Judilibre, KALI. */
84
+ export function Src({ attenue, className, ...reste }: SrcProps) {
85
+ return (
86
+ <span
87
+ className={cx('zv-src', attenue && 'zv-src--quiet', className)}
88
+ {...reste}
89
+ />
90
+ )
91
+ }
92
+
93
+ export function SrcRow({ className, ...reste }: Span & { className?: string }) {
94
+ return <span className={cx('zv-src-row', className)} {...reste} />
95
+ }
96
+
97
+ export interface TagProps extends Span {
98
+ etat?: 'ok' | 'err'
99
+ /** Sur la bande manifeste : les états s'éclaircissent, sinon ils virent au noir. */
100
+ surSombre?: boolean
101
+ className?: string
102
+ }
103
+
104
+ export function Tag({ etat, surSombre, className, ...reste }: TagProps) {
105
+ return (
106
+ <span
107
+ className={cx(
108
+ 'zv-tag',
109
+ etat === 'ok' && (surSombre ? 'zv-tag--ok-on-dark' : 'zv-tag--ok'),
110
+ etat === 'err' && (surSombre ? 'zv-tag--err-on-dark' : 'zv-tag--err'),
111
+ className,
112
+ )}
113
+ {...reste}
114
+ />
115
+ )
116
+ }
117
+
118
+ /** Entité pseudonymisée, posée dans le fil du texte. */
119
+ export function Ent({ className, ...reste }: Span & { className?: string }) {
120
+ return <span className={cx('zv-ent', className)} {...reste} />
121
+ }
122
+
123
+ /** Le paragraphe qui accueille des entités : il respire davantage, sinon les
124
+ * jetons se télescopent d'une ligne à l'autre. */
125
+ export function EntText({ className, ...reste }: Span & { className?: string }) {
126
+ return <span className={cx('zv-ent-text', className)} {...reste} />
127
+ }
128
+
129
+ export interface DividerProps extends Div {
130
+ /** Filet plein plutôt que cheveu. */
131
+ marque?: boolean
132
+ surSombre?: boolean
133
+ className?: string
134
+ }
135
+
136
+ export function Divider({ marque, surSombre, className, ...reste }: DividerProps) {
137
+ return (
138
+ <div
139
+ className={cx(
140
+ 'zv-divider',
141
+ marque && 'zv-divider--strong',
142
+ surSombre && 'zv-divider--on-dark',
143
+ className,
144
+ )}
145
+ {...reste}
146
+ />
147
+ )
148
+ }
149
+
150
+ export interface QuoteProps extends Div {
151
+ auteur?: ReactNode
152
+ className?: string
153
+ children?: ReactNode
154
+ }
155
+
156
+ /** ⚠️ RÈGLE DE CHARTE : jamais de fausse citation. Tant qu'un verbatim client
157
+ * n'a pas été recueilli, ce bloc reste en réserve — un placeholder crédible
158
+ * finit toujours par partir en production. */
159
+ export function Quote({ auteur, className, children, ...reste }: QuoteProps) {
160
+ return (
161
+ <div className={cx('zv-quote', className)} {...reste}>
162
+ <span className="zv-spec zv-spec--on-dark" aria-hidden="true" />
163
+ <span className="zv-quote__text">{children}</span>
164
+ {auteur && <span className="zv-mono zv-mono--micro zv-mono--on-dark">{auteur}</span>}
165
+ </div>
166
+ )
167
+ }
168
+
169
+ export interface PriceProps extends Div {
170
+ montant: ReactNode
171
+ /** Le « HT · 1re année » accolé au montant. */
172
+ unite?: ReactNode
173
+ /** La mention sous la carte. */
174
+ note?: ReactNode
175
+ /** La formule recommandée — bandeau indigo. Trois cartes par page au plus. */
176
+ recommande?: boolean
177
+ className?: string
178
+ children?: ReactNode
179
+ }
180
+
181
+ export function Price({
182
+ montant,
183
+ unite,
184
+ note,
185
+ recommande,
186
+ className,
187
+ children,
188
+ ...reste
189
+ }: PriceProps) {
190
+ return (
191
+ <div
192
+ className={cx('zv-price', recommande && 'zv-price--featured', className)}
193
+ {...reste}
194
+ >
195
+ {children}
196
+ <span className="zv-price__amount">
197
+ {montant}
198
+ {unite && <span className="zv-price__unit"> {unite}</span>}
199
+ </span>
200
+ {note && <span className="zv-price__note">{note}</span>}
201
+ </div>
202
+ )
203
+ }
204
+
205
+ /* ─── Statistiques de carte ────────────────────────────────────────── */
206
+
207
+ export function MiniStat({ className, ...reste }: Div & { className?: string }) {
208
+ return <div className={cx('zv-ministat', className)} {...reste} />
209
+ }
210
+
211
+ export interface MiniStatItemProps extends Div {
212
+ valeur: ReactNode
213
+ className?: string
214
+ children?: ReactNode
215
+ }
216
+
217
+ /** Paire de mini-statistiques au pied d'une carte produit. */
218
+ export function MiniStatItem({ valeur, className, children, ...reste }: MiniStatItemProps) {
219
+ return (
220
+ <div className={cx('zv-ministat__item', className)} {...reste}>
221
+ <span className="zv-ministat__value">{valeur}</span>
222
+ {children}
223
+ </div>
224
+ )
225
+ }
226
+
227
+ export interface StatCardProps extends Div {
228
+ valeur: ReactNode
229
+ className?: string
230
+ children?: ReactNode
231
+ }
232
+
233
+ export function StatCard({ valeur, className, children, ...reste }: StatCardProps) {
234
+ return (
235
+ <div className={cx('zv-stat-card', className)} {...reste}>
236
+ <span className="zv-stat-card__value">{valeur}</span>
237
+ {children}
238
+ </div>
239
+ )
240
+ }
241
+
242
+ /* ─── FAQ ──────────────────────────────────────────────────────────── */
243
+
244
+ /** ⚠️ Cinq questions par page au MAXIMUM. Au-delà, ce n'est plus une FAQ,
245
+ * c'est une documentation mal rangée. */
246
+ export function Faq({ className, ...reste }: Div & { className?: string }) {
247
+ return <div className={cx('zv-faq', className)} {...reste} />
248
+ }
249
+
250
+ export interface FaqItemProps extends Div {
251
+ question: ReactNode
252
+ className?: string
253
+ children?: ReactNode
254
+ }
255
+
256
+ /** Rendu ouvert, sans repli : la réponse est le contenu, la cacher derrière
257
+ * un clic n'ajoute rien à cinq questions. Pour un vrai accordéon, c'est un
258
+ * `<details>` — et il n'existe pas de style pour lui dans le paquet. */
259
+ export function FaqItem({ question, className, children, ...reste }: FaqItemProps) {
260
+ return (
261
+ <div className={className} {...reste}>
262
+ <p className="zv-faq__q">{question}</p>
263
+ <p className="zv-faq__a">{children}</p>
264
+ </div>
265
+ )
266
+ }
267
+
268
+ /* ─── Aperçu d'interface ───────────────────────────────────────────── */
269
+
270
+ export interface UiPreviewProps extends Div {
271
+ question: ReactNode
272
+ /** Le compteur de sources, seul élément mis en avant de la réponse. */
273
+ sources?: ReactNode
274
+ /** Les puces de source, sous la réponse. */
275
+ puces?: ReactNode
276
+ className?: string
277
+ children?: ReactNode
278
+ }
279
+
280
+ /** La preuve produit : une question, une réponse, ses sources.
281
+ * ⚠️ RÈGLE DE CHARTE : toujours de VRAIES données réalistes, jamais de
282
+ * lorem — c'est ce bloc qui porte la crédibilité de la page. */
283
+ export function UiPreview({
284
+ question,
285
+ sources,
286
+ puces,
287
+ className,
288
+ children,
289
+ ...reste
290
+ }: UiPreviewProps) {
291
+ return (
292
+ <div className={cx('zv-ui-preview', className)} {...reste}>
293
+ <p className="zv-ui-preview__q">{question}</p>
294
+ <p className="zv-ui-preview__a">{children}</p>
295
+ {sources && <span className="zv-ui-preview__count">{sources}</span>}
296
+ {puces}
297
+ </div>
298
+ )
299
+ }
300
+
301
+ /* ─── Pastille de validation ───────────────────────────────────────── */
302
+
303
+ /** ⚠️ À ne pas confondre avec `Checkbox` (forms) : celle-ci n'est PAS un
304
+ * contrôle, c'est une coche décorative en tête de liste d'arguments. */
305
+ export function Check({ className, ...reste }: Span & { className?: string }) {
306
+ return (
307
+ <span className={cx('zv-check', className)} aria-hidden="true" {...reste}>
308
+ <svg viewBox="0 0 12 12" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" width="12" height="12">
309
+ <polyline points="2.5,6.5 5,9 9.5,3.5" />
310
+ </svg>
311
+ </span>
312
+ )
313
+ }
314
+
315
+ /* ─── Blocs éditoriaux ─────────────────────────────────────────────────
316
+ * HORS RELEVÉ — ajoutés à la demande de la DA (règle de variété du
317
+ * 24/08/2026 : jamais deux blocs de même forme qui se suivent, 80 % de
318
+ * formes différentes par page). Quatre façons de poser du contenu SANS
319
+ * carte, pour que les grilles de cartes redeviennent un choix et non un
320
+ * réflexe. CSS relevé de la PR #1 (Enrase). */
321
+
322
+ export function Rows({ className, ...reste }: Div & { className?: string }) {
323
+ return <div className={cx('zv-rows', className)} {...reste} />
324
+ }
325
+
326
+ export interface RowsItemProps extends Div {
327
+ /** Le libellé fort, à gauche. Colonne fixe : c'est l'alignement d'une
328
+ * rangée à l'autre qui fait la lecture. */
329
+ cle: ReactNode
330
+ className?: string
331
+ children?: ReactNode
332
+ }
333
+
334
+ /** Rangée à filets : libellé fort + contenu, un trait fin entre chaque.
335
+ * Pour les listes où l'alignement fait la lecture (profils, prestations,
336
+ * caractéristiques). */
337
+ export function RowsItem({ cle, className, children, ...reste }: RowsItemProps) {
338
+ return (
339
+ <div className={cx('zv-rows__row', className)} {...reste}>
340
+ <span className="zv-rows__key">{cle}</span>
341
+ <span className="zv-rows__val">{children}</span>
342
+ </div>
343
+ )
344
+ }
345
+
346
+ export interface IndexListProps extends Div {
347
+ /** Grille de trois, sans filets — l'espace fait la séparation. */
348
+ cols?: boolean
349
+ className?: string
350
+ }
351
+
352
+ /** Sommaire numéroté éditorial. Pour les suites longues qui étoufferaient
353
+ * en cartes (offres détaillées, programmes). */
354
+ export function IndexList({ cols, className, ...reste }: IndexListProps) {
355
+ return (
356
+ <div className={cx('zv-index', cols && 'zv-index--cols', className)} {...reste} />
357
+ )
358
+ }
359
+
360
+ export interface IndexItemProps extends Div {
361
+ /** Le numéro, en display léger (« 01 »). C'est du CONTENU, pas un
362
+ * compteur CSS : un sommaire qui saute une entrée doit pouvoir le dire. */
363
+ num: ReactNode
364
+ className?: string
365
+ children?: ReactNode
366
+ }
367
+
368
+ export function IndexItem({ num, className, children, ...reste }: IndexItemProps) {
369
+ return (
370
+ <div className={cx('zv-index__item', className)} {...reste}>
371
+ <span className="zv-index__num" aria-hidden="true">
372
+ {num}
373
+ </span>
374
+ <div className="zv-index__body">{children}</div>
375
+ </div>
376
+ )
377
+ }
378
+
379
+ /** Déroulé vertical — la déclinaison verticale de `Steps`, pour les
380
+ * méthodes qui se lisent comme un récit. */
381
+ export function Flow({ className, ...reste }: Div & { className?: string }) {
382
+ return <div className={cx('zv-flow', className)} {...reste} />
383
+ }
384
+
385
+ export interface FlowStepProps extends Div {
386
+ /** Les points reprennent les couleurs des points de méthode — l'un des
387
+ * trois emplois autorisés du spectre. Sans teinte : iris. */
388
+ teinte?: 'violet' | 'cyan'
389
+ className?: string
390
+ }
391
+
392
+ export function FlowStep({ teinte, className, ...reste }: FlowStepProps) {
393
+ return (
394
+ <div
395
+ className={cx('zv-flow__step', teinte && `zv-flow__step--${teinte}`, className)}
396
+ {...reste}
397
+ />
398
+ )
399
+ }
400
+
401
+ export interface CalloutProps extends Div {
402
+ titre: ReactNode
403
+ className?: string
404
+ children?: ReactNode
405
+ }
406
+
407
+ /** Encart d'appel sur accent-soft : l'offre transverse qui doit se voir
408
+ * sans crier (remise, renvoi vers une autre page). L'action se passe en
409
+ * enfant — elle se range à droite, et tombe sous le titre sur mobile. */
410
+ export function Callout({ titre, className, children, ...reste }: CalloutProps) {
411
+ return (
412
+ <div className={cx('zv-callout', className)} {...reste}>
413
+ <h3 className="zv-callout__title">{titre}</h3>
414
+ {children}
415
+ </div>
416
+ )
417
+ }