@zevra/ui 0.4.0 → 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.
- package/README.md +64 -1
- package/dist/actions.d.ts +41 -0
- package/dist/actions.d.ts.map +1 -0
- package/dist/actions.js +24 -0
- package/dist/actions.js.map +1 -0
- package/dist/catalogue.d.ts +30 -0
- package/dist/catalogue.d.ts.map +1 -0
- package/dist/catalogue.js +830 -0
- package/dist/catalogue.js.map +1 -0
- package/dist/content.d.ts +135 -0
- package/dist/content.d.ts.map +1 -0
- package/dist/content.js +85 -0
- package/dist/content.js.map +1 -0
- package/dist/css-inline.d.ts +11 -0
- package/dist/css-inline.d.ts.map +1 -0
- package/dist/css-inline.js +13 -0
- package/dist/css-inline.js.map +1 -0
- package/dist/cx.d.ts +16 -0
- package/dist/cx.d.ts.map +1 -0
- package/dist/cx.js +20 -0
- package/dist/cx.js.map +1 -0
- package/dist/data.d.ts +136 -0
- package/dist/data.d.ts.map +1 -0
- package/dist/data.js +98 -0
- package/dist/data.js.map +1 -0
- package/dist/feedback.d.ts +88 -0
- package/dist/feedback.d.ts.map +1 -0
- package/dist/feedback.js +38 -0
- package/dist/feedback.js.map +1 -0
- package/dist/forms.d.ts +120 -0
- package/dist/forms.d.ts.map +1 -0
- package/dist/forms.js +60 -0
- package/dist/forms.js.map +1 -0
- package/dist/icon-data.d.ts +6 -0
- package/dist/icon-data.d.ts.map +1 -0
- package/dist/icon-data.js +77 -0
- package/dist/icon-data.js.map +1 -0
- package/dist/icon.d.ts +33 -0
- package/dist/icon.d.ts.map +1 -0
- package/dist/icon.js +13 -0
- package/dist/icon.js.map +1 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +41 -0
- package/dist/index.js.map +1 -0
- package/dist/layout.d.ts +166 -0
- package/dist/layout.d.ts.map +1 -0
- package/dist/layout.js +120 -0
- package/dist/layout.js.map +1 -0
- package/dist/navigation.d.ts +150 -0
- package/dist/navigation.d.ts.map +1 -0
- package/dist/navigation.js +104 -0
- package/dist/navigation.js.map +1 -0
- package/dist/overlays.d.ts +68 -0
- package/dist/overlays.d.ts.map +1 -0
- package/dist/overlays.js +114 -0
- package/dist/overlays.js.map +1 -0
- package/dist/surfaces.d.ts +61 -0
- package/dist/surfaces.d.ts.map +1 -0
- package/dist/surfaces.js +54 -0
- package/dist/surfaces.js.map +1 -0
- package/dist/typography.d.ts +89 -0
- package/dist/typography.d.ts.map +1 -0
- package/dist/typography.js +76 -0
- package/dist/typography.js.map +1 -0
- package/package.json +36 -4
- package/react/actions.tsx +138 -0
- package/react/catalogue.tsx +1644 -0
- package/react/content.tsx +313 -0
- package/react/css-inline.ts +14 -0
- package/react/cx.ts +20 -0
- package/react/data.tsx +300 -0
- package/react/feedback.tsx +229 -0
- package/react/forms.tsx +383 -0
- package/react/icon-data.ts +114 -0
- package/react/icon.tsx +84 -0
- package/react/index.ts +42 -0
- package/react/layout.tsx +347 -0
- package/react/navigation.tsx +329 -0
- package/react/overlays.tsx +237 -0
- package/react/surfaces.tsx +161 -0
- package/react/typography.tsx +220 -0
package/react/forms.tsx
ADDED
|
@@ -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'
|