@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.
- package/DECISIONS.md +25 -0
- package/README.md +83 -11
- 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/logos/index.json +10 -500
- 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/logos/png/campus-glyph-ink.png +0 -0
- package/logos/png/campus-glyph-white.png +0 -0
- package/logos/png/campus-lockup.png +0 -0
- package/logos/png/lexform-glyph-ink.png +0 -0
- package/logos/png/lexform-glyph-white.png +0 -0
- package/logos/png/lexform-lockup.png +0 -0
- package/logos/png/mcp-glyph-ink.png +0 -0
- package/logos/png/mcp-glyph-white.png +0 -0
- package/logos/png/mcp-lockup.png +0 -0
- package/logos/png/memoire-dark.png +0 -0
- package/logos/png/memoire-glyph-ink.png +0 -0
- package/logos/png/memoire-glyph-white.png +0 -0
- package/logos/png/memoire-lockup.png +0 -0
- package/logos/png/zevra-lockup-ink.png +0 -0
- package/logos/png/zevra-lockup-white.png +0 -0
- package/logos/png/zevra-lockup.png +0 -0
- package/logos/png/zevra-mark-ink.png +0 -0
- package/logos/png/zevra-mark-white.png +0 -0
- package/logos/png/zevra-mark.png +0 -0
- package/logos/svg/campus-glyph-ink.svg +0 -1
- package/logos/svg/campus-glyph-navy.svg +0 -1
- package/logos/svg/campus-glyph-orig.svg +0 -1
- package/logos/svg/campus-glyph-white.svg +0 -1
- package/logos/svg/campus-lockup-ink.svg +0 -1
- package/logos/svg/campus-lockup-navy.svg +0 -1
- package/logos/svg/campus-lockup-orig.svg +0 -1
- package/logos/svg/campus-lockup-white.svg +0 -1
- package/logos/svg/lexform-glyph-ink.svg +0 -1
- package/logos/svg/lexform-glyph-navy.svg +0 -1
- package/logos/svg/lexform-glyph-orig.svg +0 -1
- package/logos/svg/lexform-glyph-white.svg +0 -1
- package/logos/svg/lexform-lockup-ink.svg +0 -1
- package/logos/svg/lexform-lockup-navy.svg +0 -1
- package/logos/svg/lexform-lockup-orig.svg +0 -1
- package/logos/svg/lexform-lockup-white.svg +0 -1
- package/logos/svg/mcp-glyph-ink.svg +0 -1
- package/logos/svg/mcp-glyph-navy.svg +0 -1
- package/logos/svg/mcp-glyph-orig.svg +0 -1
- package/logos/svg/mcp-glyph-white.svg +0 -1
- package/logos/svg/mcp-lockup-ink.svg +0 -1
- package/logos/svg/mcp-lockup-navy.svg +0 -1
- package/logos/svg/mcp-lockup-orig.svg +0 -1
- package/logos/svg/mcp-lockup-white.svg +0 -1
- package/logos/svg/memoire-glyph-ink.svg +0 -1
- package/logos/svg/memoire-glyph-navy.svg +0 -1
- package/logos/svg/memoire-glyph-orig.svg +0 -1
- package/logos/svg/memoire-glyph-white.svg +0 -1
- package/logos/svg/memoire-lockup-ink.svg +0 -1
- package/logos/svg/memoire-lockup-navy.svg +0 -1
- package/logos/svg/memoire-lockup-orig.svg +0 -1
- package/logos/svg/memoire-lockup-white.svg +0 -1
- package/logos/svg/zevra-lockup-ink.svg +0 -1
- package/logos/svg/zevra-lockup-navy.svg +0 -1
- package/logos/svg/zevra-lockup-orig.svg +0 -1
- package/logos/svg/zevra-lockup-white.svg +0 -1
- package/logos/svg/zevra-mark-ink.svg +0 -1
- package/logos/svg/zevra-mark-navy.svg +0 -1
- package/logos/svg/zevra-mark-orig.svg +0 -1
- package/logos/svg/zevra-mark-white.svg +0 -1
package/react/data.tsx
ADDED
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
// Données — tableau en grille, listes, métadonnées, clé-valeur, vignettes.
|
|
2
|
+
//
|
|
3
|
+
// Le tableau du système n'est PAS un <table> : c'est une grille CSS, parce
|
|
4
|
+
// qu'une même rangée doit se rendre en colonnes au-dessus du point de rupture
|
|
5
|
+
// et en carte empilée en dessous, sans dupliquer le balisage. `Unwrap` est la
|
|
6
|
+
// primitive qui rend cela possible.
|
|
7
|
+
|
|
8
|
+
import type { AnchorHTMLAttributes, CSSProperties, HTMLAttributes, ReactNode } from 'react'
|
|
9
|
+
import { cx } from './cx.js'
|
|
10
|
+
|
|
11
|
+
type Div = Omit<HTMLAttributes<HTMLDivElement>, 'className'>
|
|
12
|
+
type Span = Omit<HTMLAttributes<HTMLSpanElement>, 'className'>
|
|
13
|
+
|
|
14
|
+
/** `--zv-cols` est un PARAMÈTRE de grille, pas un token de marque : il se
|
|
15
|
+
* pose en style inline sur le cadre, et l'en-tête comme les lignes en
|
|
16
|
+
* héritent. Une seule déclaration pour les deux — recopié aux deux
|
|
17
|
+
* endroits, on l'oublie à l'un et les intitulés se décalent des valeurs
|
|
18
|
+
* sans que rien ne casse visiblement. */
|
|
19
|
+
function cols(gabarit: string | undefined, style: CSSProperties | undefined) {
|
|
20
|
+
return gabarit ? ({ ...style, ['--zv-cols']: gabarit } as CSSProperties) : style
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/* ─── Tableau ──────────────────────────────────────────────────────── */
|
|
24
|
+
|
|
25
|
+
export interface TableProps extends Div {
|
|
26
|
+
/** Gabarit de colonnes, en syntaxe `grid-template-columns`
|
|
27
|
+
* (ex. `'minmax(0,2fr) 120px 90px'`). Hérité par l'en-tête et les lignes. */
|
|
28
|
+
gabarit?: string
|
|
29
|
+
className?: string
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Le tableau défile DANS son cadre : sans ce confinement, une colonne trop
|
|
33
|
+
* large fait défiler la page entière et le rail de navigation part avec. */
|
|
34
|
+
export function Table({ gabarit, className, style, ...reste }: TableProps) {
|
|
35
|
+
return (
|
|
36
|
+
<div className={cx('zv-table', className)} style={cols(gabarit, style)} {...reste} />
|
|
37
|
+
)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface TableHeadProps extends Div {
|
|
41
|
+
/** En-tête en retrait, pour un tableau posé dans une carte. */
|
|
42
|
+
inset?: boolean
|
|
43
|
+
className?: string
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function TableHead({ inset, className, ...reste }: TableHeadProps) {
|
|
47
|
+
return (
|
|
48
|
+
<div
|
|
49
|
+
className={cx('zv-table__head', inset && 'zv-table__head--inset', className)}
|
|
50
|
+
{...reste}
|
|
51
|
+
/>
|
|
52
|
+
)
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export interface RowProps extends Div {
|
|
56
|
+
/** Rend la rangée comme un lien : la cible entière devient cliquable, ce
|
|
57
|
+
* qu'un `onClick` sur un `<div>` ne donne jamais (ni clavier, ni ouverture
|
|
58
|
+
* dans un nouvel onglet). */
|
|
59
|
+
href?: string
|
|
60
|
+
className?: string
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function Row({ href, className, ...reste }: RowProps) {
|
|
64
|
+
if (href) {
|
|
65
|
+
return (
|
|
66
|
+
<a
|
|
67
|
+
href={href}
|
|
68
|
+
className={cx('zv-row', className)}
|
|
69
|
+
{...(reste as AnchorHTMLAttributes<HTMLAnchorElement>)}
|
|
70
|
+
/>
|
|
71
|
+
)
|
|
72
|
+
}
|
|
73
|
+
return <div className={cx('zv-row', className)} {...reste} />
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export interface CellProps extends Div {
|
|
77
|
+
/** `nom` : la colonne d'identité, la seule en encre pleine.
|
|
78
|
+
* `num` : chiffres alignés à droite, en chasse tabulaire. */
|
|
79
|
+
registre?: 'nom' | 'num'
|
|
80
|
+
/** Intitulé qui REMPLACE l'en-tête quand celui-ci disparaît (mobile). */
|
|
81
|
+
label?: ReactNode
|
|
82
|
+
/** Unité accolée au nombre, en plus petit. */
|
|
83
|
+
unite?: ReactNode
|
|
84
|
+
className?: string
|
|
85
|
+
children?: ReactNode
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function Cell({ registre, label, unite, className, children, ...reste }: CellProps) {
|
|
89
|
+
return (
|
|
90
|
+
<div
|
|
91
|
+
className={cx(
|
|
92
|
+
'zv-cell',
|
|
93
|
+
registre === 'nom' && 'zv-cell--name',
|
|
94
|
+
registre === 'num' && 'zv-cell--num',
|
|
95
|
+
className,
|
|
96
|
+
)}
|
|
97
|
+
{...reste}
|
|
98
|
+
>
|
|
99
|
+
{label && <span className="zv-cell__label">{label}</span>}
|
|
100
|
+
{children}
|
|
101
|
+
{unite && <span className="zv-cell__unit">{unite}</span>}
|
|
102
|
+
</div>
|
|
103
|
+
)
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** Groupe deux ou trois cellules pour l'empilement mobile, puis s'efface
|
|
107
|
+
* (`display: contents`) et rend ses enfants DIRECTEMENT à la grille.
|
|
108
|
+
* ⚠️ N'exprime QUE ce sens-là : le mouvement inverse (pile au-dessus du
|
|
109
|
+
* seuil, colonnes en dessous) n'est pas cette classe. */
|
|
110
|
+
export function Unwrap({ className, ...reste }: Div & { className?: string }) {
|
|
111
|
+
return <div className={cx('zv-unwrap', className)} {...reste} />
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/* ─── Liste sans colonnes ──────────────────────────────────────────── */
|
|
115
|
+
|
|
116
|
+
export function List({ className, ...reste }: Div & { className?: string }) {
|
|
117
|
+
return <div className={cx('zv-list', className)} {...reste} />
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export interface ListRowProps extends Div {
|
|
121
|
+
/** Rangée de dépôt en cours : gabarit propre à l'écran d'import. */
|
|
122
|
+
upload?: boolean
|
|
123
|
+
className?: string
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export function ListRow({ upload, className, ...reste }: ListRowProps) {
|
|
127
|
+
return (
|
|
128
|
+
<div
|
|
129
|
+
className={cx('zv-list__row', upload && 'zv-list__row--upload', className)}
|
|
130
|
+
{...reste}
|
|
131
|
+
/>
|
|
132
|
+
)
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export function ListMain({ className, ...reste }: Div & { className?: string }) {
|
|
136
|
+
return <div className={cx('zv-list__main', className)} {...reste} />
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export function ListName({ className, ...reste }: Span & { className?: string }) {
|
|
140
|
+
return <span className={cx('zv-list__name', className)} {...reste} />
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/* ─── Métadonnée mono ──────────────────────────────────────────────── */
|
|
144
|
+
|
|
145
|
+
export interface MetaProps extends Span {
|
|
146
|
+
taille?: 'sm' | 'normal' | 'md'
|
|
147
|
+
/** Encore plus effacée : contexte de second plan. */
|
|
148
|
+
discret?: boolean
|
|
149
|
+
className?: string
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/** Second registre mono du système, en BAS DE CASSE : taille de fichier,
|
|
153
|
+
* date, horodatage, latence. Ni capitales ni interlettrage — c'est ce qui le
|
|
154
|
+
* distingue de `Tag` et `Mono`, et une date en capitales espacées devient
|
|
155
|
+
* illisible. */
|
|
156
|
+
export function Meta({ taille = 'normal', discret, className, ...reste }: MetaProps) {
|
|
157
|
+
return (
|
|
158
|
+
<span
|
|
159
|
+
className={cx(
|
|
160
|
+
'zv-meta',
|
|
161
|
+
taille === 'sm' && 'zv-meta--sm',
|
|
162
|
+
taille === 'md' && 'zv-meta--md',
|
|
163
|
+
discret && 'zv-meta--quiet',
|
|
164
|
+
className,
|
|
165
|
+
)}
|
|
166
|
+
{...reste}
|
|
167
|
+
/>
|
|
168
|
+
)
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
export function MetaRow({ className, ...reste }: Div & { className?: string }) {
|
|
172
|
+
return <div className={cx('zv-meta-row', className)} {...reste} />
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/* ─── Clé-valeur ───────────────────────────────────────────────────── */
|
|
176
|
+
|
|
177
|
+
export interface KVProps extends Div {
|
|
178
|
+
/** `grid` : deux colonnes alignées. `foot` : bloc de pied de fiche. */
|
|
179
|
+
variante?: 'grid' | 'foot'
|
|
180
|
+
className?: string
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
export function KV({ variante, className, ...reste }: KVProps) {
|
|
184
|
+
return (
|
|
185
|
+
<div className={cx('zv-kv', variante && `zv-kv--${variante}`, className)} {...reste} />
|
|
186
|
+
)
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
export interface KVRowProps extends Div {
|
|
190
|
+
/** ⚠️ La CLÉ est en mono, en accent, et se coupe ; la VALEUR est en texte
|
|
191
|
+
* courant, en gris, et ne se coupe jamais. Inverser les deux change ce que
|
|
192
|
+
* l'œil balaie en premier dans une colonne de métadonnées. */
|
|
193
|
+
cle: ReactNode
|
|
194
|
+
className?: string
|
|
195
|
+
children?: ReactNode
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
export function KVRow({ cle, className, children, ...reste }: KVRowProps) {
|
|
199
|
+
return (
|
|
200
|
+
<div className={cx('zv-kv__row', className)} {...reste}>
|
|
201
|
+
<span className="zv-kv__key">{cle}</span>
|
|
202
|
+
<span className="zv-kv__val">{children}</span>
|
|
203
|
+
</div>
|
|
204
|
+
)
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/* ─── Pastille d'initiales ─────────────────────────────────────────── */
|
|
208
|
+
|
|
209
|
+
export interface AvatarProps extends Span {
|
|
210
|
+
/** ⚠️ La teinte dit la NATURE de l'entité, elle n'est pas décorative :
|
|
211
|
+
* la changer, c'est changer le sens. */
|
|
212
|
+
ton?: 'accent' | 'ink' | 'soft'
|
|
213
|
+
petit?: boolean
|
|
214
|
+
className?: string
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/** CARRÉE, jamais ronde : le rond est un usage de réseau social, l'encre et
|
|
218
|
+
* le papier posent des blocs. */
|
|
219
|
+
export function Avatar({ ton, petit, className, ...reste }: AvatarProps) {
|
|
220
|
+
return (
|
|
221
|
+
<span
|
|
222
|
+
aria-hidden="true"
|
|
223
|
+
className={cx(
|
|
224
|
+
'zv-avatar',
|
|
225
|
+
ton && `zv-avatar--${ton}`,
|
|
226
|
+
petit && 'zv-avatar--sm',
|
|
227
|
+
className,
|
|
228
|
+
)}
|
|
229
|
+
{...reste}
|
|
230
|
+
/>
|
|
231
|
+
)
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/* ─── Troncature ───────────────────────────────────────────────────── */
|
|
235
|
+
|
|
236
|
+
/** Coupe sur une ligne avec des points de suspension. Jamais interchangeable
|
|
237
|
+
* avec `Break`, qui revient à la ligne. */
|
|
238
|
+
export function Trunc({ className, ...reste }: Span & { className?: string }) {
|
|
239
|
+
return <span className={cx('zv-trunc', className)} {...reste} />
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/** Revient à la ligne au milieu d'un mot : jetons, URLs, chemins. */
|
|
243
|
+
export function Break({ className, ...reste }: Span & { className?: string }) {
|
|
244
|
+
return <span className={cx('zv-break', className)} {...reste} />
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/* ─── Étiquette de catégorie ───────────────────────────────────────── */
|
|
248
|
+
|
|
249
|
+
export interface ChipProps extends Span {
|
|
250
|
+
/** `mono` : identifiant, code. `outline` : filet au lieu du fond. */
|
|
251
|
+
variante?: 'mono' | 'outline'
|
|
252
|
+
className?: string
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/** Étiquette NON porteuse d'alerte : nature d'entité, matière, mot-clé.
|
|
256
|
+
* À ne pas confondre avec `Badge` (couleur sémantique) ni `Tag` (kicker
|
|
257
|
+
* éditorial). Une catégorie n'a pas à crier. */
|
|
258
|
+
export function Chip({ variante, className, ...reste }: ChipProps) {
|
|
259
|
+
return (
|
|
260
|
+
<span
|
|
261
|
+
className={cx('zv-chip', variante && `zv-chip--${variante}`, className)}
|
|
262
|
+
{...reste}
|
|
263
|
+
/>
|
|
264
|
+
)
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
export function ChipRow({ className, ...reste }: Div & { className?: string }) {
|
|
268
|
+
return <div className={cx('zv-chip-row', className)} {...reste} />
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/* ─── Citation ─────────────────────────────────────────────────────── */
|
|
272
|
+
|
|
273
|
+
export function Cite({
|
|
274
|
+
className,
|
|
275
|
+
...reste
|
|
276
|
+
}: Omit<HTMLAttributes<HTMLButtonElement>, 'className'> & { className?: string }) {
|
|
277
|
+
return <button type="button" className={cx('zv-cite', className)} {...reste} />
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
export function CiteRow({ className, ...reste }: Div & { className?: string }) {
|
|
281
|
+
return <div className={cx('zv-cite-row', className)} {...reste} />
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/* ─── Vignette de format ───────────────────────────────────────────── */
|
|
285
|
+
|
|
286
|
+
export interface FileTypeProps extends Span {
|
|
287
|
+
/** Règle de choix relevée : DOCX si le type MIME contient « word »,
|
|
288
|
+
* PDF sinon. Les couleurs ne viennent PAS de la charte — ce sont celles
|
|
289
|
+
* que le monde associe à ces formats. */
|
|
290
|
+
format: 'pdf' | 'docx'
|
|
291
|
+
className?: string
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
export function FileType({ format, className, ...reste }: FileTypeProps) {
|
|
295
|
+
return (
|
|
296
|
+
<span className={cx('zv-filetype', `zv-filetype--${format}`, className)} {...reste}>
|
|
297
|
+
{format.toUpperCase()}
|
|
298
|
+
</span>
|
|
299
|
+
)
|
|
300
|
+
}
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
// Retours — bandeaux d'état, étiquettes, pastilles, absence, travail en cours.
|
|
2
|
+
//
|
|
3
|
+
// Rien ici n'est interactif : pas de fermeture, pas de survol. Aucun de ces
|
|
4
|
+
// composants n'a besoin de tourner côté navigateur.
|
|
5
|
+
//
|
|
6
|
+
// ⚠️ Ce fichier ne livre NI roue de chargement, NI squelette, NI infobulle :
|
|
7
|
+
// le CSS n'en a pas, parce que le relevé de production n'en contenait aucun.
|
|
8
|
+
// Le travail en cours se dit par `Loading`, qui nomme ce qu'on attend — une
|
|
9
|
+
// roue dit « attendez », cette phrase dit sur quoi.
|
|
10
|
+
|
|
11
|
+
import type { HTMLAttributes, ReactNode } from 'react'
|
|
12
|
+
import { cx } from './cx.js'
|
|
13
|
+
|
|
14
|
+
type Div = Omit<HTMLAttributes<HTMLDivElement>, 'className'>
|
|
15
|
+
type Span = Omit<HTMLAttributes<HTMLSpanElement>, 'className'>
|
|
16
|
+
|
|
17
|
+
/* ─── Bandeaux d'état ──────────────────────────────────────────────── */
|
|
18
|
+
|
|
19
|
+
export type AlertTon = 'info' | 'ok' | 'warn' | 'err' | 'dash'
|
|
20
|
+
|
|
21
|
+
export interface AlertProps extends Div {
|
|
22
|
+
/** OBLIGATOIRE : sans ton, le bandeau hérite d'un filet en `currentColor`,
|
|
23
|
+
* soit un trait d'encre autour d'un fond blanc. */
|
|
24
|
+
ton: AlertTon
|
|
25
|
+
/** `sm` resserre, `lg` amplifie — ce dernier pour le message qui REMPLACE
|
|
26
|
+
* un contenu attendu (incident, réponse impossible). */
|
|
27
|
+
taille?: 'sm' | 'normal' | 'lg'
|
|
28
|
+
className?: string
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function Alert({ ton, taille = 'normal', className, ...reste }: AlertProps) {
|
|
32
|
+
return (
|
|
33
|
+
<div
|
|
34
|
+
role="alert"
|
|
35
|
+
className={cx(
|
|
36
|
+
'zv-alert',
|
|
37
|
+
`zv-alert--${ton}`,
|
|
38
|
+
taille === 'sm' && 'zv-alert--sm',
|
|
39
|
+
taille === 'lg' && 'zv-alert--lg',
|
|
40
|
+
className,
|
|
41
|
+
)}
|
|
42
|
+
{...reste}
|
|
43
|
+
/>
|
|
44
|
+
)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/* ─── Étiquettes d'état ────────────────────────────────────────────── */
|
|
48
|
+
|
|
49
|
+
export type BadgeTon = 'neutre' | 'accent' | 'ok' | 'warn' | 'err'
|
|
50
|
+
|
|
51
|
+
export interface BadgeProps extends Span {
|
|
52
|
+
ton?: BadgeTon
|
|
53
|
+
/** `micro` : niveau de log en tête de ligne, badge de coque.
|
|
54
|
+
* `state` : état de service (OK / CHARGE). */
|
|
55
|
+
gabarit?: 'micro' | 'state'
|
|
56
|
+
/** Pastille en tête, de la couleur du texte. */
|
|
57
|
+
pastille?: boolean
|
|
58
|
+
className?: string
|
|
59
|
+
children?: ReactNode
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** Le libellé s'écrit EN CAPITALES dans le contenu, pas en CSS : transformé
|
|
63
|
+
* par le navigateur, « INDEXÉ » se casse selon la locale. */
|
|
64
|
+
export function Badge({
|
|
65
|
+
ton = 'neutre',
|
|
66
|
+
gabarit,
|
|
67
|
+
pastille,
|
|
68
|
+
className,
|
|
69
|
+
children,
|
|
70
|
+
...reste
|
|
71
|
+
}: BadgeProps) {
|
|
72
|
+
return (
|
|
73
|
+
<span
|
|
74
|
+
className={cx(
|
|
75
|
+
'zv-badge',
|
|
76
|
+
`zv-badge--${ton}`,
|
|
77
|
+
gabarit === 'micro' && 'zv-badge--micro',
|
|
78
|
+
gabarit === 'state' && 'zv-badge--state',
|
|
79
|
+
pastille && 'zv-badge--dot',
|
|
80
|
+
className,
|
|
81
|
+
)}
|
|
82
|
+
{...reste}
|
|
83
|
+
>
|
|
84
|
+
{pastille && <span className="zv-dot" aria-hidden="true" />}
|
|
85
|
+
{children}
|
|
86
|
+
</span>
|
|
87
|
+
)
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/* ─── Pastilles ────────────────────────────────────────────────────── */
|
|
91
|
+
|
|
92
|
+
export type DotEtat = 'recu' | 'ocr' | 'indexation' | 'indexe' | 'ok' | 'erreur'
|
|
93
|
+
|
|
94
|
+
export interface DotProps extends Span {
|
|
95
|
+
/** Sans état, la pastille reste GRISE : un statut inconnu — ajouté côté
|
|
96
|
+
* serveur, pas encore côté interface — ne doit pas passer pour un succès. */
|
|
97
|
+
etat?: DotEtat
|
|
98
|
+
taille?: 'sm' | 'normal' | 'lg'
|
|
99
|
+
/** CARRÉE : marque une SOURCE, pas un état. Ne pas uniformiser. */
|
|
100
|
+
carree?: boolean
|
|
101
|
+
className?: string
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export function Dot({ etat, taille = 'normal', carree, className, ...reste }: DotProps) {
|
|
105
|
+
return (
|
|
106
|
+
<span
|
|
107
|
+
aria-hidden="true"
|
|
108
|
+
className={cx(
|
|
109
|
+
'zv-dot',
|
|
110
|
+
etat && `zv-dot--${etat}`,
|
|
111
|
+
taille === 'sm' && 'zv-dot--sm',
|
|
112
|
+
taille === 'lg' && 'zv-dot--lg',
|
|
113
|
+
carree && 'zv-dot--square',
|
|
114
|
+
className,
|
|
115
|
+
)}
|
|
116
|
+
{...reste}
|
|
117
|
+
/>
|
|
118
|
+
)
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/* ─── Statut en clair ──────────────────────────────────────────────── */
|
|
122
|
+
|
|
123
|
+
export interface StatusProps extends Div {
|
|
124
|
+
etat?: DotEtat
|
|
125
|
+
/** Le message d'erreur REMPLACE le libellé quand il y en a un :
|
|
126
|
+
* l'utilisateur veut lire la cause, pas le mot « Erreur ». */
|
|
127
|
+
children?: ReactNode
|
|
128
|
+
className?: string
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export function Status({ etat, className, children, ...reste }: StatusProps) {
|
|
132
|
+
return (
|
|
133
|
+
<div className={cx('zv-status', className)} {...reste}>
|
|
134
|
+
<Dot etat={etat} />
|
|
135
|
+
<span className="zv-status__label">{children}</span>
|
|
136
|
+
</div>
|
|
137
|
+
)
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/* ─── Absence ──────────────────────────────────────────────────────── */
|
|
141
|
+
|
|
142
|
+
export interface EmptyProps extends Div {
|
|
143
|
+
/** ⚠️ DEUX OBJETS DISTINCTS. Sans `titre` c'est la LIGNE d'absence : un
|
|
144
|
+
* filtre trop étroit, la collection existe, l'en-tête du tableau reste
|
|
145
|
+
* visible. Avec `titre` et `page`, c'est l'ÉCRAN d'absence : la collection
|
|
146
|
+
* est vide et il faut lui offrir une porte de sortie. Les confondre
|
|
147
|
+
* affiche un écran de bienvenue à chaque filtre trop strict. */
|
|
148
|
+
titre?: ReactNode
|
|
149
|
+
page?: boolean
|
|
150
|
+
/** L'action qui remplira la collection. Réservée à l'écran d'absence. */
|
|
151
|
+
action?: ReactNode
|
|
152
|
+
className?: string
|
|
153
|
+
children?: ReactNode
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
export function Empty({ titre, page, action, className, children, ...reste }: EmptyProps) {
|
|
157
|
+
return (
|
|
158
|
+
<div className={cx('zv-empty', page && 'zv-empty--page', className)} {...reste}>
|
|
159
|
+
{titre && <p className="zv-empty__title">{titre}</p>}
|
|
160
|
+
{children && <p className="zv-empty__text">{children}</p>}
|
|
161
|
+
{action && <div className="zv-empty__action">{action}</div>}
|
|
162
|
+
</div>
|
|
163
|
+
)
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/* ─── Travail en cours ─────────────────────────────────────────────── */
|
|
167
|
+
|
|
168
|
+
export interface LoadingProps extends Div {
|
|
169
|
+
className?: string
|
|
170
|
+
children?: ReactNode
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/** Pastille + phrase qui dit le travail réel (« Recherche dans 4 812
|
|
174
|
+
* documents anonymisés… »). STATIQUE : la pastille ne bat pas, et lui
|
|
175
|
+
* ajouter un pouls est l'invention la plus tentante du système. */
|
|
176
|
+
export function Loading({ className, children, ...reste }: LoadingProps) {
|
|
177
|
+
return (
|
|
178
|
+
<div
|
|
179
|
+
role="status"
|
|
180
|
+
aria-live="polite"
|
|
181
|
+
className={cx('zv-loading', className)}
|
|
182
|
+
{...reste}
|
|
183
|
+
>
|
|
184
|
+
<span className="zv-dot" aria-hidden="true" />
|
|
185
|
+
{children}
|
|
186
|
+
</div>
|
|
187
|
+
)
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/** Bloc collé à la fin d'un texte en cours de diffusion, sur le DERNIER
|
|
191
|
+
* message seulement : sans lui, une réponse coupée à mi-phrase se lit comme
|
|
192
|
+
* une réponse complète et fausse. */
|
|
193
|
+
export function Caret({ className, ...reste }: Span & { className?: string }) {
|
|
194
|
+
return (
|
|
195
|
+
<span className={cx('zv-caret', className)} aria-hidden="true" {...reste}>
|
|
196
|
+
▍
|
|
197
|
+
</span>
|
|
198
|
+
)
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/* ─── Barre de progression ─────────────────────────────────────────── */
|
|
202
|
+
|
|
203
|
+
export interface ProgressProps extends Div {
|
|
204
|
+
/** Entre 0 et 1. Hors bornes, la valeur est ramenée dedans : une barre à
|
|
205
|
+
* 140 % déborde de son cadre. */
|
|
206
|
+
valeur: number
|
|
207
|
+
/** Ce que la barre mesure — annoncé, jamais dessiné. */
|
|
208
|
+
label?: string
|
|
209
|
+
className?: string
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/** Deuxième des trois emplois autorisés du spectre (avec le filet de section
|
|
213
|
+
* et les points de méthode). Jamais en fond, jamais en texte. */
|
|
214
|
+
export function Progress({ valeur, label, className, ...reste }: ProgressProps) {
|
|
215
|
+
const part = Math.min(1, Math.max(0, valeur))
|
|
216
|
+
return (
|
|
217
|
+
<div
|
|
218
|
+
role="progressbar"
|
|
219
|
+
aria-valuemin={0}
|
|
220
|
+
aria-valuemax={100}
|
|
221
|
+
aria-valuenow={Math.round(part * 100)}
|
|
222
|
+
aria-label={label}
|
|
223
|
+
className={cx('zv-progress', className)}
|
|
224
|
+
{...reste}
|
|
225
|
+
>
|
|
226
|
+
<div className="zv-progress__fill" style={{ width: `${part * 100}%` }} />
|
|
227
|
+
</div>
|
|
228
|
+
)
|
|
229
|
+
}
|