@zevra/ui 0.14.0 → 0.15.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 +68 -0
- package/dist/catalogue.d.ts.map +1 -1
- package/dist/catalogue.js +46 -1
- 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/layout.d.ts +59 -1
- package/dist/layout.d.ts.map +1 -1
- package/dist/layout.js +42 -0
- package/dist/layout.js.map +1 -1
- package/package.json +4 -2
- package/react/catalogue.tsx +102 -1
- package/react/css-inline.ts +1 -1
- package/react/layout.tsx +149 -1
- package/src/layout.css +195 -3
- package/src/nav.js +67 -0
package/react/layout.tsx
CHANGED
|
@@ -4,10 +4,18 @@
|
|
|
4
4
|
// Couche VITRINE : une application n'a aucune raison de charger ce fichier.
|
|
5
5
|
// Tout y est sans état.
|
|
6
6
|
|
|
7
|
-
import type {
|
|
7
|
+
import type {
|
|
8
|
+
ButtonHTMLAttributes,
|
|
9
|
+
CSSProperties,
|
|
10
|
+
ElementType,
|
|
11
|
+
HTMLAttributes,
|
|
12
|
+
ReactNode,
|
|
13
|
+
} from 'react'
|
|
8
14
|
import { cx } from './cx.js'
|
|
9
15
|
|
|
10
16
|
type Div = Omit<HTMLAttributes<HTMLDivElement>, 'className'>
|
|
17
|
+
type Span = Omit<HTMLAttributes<HTMLSpanElement>, 'className'>
|
|
18
|
+
type Btn = Omit<ButtonHTMLAttributes<HTMLButtonElement>, 'className'>
|
|
11
19
|
|
|
12
20
|
/* ─── Page et gouttières ───────────────────────────────────────────── */
|
|
13
21
|
|
|
@@ -206,6 +214,146 @@ export function NavDrawerLink({
|
|
|
206
214
|
)
|
|
207
215
|
}
|
|
208
216
|
|
|
217
|
+
export function NavDrawerGroup({ className, ...reste }: Span & { className?: string }) {
|
|
218
|
+
return (
|
|
219
|
+
<span className={cx('zv-mono zv-mono--label zv-nav-drawer__groupe', className)} {...reste} />
|
|
220
|
+
)
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/* ─── Entrées à menu déroulant ─────────────────────────────────────── */
|
|
224
|
+
|
|
225
|
+
/** Une famille qui se déplie. Ouverture au survol ET au focus, en CSS seul :
|
|
226
|
+
* pas d'état à tenir, donc rien à hydrater — une barre de vitrine reste
|
|
227
|
+
* rendue au serveur. */
|
|
228
|
+
export function NavDropdown({ className, ...reste }: Span & { className?: string }) {
|
|
229
|
+
return <span className={cx('zv-navdd', className)} {...reste} />
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/** Le déclencheur. Il porte `.zv-navlink` pour se rendre comme un lien simple :
|
|
233
|
+
* ne JAMAIS lui poser `font: inherit`, qui reprendrait la fonte du conteneur
|
|
234
|
+
* et le grossirait par rapport à ses voisins. */
|
|
235
|
+
export function NavDropdownButton({
|
|
236
|
+
className,
|
|
237
|
+
type = 'button',
|
|
238
|
+
children,
|
|
239
|
+
...reste
|
|
240
|
+
}: Btn & { className?: string; type?: 'button'; children?: ReactNode }) {
|
|
241
|
+
return (
|
|
242
|
+
<button
|
|
243
|
+
type={type}
|
|
244
|
+
aria-haspopup="true"
|
|
245
|
+
className={cx('zv-navlink zv-navdd__btn', className)}
|
|
246
|
+
{...reste}
|
|
247
|
+
>
|
|
248
|
+
{children}
|
|
249
|
+
<svg className="zv-navdd__chevron" viewBox="0 0 24 24" aria-hidden="true">
|
|
250
|
+
<path d="M6 9.4 L12 15.4 L18 9.4" />
|
|
251
|
+
</svg>
|
|
252
|
+
</button>
|
|
253
|
+
)
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/** Le panneau : c'est le `.zv-menu` du paquet, ancré sous l'entrée. */
|
|
257
|
+
export function NavDropdownMenu({ className, ...reste }: Span & { className?: string }) {
|
|
258
|
+
return <span className={cx('zv-menu zv-navdd__menu', className)} {...reste} />
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
export function NavDropdownSep({ className, ...reste }: Span & { className?: string }) {
|
|
262
|
+
return <span aria-hidden="true" className={cx('zv-navdd__sep', className)} {...reste} />
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/* ─── Sélecteur de langue ──────────────────────────────────────────── */
|
|
266
|
+
|
|
267
|
+
/** ⚠️ Ne pas le poser sur une page qui n'existe que dans une langue : mieux
|
|
268
|
+
* vaut pas de sélecteur qu'un sélecteur qui mène à des 404. */
|
|
269
|
+
export function Langues({
|
|
270
|
+
tiroir,
|
|
271
|
+
className,
|
|
272
|
+
...reste
|
|
273
|
+
}: Span & { tiroir?: boolean; className?: string }) {
|
|
274
|
+
return (
|
|
275
|
+
<span
|
|
276
|
+
className={cx(
|
|
277
|
+
'zv-langues zv-mono zv-mono--micro',
|
|
278
|
+
tiroir && 'zv-langues--tiroir',
|
|
279
|
+
className,
|
|
280
|
+
)}
|
|
281
|
+
{...reste}
|
|
282
|
+
/>
|
|
283
|
+
)
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
export function Langue({
|
|
287
|
+
courante,
|
|
288
|
+
className,
|
|
289
|
+
...reste
|
|
290
|
+
}: Omit<HTMLAttributes<HTMLAnchorElement>, 'className'> & {
|
|
291
|
+
courante?: boolean
|
|
292
|
+
href?: string
|
|
293
|
+
className?: string
|
|
294
|
+
}) {
|
|
295
|
+
return (
|
|
296
|
+
<a
|
|
297
|
+
aria-current={courante ? 'true' : undefined}
|
|
298
|
+
className={cx('zv-langue', courante && 'zv-langue--on', className)}
|
|
299
|
+
{...reste}
|
|
300
|
+
/>
|
|
301
|
+
)
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/* ─── Lockup ───────────────────────────────────────────────────────── */
|
|
305
|
+
|
|
306
|
+
export interface LockupProps extends Omit<Span, 'style'> {
|
|
307
|
+
style?: CSSProperties
|
|
308
|
+
/** Hauteur de CAPITALE, en px. Tout le reste s'en déduit. Sous 11, poser
|
|
309
|
+
* `marqueSeule` : le mot ne se lit plus, quelle que soit sa netteté. */
|
|
310
|
+
capitale?: number
|
|
311
|
+
marqueSeule?: boolean
|
|
312
|
+
className?: string
|
|
313
|
+
children?: ReactNode
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/** La marque et le mot, aux proportions relevées sur le lockup d'origine. */
|
|
317
|
+
export function Lockup({
|
|
318
|
+
capitale = 15,
|
|
319
|
+
marqueSeule,
|
|
320
|
+
className,
|
|
321
|
+
style,
|
|
322
|
+
...reste
|
|
323
|
+
}: LockupProps) {
|
|
324
|
+
return (
|
|
325
|
+
<span
|
|
326
|
+
className={cx('zv-lockup', marqueSeule && 'zv-lockup--marque-seule', className)}
|
|
327
|
+
style={{ ['--zv-cap' as string]: `${capitale}px`, ...style }}
|
|
328
|
+
{...reste}
|
|
329
|
+
/>
|
|
330
|
+
)
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
export function LockupMark({
|
|
334
|
+
className,
|
|
335
|
+
alt = '',
|
|
336
|
+
...reste
|
|
337
|
+
}: Omit<HTMLAttributes<HTMLImageElement>, 'className'> & {
|
|
338
|
+
className?: string
|
|
339
|
+
src: string
|
|
340
|
+
alt?: string
|
|
341
|
+
}) {
|
|
342
|
+
return <img alt={alt} className={cx('zv-lockup__mark', className)} {...reste} />
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
export function LockupWord({
|
|
346
|
+
className,
|
|
347
|
+
alt = '',
|
|
348
|
+
...reste
|
|
349
|
+
}: Omit<HTMLAttributes<HTMLImageElement>, 'className'> & {
|
|
350
|
+
className?: string
|
|
351
|
+
src: string
|
|
352
|
+
alt?: string
|
|
353
|
+
}) {
|
|
354
|
+
return <img alt={alt} className={cx('zv-lockup__word', className)} {...reste} />
|
|
355
|
+
}
|
|
356
|
+
|
|
209
357
|
/* ─── Frise d'étapes ───────────────────────────────────────────────── */
|
|
210
358
|
|
|
211
359
|
export interface StepsProps extends Div {
|
package/src/layout.css
CHANGED
|
@@ -313,8 +313,159 @@
|
|
|
313
313
|
color: var(--label);
|
|
314
314
|
}
|
|
315
315
|
|
|
316
|
-
/*
|
|
317
|
-
*
|
|
316
|
+
/* ─── Entrées à menu déroulant ──────────────────────────────────────────
|
|
317
|
+
*
|
|
318
|
+
* Une nav de vitrine qui porte l'architecture d'un site — des familles,
|
|
319
|
+
* pas quatre liens — a besoin de déplier. Ouverture au survol ET au focus
|
|
320
|
+
* (`:focus-within`) : en CSS seul, donc accessible au clavier sans une
|
|
321
|
+
* ligne de JavaScript et sans état à tenir.
|
|
322
|
+
*
|
|
323
|
+
* Le panneau est le .zv-menu du paquet, simplement ancré sous l'entrée. */
|
|
324
|
+
|
|
325
|
+
.zv-navdd {
|
|
326
|
+
position: relative;
|
|
327
|
+
display: inline-flex;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
/* Le bouton porte .zv-navlink : il doit se rendre EXACTEMENT comme un lien
|
|
331
|
+
* simple de la barre. Piège à éviter — un `font: inherit`, réflexe habituel
|
|
332
|
+
* pour dénuder un bouton, remet la fonte du CONTENEUR (16px/400) là où
|
|
333
|
+
* .zv-navlink pose 14px/500 : les entrées à menu deviennent plus grosses
|
|
334
|
+
* que les autres. On ne reprend donc du bouton que ce qui doit disparaître. */
|
|
335
|
+
.zv-navdd__btn {
|
|
336
|
+
display: inline-flex;
|
|
337
|
+
align-items: center;
|
|
338
|
+
gap: 5px;
|
|
339
|
+
padding: 0;
|
|
340
|
+
border: 0;
|
|
341
|
+
background: none;
|
|
342
|
+
cursor: pointer;
|
|
343
|
+
font-family: inherit;
|
|
344
|
+
line-height: inherit;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
.zv-navdd__chevron {
|
|
348
|
+
width: 12px;
|
|
349
|
+
height: 12px;
|
|
350
|
+
fill: none;
|
|
351
|
+
stroke: currentColor;
|
|
352
|
+
stroke-width: 2;
|
|
353
|
+
/* Bouts francs : le trait carré du jeu d'icônes (25/08/2026). */
|
|
354
|
+
stroke-linecap: square;
|
|
355
|
+
stroke-linejoin: miter;
|
|
356
|
+
transition: transform 160ms var(--zv-easing, cubic-bezier(0.2, 0.7, 0.2, 1));
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
.zv-navdd:hover .zv-navdd__chevron,
|
|
360
|
+
.zv-navdd:focus-within .zv-navdd__chevron {
|
|
361
|
+
transform: rotate(180deg);
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
/* Le pont invisible (::before) couvre l'interstice entre l'entrée et son
|
|
365
|
+
* panneau : sans lui, le survol se rompt en descendant vers le menu et le
|
|
366
|
+
* menu se referme sous le curseur. */
|
|
367
|
+
.zv-navdd__menu {
|
|
368
|
+
position: absolute;
|
|
369
|
+
top: 100%;
|
|
370
|
+
left: 50%;
|
|
371
|
+
translate: -50% 0;
|
|
372
|
+
padding-top: 14px;
|
|
373
|
+
display: none;
|
|
374
|
+
min-width: 250px;
|
|
375
|
+
z-index: 50;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
.zv-navdd__menu::before {
|
|
379
|
+
content: '';
|
|
380
|
+
position: absolute;
|
|
381
|
+
inset: 0 0 auto 0;
|
|
382
|
+
height: 14px;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
.zv-navdd:hover .zv-navdd__menu,
|
|
386
|
+
.zv-navdd:focus-within .zv-navdd__menu {
|
|
387
|
+
display: flex;
|
|
388
|
+
flex-direction: column;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
.zv-navdd__sep {
|
|
392
|
+
display: block;
|
|
393
|
+
height: 1px;
|
|
394
|
+
margin: 6px 0;
|
|
395
|
+
background: var(--hairline);
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
/* ─── Sélecteur de langue ───────────────────────────────────────────────
|
|
399
|
+
* Des initiales au registre mono, pas un menu : trois langues se lisent
|
|
400
|
+
* d'un coup d'œil, et un menu pour trois entrées coûte un clic pour rien.
|
|
401
|
+
* Une page qui n'existe que dans une langue ne le pose pas — il vaut mieux
|
|
402
|
+
* pas de sélecteur qu'un sélecteur qui mène à des 404. */
|
|
403
|
+
|
|
404
|
+
.zv-langues {
|
|
405
|
+
display: inline-flex;
|
|
406
|
+
align-items: center;
|
|
407
|
+
gap: 10px;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
.zv-langue {
|
|
411
|
+
color: var(--label);
|
|
412
|
+
text-decoration: none;
|
|
413
|
+
letter-spacing: 0.12em;
|
|
414
|
+
transition: color 160ms var(--zv-easing, cubic-bezier(0.2, 0.7, 0.2, 1));
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
.zv-langue:hover {
|
|
418
|
+
color: var(--accent);
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
.zv-langue--on {
|
|
422
|
+
color: var(--ink);
|
|
423
|
+
font-weight: 700;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
/* Dans le tiroir, il se détache en pied de liste. */
|
|
427
|
+
.zv-langues--tiroir {
|
|
428
|
+
display: flex;
|
|
429
|
+
gap: 16px;
|
|
430
|
+
margin-top: 18px;
|
|
431
|
+
padding-top: 14px;
|
|
432
|
+
border-top: 1px solid var(--hairline);
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
/* ─── Le tiroir, groupé ─────────────────────────────────────────────────
|
|
436
|
+
* Quand la barre porte des familles, les aplatir en une liste unique rend
|
|
437
|
+
* le tiroir illisible. Un label capitale par famille, ses entrées en
|
|
438
|
+
* dessous : la même architecture, repliée. */
|
|
439
|
+
|
|
440
|
+
.zv-nav-drawer__groupe {
|
|
441
|
+
display: block;
|
|
442
|
+
margin: 16px 0 4px;
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
.zv-nav-drawer__groupe:first-child {
|
|
446
|
+
margin-top: 0;
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
.zv-nav-drawer__link--sous {
|
|
450
|
+
font-size: 15px;
|
|
451
|
+
padding-block: 7px;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
/* Chrome fixe : voile dégradé plutôt qu'un fond plein, pour que le contenu
|
|
455
|
+
* s'y fonde au défilement.
|
|
456
|
+
*
|
|
457
|
+
* Deux usages, une seule classe. Il est LA BARRE quand il porte directement
|
|
458
|
+
* ses pièces ; il l'ENVELOPPE quand il contient une .zv-nav — c'est la forme
|
|
459
|
+
* qu'impose une nav complète, dont les menus déroulants et le tiroir ont
|
|
460
|
+
* besoin d'un conteneur à eux. Le `:has` bascule tout seul, plutôt que de
|
|
461
|
+
* demander un modificateur qu'on oublierait.
|
|
462
|
+
*
|
|
463
|
+
* ⚠️ backdrop-filter fait de ce bloc le containing block de TOUT descendant
|
|
464
|
+
* en position fixe. Le tiroir (fixe lui aussi) doit donc vivre EN DEHORS,
|
|
465
|
+
* en frère — dedans, il serait rogné par la hauteur de la barre.
|
|
466
|
+
*
|
|
467
|
+
* Le voile venait en rgba littéral, c'est-à-dire hors token : --paper
|
|
468
|
+
* repeint, la barre serait restée sur l'ancienne couleur. */
|
|
318
469
|
.zv-header {
|
|
319
470
|
position: fixed;
|
|
320
471
|
top: 0;
|
|
@@ -326,11 +477,25 @@
|
|
|
326
477
|
padding-top: 22px;
|
|
327
478
|
padding-bottom: 12px;
|
|
328
479
|
z-index: 40;
|
|
329
|
-
background: linear-gradient(
|
|
480
|
+
background: linear-gradient(
|
|
481
|
+
180deg,
|
|
482
|
+
color-mix(in srgb, var(--paper) 92%, transparent),
|
|
483
|
+
color-mix(in srgb, var(--paper) 0%, transparent)
|
|
484
|
+
);
|
|
330
485
|
backdrop-filter: blur(6px);
|
|
331
486
|
-webkit-backdrop-filter: blur(6px);
|
|
332
487
|
}
|
|
333
488
|
|
|
489
|
+
.zv-header:has(> .zv-nav) {
|
|
490
|
+
display: block;
|
|
491
|
+
padding-top: 0;
|
|
492
|
+
padding-bottom: 0;
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
.zv-header > .zv-nav {
|
|
496
|
+
padding-bottom: 14px;
|
|
497
|
+
}
|
|
498
|
+
|
|
334
499
|
/* ─── Frise d'étapes ────────────────────────────────────────────────────
|
|
335
500
|
* Troisième et dernier emploi autorisé du spectre : les points de méthode,
|
|
336
501
|
* dans l'ordre iris → violet → cyan. */
|
|
@@ -711,3 +876,30 @@
|
|
|
711
876
|
gap: 24px;
|
|
712
877
|
}
|
|
713
878
|
}
|
|
879
|
+
|
|
880
|
+
/* ─── Le seuil d'une nav riche ──────────────────────────────────────────
|
|
881
|
+
*
|
|
882
|
+
* La bascule vers le tiroir est à 767px, réglée sur la nav de référence :
|
|
883
|
+
* quatre liens simples. Une nav qui porte l'architecture d'un site — cinq
|
|
884
|
+
* familles à menu, un sélecteur de langue, un appel à l'action — déborde
|
|
885
|
+
* bien avant. Mesuré : entre 768 et 1150px, le logo passe SOUS la première
|
|
886
|
+
* entrée.
|
|
887
|
+
*
|
|
888
|
+
* `.zv-nav--riche` déplace le seuil à 1150px, POUR LA BARRE SEULEMENT. Ce
|
|
889
|
+
* n'est pas un second point de rupture de mise en page : le reste de la
|
|
890
|
+
* page garde l'unique seuil de la charte. C'est le seuil de débordement
|
|
891
|
+
* d'un composant, et il se déclare sur ce composant. */
|
|
892
|
+
|
|
893
|
+
@media (max-width: 1150px) {
|
|
894
|
+
.zv-nav--riche .zv-nav__links {
|
|
895
|
+
display: none;
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
.zv-nav--riche .zv-nav__burger {
|
|
899
|
+
display: inline-flex;
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
.zv-nav--riche .zv-nav__logo {
|
|
903
|
+
height: 30px;
|
|
904
|
+
}
|
|
905
|
+
}
|
package/src/nav.js
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/* Zevra UI — nav.js : l'ouverture et la fermeture du tiroir de navigation.
|
|
2
|
+
*
|
|
3
|
+
* Rôle exact, et rien d'autre : poser et retirer `data-open` sur le tiroir,
|
|
4
|
+
* en tenir `aria-expanded` sur le bouton. Le reste — le voile, la glissade,
|
|
5
|
+
* le verrouillage du défilement — est dans overlays.css et se joue sur cet
|
|
6
|
+
* attribut seul.
|
|
7
|
+
*
|
|
8
|
+
* Le contrat, en quatre attributs :
|
|
9
|
+
* [data-zv-drawer] le tiroir (.zv-drawer)
|
|
10
|
+
* [data-zv-drawer-open] ce qui l'ouvre (le burger)
|
|
11
|
+
* [data-zv-drawer-close] ce qui le ferme (le voile, chaque lien)
|
|
12
|
+
* [data-zv-drawer="nom"] pour en câbler plusieurs sur une même page ;
|
|
13
|
+
* les déclencheurs portent alors la même valeur.
|
|
14
|
+
*
|
|
15
|
+
* Échap ferme : le CSS ne sait pas le faire, et un panneau qu'on ne ferme
|
|
16
|
+
* qu'en visant un voile est pénible au clavier — et impraticable sans souris.
|
|
17
|
+
*
|
|
18
|
+
* S'importe une fois par page : `import '@zevra/ui/nav.js'`. Idempotent, et
|
|
19
|
+
* sans effet si aucun tiroir n'est présent. Le module observe le document :
|
|
20
|
+
* un tiroir rendu plus tard (navigation client, îlot hydraté) est câblé sans
|
|
21
|
+
* qu'il y ait à réappeler quoi que ce soit.
|
|
22
|
+
*/
|
|
23
|
+
(() => {
|
|
24
|
+
if (typeof document === 'undefined') return
|
|
25
|
+
if (document.documentElement.dataset.zvNav === 'on') return
|
|
26
|
+
document.documentElement.dataset.zvNav = 'on'
|
|
27
|
+
|
|
28
|
+
const tiroirs = (nom) =>
|
|
29
|
+
Array.from(document.querySelectorAll('[data-zv-drawer]')).filter(
|
|
30
|
+
(el) => !nom || el.getAttribute('data-zv-drawer') === nom,
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
const basculer = (nom, ouvert) => {
|
|
34
|
+
for (const tiroir of tiroirs(nom)) {
|
|
35
|
+
if (ouvert) tiroir.setAttribute('data-open', '')
|
|
36
|
+
else tiroir.removeAttribute('data-open')
|
|
37
|
+
}
|
|
38
|
+
const cle = nom || ''
|
|
39
|
+
for (const btn of document.querySelectorAll('[data-zv-drawer-open]')) {
|
|
40
|
+
if ((btn.getAttribute('data-zv-drawer-open') || '') === cle) {
|
|
41
|
+
btn.setAttribute('aria-expanded', String(ouvert))
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Délégation : un seul écouteur pour toute la page. Les liens du tiroir
|
|
47
|
+
// sont souvent rendus après coup — les câbler un par un obligerait à
|
|
48
|
+
// relancer le module à chaque fois.
|
|
49
|
+
document.addEventListener('click', (e) => {
|
|
50
|
+
const cible = e.target instanceof Element ? e.target : null
|
|
51
|
+
if (!cible) return
|
|
52
|
+
const ouvre = cible.closest('[data-zv-drawer-open]')
|
|
53
|
+
if (ouvre) {
|
|
54
|
+
basculer(ouvre.getAttribute('data-zv-drawer-open') || '', true)
|
|
55
|
+
return
|
|
56
|
+
}
|
|
57
|
+
const ferme = cible.closest('[data-zv-drawer-close]')
|
|
58
|
+
if (ferme) basculer(ferme.getAttribute('data-zv-drawer-close') || '', false)
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
document.addEventListener('keydown', (e) => {
|
|
62
|
+
if (e.key !== 'Escape') return
|
|
63
|
+
for (const tiroir of document.querySelectorAll('[data-zv-drawer][data-open]')) {
|
|
64
|
+
basculer(tiroir.getAttribute('data-zv-drawer') || '', false)
|
|
65
|
+
}
|
|
66
|
+
})
|
|
67
|
+
})()
|