@zevra/ui 0.7.0 → 0.9.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 +227 -0
- package/dist/actions.d.ts +13 -3
- package/dist/actions.d.ts.map +1 -1
- package/dist/actions.js +9 -2
- package/dist/actions.js.map +1 -1
- package/dist/catalogue.d.ts.map +1 -1
- package/dist/catalogue.js +160 -11
- package/dist/catalogue.js.map +1 -1
- package/dist/css-inline.d.ts.map +1 -1
- package/dist/css-inline.js +1 -1
- package/dist/css-inline.js.map +1 -1
- package/dist/data-table.d.ts +30 -0
- package/dist/data-table.d.ts.map +1 -0
- package/dist/data-table.js +35 -0
- package/dist/data-table.js.map +1 -0
- package/dist/data.d.ts +34 -1
- package/dist/data.d.ts.map +1 -1
- package/dist/data.js +27 -0
- package/dist/data.js.map +1 -1
- package/dist/feedback.d.ts +49 -0
- package/dist/feedback.d.ts.map +1 -1
- package/dist/feedback.js +24 -0
- package/dist/feedback.js.map +1 -1
- package/dist/forms.d.ts +36 -0
- package/dist/forms.d.ts.map +1 -1
- package/dist/forms.js +24 -0
- package/dist/forms.js.map +1 -1
- package/dist/icon-data.d.ts +1 -1
- package/dist/icon-data.d.ts.map +1 -1
- package/dist/icon-data.js +42 -0
- package/dist/icon-data.js.map +1 -1
- package/dist/icon.d.ts +1 -1
- package/dist/icon.js +1 -1
- package/dist/layout.d.ts +21 -0
- package/dist/layout.d.ts.map +1 -1
- package/dist/layout.js +18 -0
- package/dist/layout.js.map +1 -1
- package/dist/navigation.d.ts +45 -7
- package/dist/navigation.d.ts.map +1 -1
- package/dist/navigation.js +54 -7
- package/dist/navigation.js.map +1 -1
- package/dist/surfaces.d.ts +18 -0
- package/dist/surfaces.d.ts.map +1 -1
- package/dist/surfaces.js +6 -0
- package/dist/surfaces.js.map +1 -1
- package/icons/index.json +126 -0
- package/icons/sprite.svg +21 -0
- package/package.json +18 -2
- package/react/actions.tsx +28 -2
- package/react/catalogue.tsx +343 -17
- package/react/css-inline.ts +1 -1
- package/react/data-table.tsx +153 -0
- package/react/data.tsx +87 -1
- package/react/feedback.tsx +98 -0
- package/react/forms.tsx +90 -0
- package/react/icon-data.ts +63 -0
- package/react/icon.tsx +2 -2
- package/react/layout.tsx +39 -0
- package/react/navigation.tsx +108 -6
- package/react/surfaces.tsx +45 -0
- package/src/base.css +16 -0
- package/src/components.css +135 -3
- package/src/data.css +188 -1
- package/src/feedback.css +196 -37
- package/src/forms.css +137 -1
- package/src/layout.css +72 -0
- package/src/navigation.css +207 -12
- package/src/surfaces.css +76 -7
- package/src/tokens.css +6 -0
- package/tokens/index.js +2 -0
package/react/layout.tsx
CHANGED
|
@@ -346,3 +346,42 @@ export function OnlyDesktop({ className, ...reste }: Div & { className?: string
|
|
|
346
346
|
export function OnlyMobile({ className, ...reste }: Div & { className?: string }) {
|
|
347
347
|
return <div className={cx('zv-only-mobile', className)} {...reste} />
|
|
348
348
|
}
|
|
349
|
+
|
|
350
|
+
/* ─── Bandeau d'annonce ────────────────────────────────────────────── */
|
|
351
|
+
|
|
352
|
+
/** La bande AU-DESSUS de la barre de navigation : annonce de version,
|
|
353
|
+
* information réglementaire. UN SEUL par page, et jamais pour du marketing
|
|
354
|
+
* permanent — un bandeau qui ne part pas devient un bourrelet de chrome. */
|
|
355
|
+
export function Banner({ className, ...reste }: Div & { className?: string }) {
|
|
356
|
+
return <div className={cx('zv-banner', className)} {...reste} />
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
/* ─── Carrousel ────────────────────────────────────────────────────── */
|
|
360
|
+
|
|
361
|
+
export interface CarouselProps extends Div {
|
|
362
|
+
/** Ce que la rangée contient (« Nos produits ») : une région qui défile
|
|
363
|
+
* sans nom est une énigme au lecteur d'écran. */
|
|
364
|
+
label: string
|
|
365
|
+
className?: string
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
/** Défilement natif scroll-snap, AUCUN JavaScript : pas de flèches, pas de
|
|
369
|
+
* points. Le conteneur est focalisable — c'est lui que les flèches du
|
|
370
|
+
* clavier font défiler. La largeur des éléments vient du consommateur.
|
|
371
|
+
* ⚠️ Un carrousel reste un aveu d'espace : la règle de variété lui préfère
|
|
372
|
+
* une grille tant que le contenu tient. */
|
|
373
|
+
export function Carousel({ label, className, ...reste }: CarouselProps) {
|
|
374
|
+
return (
|
|
375
|
+
<div
|
|
376
|
+
role="region"
|
|
377
|
+
aria-label={label}
|
|
378
|
+
tabIndex={0}
|
|
379
|
+
className={cx('zv-carousel', className)}
|
|
380
|
+
{...reste}
|
|
381
|
+
/>
|
|
382
|
+
)
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
export function CarouselItem({ className, ...reste }: Div & { className?: string }) {
|
|
386
|
+
return <div className={cx('zv-carousel__item', className)} {...reste} />
|
|
387
|
+
}
|
package/react/navigation.tsx
CHANGED
|
@@ -12,6 +12,7 @@ import type {
|
|
|
12
12
|
AnchorHTMLAttributes,
|
|
13
13
|
ButtonHTMLAttributes,
|
|
14
14
|
HTMLAttributes,
|
|
15
|
+
KeyboardEvent as ReactKeyboardEvent,
|
|
15
16
|
ReactNode,
|
|
16
17
|
} from 'react'
|
|
17
18
|
import { cx } from './cx.js'
|
|
@@ -182,32 +183,91 @@ export function MenuItem({ actif, className, type = 'button', ...reste }: MenuIt
|
|
|
182
183
|
)
|
|
183
184
|
}
|
|
184
185
|
|
|
186
|
+
/* ─── Popover ──────────────────────────────────────────────────────── */
|
|
187
|
+
|
|
188
|
+
/** Surface ancrée LIBRE — aide contextuelle, mini-formulaire de filtre.
|
|
189
|
+
* Même ancre que le menu : à poser dans un `SelectAnchor`, rendu à
|
|
190
|
+
* l'ouverture seulement (`{ouvert && <Popover>…}`). Ce n'est PAS une
|
|
191
|
+
* infobulle : le popover s'ouvre au CLIC et peut porter de l'interactif ;
|
|
192
|
+
* l'infobulle (retours) se montre au survol et n'en porte jamais.
|
|
193
|
+
* ⚠️ Fermeture par clic extérieur et Échap : au composant appelant, comme
|
|
194
|
+
* pour le menu — et le déclencheur porte aria-expanded. */
|
|
195
|
+
export function Popover({ className, ...reste }: Div & { className?: string }) {
|
|
196
|
+
return <div className={cx('zv-popover', className)} {...reste} />
|
|
197
|
+
}
|
|
198
|
+
|
|
185
199
|
/* ─── Onglets ──────────────────────────────────────────────────────── */
|
|
186
200
|
|
|
187
201
|
export interface TabsProps<T extends string> {
|
|
188
202
|
onglets: readonly { id: T; label: ReactNode; compte?: ReactNode }[]
|
|
189
203
|
actif: T
|
|
190
204
|
onChange: (id: T) => void
|
|
191
|
-
/**
|
|
205
|
+
/** « puces » (défaut) : la rangée de Seg — le motif des FILTRES, qui
|
|
206
|
+
* restreignent une collection. « soulignes » : l'onglet au trait —
|
|
207
|
+
* arbitré le 25/08/2026 (2 px, accent) — pour les VUES d'un même objet
|
|
208
|
+
* (Aperçu / Sources). */
|
|
209
|
+
variante?: 'puces' | 'soulignes'
|
|
210
|
+
/** Rangée qui défile au lieu de passer à la ligne (variante puces ; la
|
|
211
|
+
* rangée soulignée défile toujours dans son cadre). */
|
|
192
212
|
rail?: boolean
|
|
193
213
|
/** Identifiant du panneau piloté, pour `aria-controls`. */
|
|
194
214
|
panneau?: string
|
|
195
215
|
className?: string
|
|
196
216
|
}
|
|
197
217
|
|
|
198
|
-
/**
|
|
199
|
-
*
|
|
200
|
-
*
|
|
201
|
-
*
|
|
202
|
-
*
|
|
218
|
+
/** Deux dessins, une seule grammaire d'état. En puces, ce composant n'ajoute
|
|
219
|
+
* que l'ARIA au-dessus de `Seg` ; en soulignés, il pose `.zv-tabs-line` —
|
|
220
|
+
* l'arbitrage longtemps en attente, tranché le 25/08/2026 (DECISIONS.md).
|
|
221
|
+
* Les deux portent le contrat clavier du rôle : la rangée est UNE étape de
|
|
222
|
+
* tabulation (roving tabindex), les flèches passent d'un onglet à l'autre
|
|
223
|
+
* et la sélection suit le focus. */
|
|
203
224
|
export function Tabs<T extends string>({
|
|
204
225
|
onglets,
|
|
205
226
|
actif,
|
|
206
227
|
onChange,
|
|
228
|
+
variante = 'puces',
|
|
207
229
|
rail,
|
|
208
230
|
panneau,
|
|
209
231
|
className,
|
|
210
232
|
}: TabsProps<T>) {
|
|
233
|
+
function auClavier(e: ReactKeyboardEvent<HTMLButtonElement>) {
|
|
234
|
+
const rangee = e.currentTarget.parentElement
|
|
235
|
+
if (!rangee) return
|
|
236
|
+
const tabs = Array.from(rangee.querySelectorAll<HTMLButtonElement>('[role="tab"]'))
|
|
237
|
+
const i = tabs.indexOf(e.currentTarget)
|
|
238
|
+
let cible = -1
|
|
239
|
+
if (e.key === 'ArrowRight') cible = (i + 1) % tabs.length
|
|
240
|
+
else if (e.key === 'ArrowLeft') cible = (i - 1 + tabs.length) % tabs.length
|
|
241
|
+
else if (e.key === 'Home') cible = 0
|
|
242
|
+
else if (e.key === 'End') cible = tabs.length - 1
|
|
243
|
+
if (cible < 0) return
|
|
244
|
+
e.preventDefault()
|
|
245
|
+
tabs[cible]?.focus()
|
|
246
|
+
onChange(onglets[cible]!.id)
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
if (variante === 'soulignes') {
|
|
250
|
+
return (
|
|
251
|
+
<div role="tablist" className={cx('zv-tabs-line', className)}>
|
|
252
|
+
{onglets.map((o) => (
|
|
253
|
+
<button
|
|
254
|
+
key={o.id}
|
|
255
|
+
type="button"
|
|
256
|
+
role="tab"
|
|
257
|
+
aria-selected={o.id === actif}
|
|
258
|
+
aria-controls={panneau}
|
|
259
|
+
tabIndex={o.id === actif ? 0 : -1}
|
|
260
|
+
className="zv-tabs-line__tab"
|
|
261
|
+
onClick={() => onChange(o.id)}
|
|
262
|
+
onKeyDown={auClavier}
|
|
263
|
+
>
|
|
264
|
+
{o.label}
|
|
265
|
+
{o.compte != null && <span className="zv-tabs-line__count">{o.compte}</span>}
|
|
266
|
+
</button>
|
|
267
|
+
))}
|
|
268
|
+
</div>
|
|
269
|
+
)
|
|
270
|
+
}
|
|
211
271
|
return (
|
|
212
272
|
<SegGroup role="tablist" rail={rail} className={className}>
|
|
213
273
|
{onglets.map((o) => (
|
|
@@ -216,9 +276,11 @@ export function Tabs<T extends string>({
|
|
|
216
276
|
role="tab"
|
|
217
277
|
aria-selected={o.id === actif}
|
|
218
278
|
aria-controls={panneau}
|
|
279
|
+
tabIndex={o.id === actif ? 0 : -1}
|
|
219
280
|
on={o.id === actif}
|
|
220
281
|
compte={o.compte}
|
|
221
282
|
onClick={() => onChange(o.id)}
|
|
283
|
+
onKeyDown={auClavier}
|
|
222
284
|
>
|
|
223
285
|
{o.label}
|
|
224
286
|
</Seg>
|
|
@@ -327,3 +389,43 @@ export function Scroll({ habillee, className, ...reste }: ScrollProps) {
|
|
|
327
389
|
/>
|
|
328
390
|
)
|
|
329
391
|
}
|
|
392
|
+
|
|
393
|
+
/* ─── Barre d'onglets mobile ───────────────────────────────────────── */
|
|
394
|
+
|
|
395
|
+
export interface TabbarProps extends Div {
|
|
396
|
+
/** Le nom de la barre (« Sections ») : un <nav> anonyme de plus est une
|
|
397
|
+
* énigme au lecteur d'écran — même règle que le label de `Carousel`. */
|
|
398
|
+
label: string
|
|
399
|
+
className?: string
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
/** Barre basse de navigation, pour une application à trois ou quatre
|
|
403
|
+
* sections. N'existe que SOUS le point de rupture : au-dessus, la colonne
|
|
404
|
+
* latérale est là. CINQ entrées au maximum — au-delà, c'est le tiroir.
|
|
405
|
+
* ⚠️ Elle est fixe : le gabarit qui la pose doit donner au contenu un
|
|
406
|
+
* padding-bottom à sa hauteur, sinon la dernière ligne se cache dessous. */
|
|
407
|
+
export function Tabbar({ label, className, ...reste }: TabbarProps) {
|
|
408
|
+
return <nav aria-label={label} className={cx('zv-tabbar', className)} {...reste} />
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
export interface TabbarItemProps extends A {
|
|
412
|
+
/** Section courante : pose `aria-current="page"`, que le CSS suit. */
|
|
413
|
+
courant?: boolean
|
|
414
|
+
/** L'icône au-dessus du libellé — un `<Icon>` du paquet. */
|
|
415
|
+
icone?: ReactNode
|
|
416
|
+
className?: string
|
|
417
|
+
children?: ReactNode
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
export function TabbarItem({ courant, icone, className, children, ...reste }: TabbarItemProps) {
|
|
421
|
+
return (
|
|
422
|
+
<a
|
|
423
|
+
aria-current={courant ? 'page' : undefined}
|
|
424
|
+
className={cx('zv-tabbar__item', className)}
|
|
425
|
+
{...reste}
|
|
426
|
+
>
|
|
427
|
+
{icone}
|
|
428
|
+
{children}
|
|
429
|
+
</a>
|
|
430
|
+
)
|
|
431
|
+
}
|
package/react/surfaces.tsx
CHANGED
|
@@ -159,3 +159,48 @@ export function anim(
|
|
|
159
159
|
return `zv-anim-converge zv-anim-converge--${option}`
|
|
160
160
|
return `zv-anim-${nom}`
|
|
161
161
|
}
|
|
162
|
+
|
|
163
|
+
/* ─── Formats sociaux ──────────────────────────────────────────────── */
|
|
164
|
+
|
|
165
|
+
export interface SocialFrameProps extends Div {
|
|
166
|
+
/** paysage 1200×630 (OpenGraph, lien LinkedIn) · carre 1080×1080 (post) ·
|
|
167
|
+
* story 1080×1920. FIGÉS en pixels : ce sont des planches qu'on CAPTURE,
|
|
168
|
+
* pas des pages — elles ignorent le point de rupture, et c'est voulu. */
|
|
169
|
+
format?: 'paysage' | 'carre' | 'story'
|
|
170
|
+
/** La planche sombre : le manifeste en fond. Les règles du fond sombre
|
|
171
|
+
* s'appliquent — labels en `Mono ton="on-dark"`, jamais les neutres
|
|
172
|
+
* inversés. */
|
|
173
|
+
manifeste?: boolean
|
|
174
|
+
/** Le pied : wordmark à gauche, adresse en mono à droite. */
|
|
175
|
+
pied?: ReactNode
|
|
176
|
+
className?: string
|
|
177
|
+
children?: ReactNode
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/** Planche d'export pour les réseaux. ⚠️ La règle « jamais de faux
|
|
181
|
+
* contenu » s'y applique au premier chef : un visuel social EST une
|
|
182
|
+
* publication — pas de chiffre inventé, pas de citation fabriquée. */
|
|
183
|
+
export function SocialFrame({
|
|
184
|
+
format = 'paysage',
|
|
185
|
+
manifeste,
|
|
186
|
+
pied,
|
|
187
|
+
className,
|
|
188
|
+
children,
|
|
189
|
+
...reste
|
|
190
|
+
}: SocialFrameProps) {
|
|
191
|
+
return (
|
|
192
|
+
<div
|
|
193
|
+
className={cx(
|
|
194
|
+
'zv-social',
|
|
195
|
+
format === 'carre' && 'zv-social--carre',
|
|
196
|
+
format === 'story' && 'zv-social--story',
|
|
197
|
+
manifeste && 'zv-social--manifeste',
|
|
198
|
+
className,
|
|
199
|
+
)}
|
|
200
|
+
{...reste}
|
|
201
|
+
>
|
|
202
|
+
<div className="zv-social__contenu">{children}</div>
|
|
203
|
+
{pied && <div className="zv-social__pied">{pied}</div>}
|
|
204
|
+
</div>
|
|
205
|
+
)
|
|
206
|
+
}
|
package/src/base.css
CHANGED
|
@@ -64,6 +64,22 @@ a:not([class]):hover {
|
|
|
64
64
|
outline-offset: 2px;
|
|
65
65
|
}
|
|
66
66
|
|
|
67
|
+
/* Texte pour lecteurs d'écran seulement — la géométrie standard du motif,
|
|
68
|
+
* aucune valeur de charte. HORS RELEVÉ (25/08/2026) : ajouté avec la roue,
|
|
69
|
+
* dont le libellé d'annonce doit être un TEXTE dans le flux (un aria-label
|
|
70
|
+
* sur un span vide n'est pas annoncé par tous les lecteurs). */
|
|
71
|
+
.zv-sr-only {
|
|
72
|
+
position: absolute;
|
|
73
|
+
width: 1px;
|
|
74
|
+
height: 1px;
|
|
75
|
+
margin: -1px;
|
|
76
|
+
padding: 0;
|
|
77
|
+
border: 0;
|
|
78
|
+
overflow: hidden;
|
|
79
|
+
clip-path: inset(50%);
|
|
80
|
+
white-space: nowrap;
|
|
81
|
+
}
|
|
82
|
+
|
|
67
83
|
@media (prefers-reduced-motion: reduce) {
|
|
68
84
|
*,
|
|
69
85
|
*::before,
|
package/src/components.css
CHANGED
|
@@ -16,7 +16,8 @@
|
|
|
16
16
|
|
|
17
17
|
.zv-btn,
|
|
18
18
|
.zv-btn--ghost,
|
|
19
|
-
.zv-btn--ink
|
|
19
|
+
.zv-btn--ink,
|
|
20
|
+
.zv-btn--danger {
|
|
20
21
|
display: inline-flex;
|
|
21
22
|
align-items: center;
|
|
22
23
|
justify-content: center;
|
|
@@ -30,10 +31,21 @@
|
|
|
30
31
|
text-decoration: none;
|
|
31
32
|
border: 0;
|
|
32
33
|
cursor: pointer;
|
|
34
|
+
/* La courbe maison, 160 ms — coupée globalement par prefers-reduced-motion
|
|
35
|
+
* (base.css). Le fond en dégradé du bouton plein ne s'interpole pas : lui
|
|
36
|
+
* seul bascule sans fondu, l'écart est trop faible pour se voir. */
|
|
37
|
+
transition:
|
|
38
|
+
background-color 160ms cubic-bezier(0.2, 0.7, 0.2, 1),
|
|
39
|
+
border-color 160ms cubic-bezier(0.2, 0.7, 0.2, 1),
|
|
40
|
+
color 160ms cubic-bezier(0.2, 0.7, 0.2, 1),
|
|
41
|
+
box-shadow 160ms cubic-bezier(0.2, 0.7, 0.2, 1);
|
|
33
42
|
}
|
|
34
43
|
|
|
35
44
|
.zv-btn {
|
|
36
|
-
|
|
45
|
+
/* Le sommet du dégradé, relevé dans Main.html et SANS token : écrit une
|
|
46
|
+
* seule fois ici, les états de survol le dérivent par color-mix. */
|
|
47
|
+
--zv-btn-haut: #2b2bb4;
|
|
48
|
+
background: linear-gradient(180deg, var(--zv-btn-haut) 0%, var(--accent-deep) 100%);
|
|
37
49
|
color: var(--white);
|
|
38
50
|
/* Le liseré interne fait tout le galbe du bouton : il est dans Main.html
|
|
39
51
|
* mais absent de --shadow-btn, d'où la composition ici. */
|
|
@@ -53,6 +65,74 @@
|
|
|
53
65
|
color: var(--white);
|
|
54
66
|
}
|
|
55
67
|
|
|
68
|
+
/* Action DESTRUCTIVE : la confirmation d'une modale danger, la suppression
|
|
69
|
+
* définitive. HORS RELEVÉ (25/08/2026, voir DECISIONS.md) : sans elle,
|
|
70
|
+
* « Supprimer » se rendait en plein indigo et se lisait comme n'importe
|
|
71
|
+
* quelle action. Aplat --err, sans dégradé ni ombre indigo.
|
|
72
|
+
* ⚠️ Elle COMPTE comme le bouton plein de l'écran : la règle « un seul plein
|
|
73
|
+
* par page » compte les aplats, pas les couleurs. */
|
|
74
|
+
.zv-btn--danger {
|
|
75
|
+
background: var(--err);
|
|
76
|
+
color: var(--white);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/* ─── États de survol et d'appui ────────────────────────────────────────
|
|
80
|
+
* HORS RELEVÉ (25/08/2026, voir DECISIONS.md) : aucune maquette n'a d'état
|
|
81
|
+
* hover — elles sont figées. Le survol ASSOMBRIT, l'appui assombrit
|
|
82
|
+
* davantage et rassoit le bouton (l'ombre tombe, le liseré s'éteint).
|
|
83
|
+
* Les teintes sont DÉRIVÉES par color-mix — l'idiome du voile
|
|
84
|
+
* (overlays.css) — des couleurs que le bouton porte déjà : le sommet de
|
|
85
|
+
* dégradé relevé (--zv-btn-haut, sans token) et --accent-deep. Aucune
|
|
86
|
+
* teinte nouvelle n'entre. Aucun déplacement : les entrées du système
|
|
87
|
+
* montent au défilement, un bouton qui saute au survol raconterait autre
|
|
88
|
+
* chose.
|
|
89
|
+
* Déclarés AVANT [disabled], qui doit l'emporter à spécificité égale. */
|
|
90
|
+
|
|
91
|
+
.zv-btn:hover {
|
|
92
|
+
background: linear-gradient(
|
|
93
|
+
180deg,
|
|
94
|
+
color-mix(in oklab, var(--zv-btn-haut) 86%, var(--ink)) 0%,
|
|
95
|
+
color-mix(in oklab, var(--accent-deep) 86%, var(--ink)) 100%
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
.zv-btn:active {
|
|
100
|
+
background: linear-gradient(
|
|
101
|
+
180deg,
|
|
102
|
+
color-mix(in oklab, var(--zv-btn-haut) 72%, var(--ink)) 0%,
|
|
103
|
+
color-mix(in oklab, var(--accent-deep) 72%, var(--ink)) 100%
|
|
104
|
+
);
|
|
105
|
+
box-shadow: none;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
.zv-btn--ghost:hover {
|
|
109
|
+
border-color: var(--accent);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
.zv-btn--ghost:active {
|
|
113
|
+
border-color: var(--accent);
|
|
114
|
+
background: var(--accent-soft);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/* Un aplat d'encre ne peut pas s'assombrir visiblement : il s'ÉCLAIRCIT au
|
|
118
|
+
* survol, et l'appui revient à l'encre pleine — plus sombre que le survol,
|
|
119
|
+
* c'est ce qui fait lire l'enfoncement. */
|
|
120
|
+
.zv-btn--ink:hover {
|
|
121
|
+
background: color-mix(in oklab, var(--ink) 82%, var(--white));
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
.zv-btn--ink:active {
|
|
125
|
+
background: var(--ink);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
.zv-btn--danger:hover {
|
|
129
|
+
background: color-mix(in oklab, var(--err) 86%, var(--ink));
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
.zv-btn--danger:active {
|
|
133
|
+
background: color-mix(in oklab, var(--err) 72%, var(--ink));
|
|
134
|
+
}
|
|
135
|
+
|
|
56
136
|
/* Variante compacte : barres de navigation. L'ombre tombe — un bouton de
|
|
57
137
|
* chrome ne décolle pas de la page. */
|
|
58
138
|
.zv-btn--sm {
|
|
@@ -68,7 +148,8 @@
|
|
|
68
148
|
|
|
69
149
|
.zv-btn[disabled],
|
|
70
150
|
.zv-btn--ghost[disabled],
|
|
71
|
-
.zv-btn--ink[disabled]
|
|
151
|
+
.zv-btn--ink[disabled],
|
|
152
|
+
.zv-btn--danger[disabled] {
|
|
72
153
|
/* HORS RELEVÉ — aucune maquette ne montre de bouton désactivé. Traitement
|
|
73
154
|
* minimal et neutre plutôt qu'une couleur inventée. */
|
|
74
155
|
opacity: 0.5;
|
|
@@ -76,6 +157,57 @@
|
|
|
76
157
|
box-shadow: none;
|
|
77
158
|
}
|
|
78
159
|
|
|
160
|
+
/* Ni le survol ni l'appui ne doivent rattraper un bouton inerte —
|
|
161
|
+
* désactivé OU occupé (dont le clic est avalé par la façade) : sans ces
|
|
162
|
+
* règles, les états ci-dessus repeignent le fond et le bouton a l'air
|
|
163
|
+
* actif. Même précaution que les contrôles désactivés de forms.css. */
|
|
164
|
+
.zv-btn[disabled]:is(:hover, :active),
|
|
165
|
+
.zv-btn[aria-busy='true']:is(:hover, :active) {
|
|
166
|
+
background: linear-gradient(180deg, var(--zv-btn-haut) 0%, var(--accent-deep) 100%);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/* Occupé mais pas désactivé, le bouton plein garde son ombre à l'appui —
|
|
170
|
+
* un enfoncement qui ne déclenche rien serait un mensonge de rendu. Le
|
|
171
|
+
* compact, lui, n'a pas d'ombre à garder. */
|
|
172
|
+
.zv-btn:not(.zv-btn--sm)[aria-busy='true']:active {
|
|
173
|
+
box-shadow:
|
|
174
|
+
0 1px 0 rgba(255, 255, 255, 0.18) inset,
|
|
175
|
+
var(--shadow-btn);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
.zv-btn--ghost[disabled]:is(:hover, :active),
|
|
179
|
+
.zv-btn--ghost[aria-busy='true']:is(:hover, :active) {
|
|
180
|
+
border-color: var(--accent-line);
|
|
181
|
+
background: var(--white);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
.zv-btn--ink[disabled]:is(:hover, :active),
|
|
185
|
+
.zv-btn--ink[aria-busy='true']:is(:hover, :active) {
|
|
186
|
+
background: var(--ink);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
.zv-btn--danger[disabled]:is(:hover, :active),
|
|
190
|
+
.zv-btn--danger[aria-busy='true']:is(:hover, :active) {
|
|
191
|
+
background: var(--err);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/* Attente : le motif de Mémoire (feedback.css le documente) — le LIBELLÉ
|
|
195
|
+
* change (« Connexion… ») pendant que le bouton s'estompe, en
|
|
196
|
+
* `cursor-wait; opacity: .7`, valeurs relevées reprises telles quelles.
|
|
197
|
+
* Codé sur [aria-busy], l'attribut qui rend l'attente audible : le
|
|
198
|
+
* balisage correct ne coûte rien. La façade React l'expose en prop
|
|
199
|
+
* `occupe` — qui pose aria-disabled et AVALE le clic plutôt que de poser
|
|
200
|
+
* `disabled`, pour que le bouton reste dans le parcours clavier pendant
|
|
201
|
+
* que l'attente s'annonce (voir DECISIONS.md). Déclaré APRÈS [disabled]
|
|
202
|
+
* pour l'emporter à spécificité égale quand les deux se combinent. */
|
|
203
|
+
.zv-btn[aria-busy='true'],
|
|
204
|
+
.zv-btn--ghost[aria-busy='true'],
|
|
205
|
+
.zv-btn--ink[aria-busy='true'],
|
|
206
|
+
.zv-btn--danger[aria-busy='true'] {
|
|
207
|
+
cursor: wait;
|
|
208
|
+
opacity: 0.7;
|
|
209
|
+
}
|
|
210
|
+
|
|
79
211
|
.zv-btn-row {
|
|
80
212
|
display: flex;
|
|
81
213
|
align-items: center;
|
package/src/data.css
CHANGED
|
@@ -5,7 +5,9 @@
|
|
|
5
5
|
* les trois registres de cellule, le dégroupage mobile (.zv-unwrap), la
|
|
6
6
|
* liste sans colonnes, la métadonnée mono, le couple clé-valeur, la
|
|
7
7
|
* pastille d'initiales, les deux utilitaires de troncature, l'étiquette de
|
|
8
|
-
* catégorie, la puce de citation et la vignette de format
|
|
8
|
+
* catégorie, la puce de citation et la vignette de format — et, depuis le
|
|
9
|
+
* 25/08/2026 (hors relevé, voir DECISIONS.md) : la rangée dépliante, la
|
|
10
|
+
* touche de clavier et le code, en ligne comme en bloc.
|
|
9
11
|
*
|
|
10
12
|
* DÉPENDANCES
|
|
11
13
|
* tokens.css — pour les seules variables qui y sont déclarées ;
|
|
@@ -156,6 +158,51 @@
|
|
|
156
158
|
* contient aucun, et les inventer ferait apparaître un fond de plus dans un
|
|
157
159
|
* système qui n'en a que deux (blanc, survol). */
|
|
158
160
|
|
|
161
|
+
/* ─── En-tête triable ──────────────────────────────────────────────────
|
|
162
|
+
* ⚠️ HORS RELEVÉ — ajouté le 25/08/2026 avec l'adaptateur TanStack Table.
|
|
163
|
+
* Aucun tableau de la production n'est triable ; l'intitulé y est du texte
|
|
164
|
+
* inerte. Un en-tête qui trie DOIT être un <button> — c'est la seule façon
|
|
165
|
+
* qu'il soit atteignable au clavier et annoncé comme actionnable.
|
|
166
|
+
*
|
|
167
|
+
* `font: inherit` plutôt qu'une police redite : l'en-tête a déjà la sienne
|
|
168
|
+
* (mono, capitales, interlettrée), et un <button> retomberait sur celle du
|
|
169
|
+
* navigateur. Hériter ne fait entrer aucune valeur nouvelle. */
|
|
170
|
+
|
|
171
|
+
.zv-sort {
|
|
172
|
+
display: inline-flex;
|
|
173
|
+
align-items: center;
|
|
174
|
+
gap: 5px;
|
|
175
|
+
border: 0;
|
|
176
|
+
background: none;
|
|
177
|
+
padding: 0;
|
|
178
|
+
font: inherit;
|
|
179
|
+
letter-spacing: inherit;
|
|
180
|
+
text-transform: inherit;
|
|
181
|
+
color: inherit;
|
|
182
|
+
cursor: pointer;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
.zv-sort:hover {
|
|
186
|
+
color: var(--accent);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
.zv-sort:focus-visible {
|
|
190
|
+
/* L'en-tête colle au bord du cadre : l'anneau de base.css y serait rogné. */
|
|
191
|
+
outline-offset: -2px;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/* La marque de tri reste TOUJOURS posée, même sur une colonne non triée
|
|
195
|
+
* (elle vaut alors ↕) : si elle n'apparaissait qu'au tri, la largeur de la
|
|
196
|
+
* colonne changerait au clic et toute la ligne d'en-tête danserait. */
|
|
197
|
+
.zv-sort__mark {
|
|
198
|
+
font-size: var(--mono-sm);
|
|
199
|
+
color: var(--label);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
.zv-sort:hover .zv-sort__mark {
|
|
203
|
+
color: var(--accent);
|
|
204
|
+
}
|
|
205
|
+
|
|
159
206
|
/* ─── Cellules ─────────────────────────────────────────────────────────
|
|
160
207
|
* Trois registres seulement : le texte courant, le nombre, et l'intitulé qui
|
|
161
208
|
* remplace l'en-tête quand celui-ci disparaît. */
|
|
@@ -644,6 +691,134 @@
|
|
|
644
691
|
background: var(--fmt-docx);
|
|
645
692
|
}
|
|
646
693
|
|
|
694
|
+
/* ─── Rangée dépliante ─────────────────────────────────────────────────
|
|
695
|
+
* HORS RELEVÉ (25/08/2026, voir DECISIONS.md). La FAQ est le pliage de la
|
|
696
|
+
* vitrine ; l'applicatif n'en avait aucun — options avancées, détail d'une
|
|
697
|
+
* ligne de journal. Construite sur <details> NATIF : le clavier, l'annonce
|
|
698
|
+
* et l'état ouvert viennent du navigateur, aucun JavaScript.
|
|
699
|
+
* Plusieurs rangées se suivent bord à bord : le filet mitoyen tombe. */
|
|
700
|
+
|
|
701
|
+
.zv-fold {
|
|
702
|
+
border: 1px solid var(--line);
|
|
703
|
+
background: var(--white);
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
.zv-fold + .zv-fold {
|
|
707
|
+
border-top: 0;
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
.zv-fold__titre {
|
|
711
|
+
display: flex;
|
|
712
|
+
align-items: center;
|
|
713
|
+
justify-content: space-between;
|
|
714
|
+
gap: 12px;
|
|
715
|
+
padding: 12px 16px;
|
|
716
|
+
font-size: var(--app);
|
|
717
|
+
font-weight: 600;
|
|
718
|
+
color: var(--ink);
|
|
719
|
+
cursor: pointer;
|
|
720
|
+
/* Le marqueur natif du <summary> tombe : le chevron du composant fait le
|
|
721
|
+
* même travail sans imposer le triangle du navigateur. */
|
|
722
|
+
list-style: none;
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
.zv-fold__titre::-webkit-details-marker {
|
|
726
|
+
display: none;
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
.zv-fold__titre:hover {
|
|
730
|
+
background: var(--panel);
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
/* Le chevron porte sa classe : cibler tout svg du titre attraperait aussi
|
|
734
|
+
* une icône posée dans le libellé, qui se verrait rétrécie et retournée à
|
|
735
|
+
* l'ouverture. */
|
|
736
|
+
.zv-fold__chevron {
|
|
737
|
+
width: 14px;
|
|
738
|
+
height: 14px;
|
|
739
|
+
flex: none;
|
|
740
|
+
color: var(--label);
|
|
741
|
+
/* `rotate` et non `transform` : la propriété indépendante, comme partout
|
|
742
|
+
* où le paquet anime. Coupée par prefers-reduced-motion (base.css). */
|
|
743
|
+
transition: rotate 160ms cubic-bezier(0.2, 0.7, 0.2, 1);
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
.zv-fold[open] > .zv-fold__titre {
|
|
747
|
+
border-bottom: 1px solid var(--hairline);
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
.zv-fold[open] > .zv-fold__titre .zv-fold__chevron {
|
|
751
|
+
rotate: 180deg;
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
.zv-fold__corps {
|
|
755
|
+
padding: 12px 16px;
|
|
756
|
+
font-size: var(--app);
|
|
757
|
+
line-height: 1.6;
|
|
758
|
+
color: var(--muted);
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
/* ─── Touche de clavier ────────────────────────────────────────────────
|
|
762
|
+
* HORS RELEVÉ (25/08/2026). Une documentation d'interface cite des
|
|
763
|
+
* raccourcis ; le paquet n'avait rien pour les poser. Le bas de 2 px fait
|
|
764
|
+
* le relief de la touche — géométrie, pas ombre de charte, comme l'anneau
|
|
765
|
+
* du radio. */
|
|
766
|
+
|
|
767
|
+
.zv-kbd {
|
|
768
|
+
display: inline-block;
|
|
769
|
+
padding: 2px 6px;
|
|
770
|
+
border: 1px solid var(--line);
|
|
771
|
+
border-bottom-width: 2px;
|
|
772
|
+
background: var(--panel);
|
|
773
|
+
font-family: var(--font-mono);
|
|
774
|
+
font-size: var(--mono-lg);
|
|
775
|
+
font-weight: 600;
|
|
776
|
+
white-space: nowrap;
|
|
777
|
+
color: var(--ink-2);
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
/* ─── Code ─────────────────────────────────────────────────────────────
|
|
781
|
+
* HORS RELEVÉ (25/08/2026, voir DECISIONS.md — c'est l'entrée qui a créé
|
|
782
|
+
* --font-code). --font-mono (Space Grotesk) est PROPORTIONNELLE : elle
|
|
783
|
+
* porte le registre des labels, pas l'alignement, et tabular-nums n'aligne
|
|
784
|
+
* que les chiffres. Du code exige la chasse fixe entière : --font-code, la
|
|
785
|
+
* pile système, sans fonte chargée.
|
|
786
|
+
* Aucune coloration syntaxique : elle imposerait une palette entière que la
|
|
787
|
+
* charte n'a pas. Le code se lit à l'encre sur panneau, comme un extrait. */
|
|
788
|
+
|
|
789
|
+
/* En ligne, dans une phrase : `pnpm build`. Taille RELATIVE au texte qui
|
|
790
|
+
* l'accueille — le même extrait sert dans un corps 15 et une note 12,5. */
|
|
791
|
+
.zv-code {
|
|
792
|
+
padding: 1px 5px;
|
|
793
|
+
border: 1px solid var(--hairline);
|
|
794
|
+
background: var(--panel);
|
|
795
|
+
font-family: var(--font-code);
|
|
796
|
+
font-size: 0.85em;
|
|
797
|
+
overflow-wrap: break-word;
|
|
798
|
+
color: var(--ink-2);
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
/* En bloc. Le cadre défile en son sein, comme le tableau : une ligne longue
|
|
802
|
+
* ne pousse jamais la page en défilement horizontal. */
|
|
803
|
+
.zv-codeblock {
|
|
804
|
+
overflow-x: auto;
|
|
805
|
+
margin: 0;
|
|
806
|
+
border: 1px solid var(--line);
|
|
807
|
+
background: var(--panel);
|
|
808
|
+
padding: 14px 16px;
|
|
809
|
+
font-family: var(--font-code);
|
|
810
|
+
font-size: var(--app-sm);
|
|
811
|
+
line-height: 1.7;
|
|
812
|
+
tab-size: 2;
|
|
813
|
+
color: var(--ink);
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
/* Le <code> intérieur n'apporte que la sémantique : sans ce rappel, il
|
|
817
|
+
* retomberait sur la mono du navigateur, à une autre taille. */
|
|
818
|
+
.zv-codeblock code {
|
|
819
|
+
font: inherit;
|
|
820
|
+
}
|
|
821
|
+
|
|
647
822
|
/* ─── Sous 768px ───────────────────────────────────────────────────────
|
|
648
823
|
* Point de rupture unique de la charte. Les lignes cessent d'être des lignes :
|
|
649
824
|
* une grille de six colonnes à 375px produit six colonnes illisibles, ou un
|
|
@@ -739,4 +914,16 @@
|
|
|
739
914
|
.zv-cite {
|
|
740
915
|
white-space: normal;
|
|
741
916
|
}
|
|
917
|
+
|
|
918
|
+
/* L'en-tête du dépliant devient une cible tactile. */
|
|
919
|
+
.zv-fold__titre {
|
|
920
|
+
min-height: var(--tap);
|
|
921
|
+
padding-block: 10px;
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
/* 16px de gouttière interne sur 375px déjà bordés par la page : le bloc
|
|
925
|
+
* se resserre, comme les bandeaux. */
|
|
926
|
+
.zv-codeblock {
|
|
927
|
+
padding: 12px;
|
|
928
|
+
}
|
|
742
929
|
}
|