@zevra/ui 0.13.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 +128 -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/logos/index.json +21 -1
- package/logos/svg/zevra-wordmark-current.svg +7 -0
- 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/components.css +62 -0
- package/src/layout.css +195 -3
- package/src/nav.js +67 -0
- package/src/tokens.css +1 -1
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/components.css
CHANGED
|
@@ -967,3 +967,65 @@
|
|
|
967
967
|
font-size: 21px;
|
|
968
968
|
}
|
|
969
969
|
}
|
|
970
|
+
|
|
971
|
+
/* ─── Le lockup : la marque et le mot ───────────────────────────────────
|
|
972
|
+
*
|
|
973
|
+
* Jusqu'ici le nom d'un produit voyageait cuit dans un PNG, marque et mot
|
|
974
|
+
* ensemble. Un lockup de 555×200 dont la capitale mesure 70px se retrouvait
|
|
975
|
+
* affiché à 15px de capitale dans une barre de navigation et 10px dans un
|
|
976
|
+
* pied de page : réduit de près de cinq fois, chaque fût tombait sur une
|
|
977
|
+
* fraction de pixel et recevait sa propre dose de lissage. Le mot devenait
|
|
978
|
+
* gris et le V, seul à n'avoir aucun fût vertical sur quoi s'aligner,
|
|
979
|
+
* semblait flotter.
|
|
980
|
+
*
|
|
981
|
+
* Le mot est donc un SVG (`logos/svg/<produit>-wordmark.svg`), à
|
|
982
|
+
* `currentColor` : net à toute taille, et il prend enfin les nuances encre
|
|
983
|
+
* / navy / blanc que le registre des logos décrit. La marque reste le PNG
|
|
984
|
+
* couleur du paquet — c'est un dégradé, il ne se recolore pas.
|
|
985
|
+
*
|
|
986
|
+
* ⚠️ Ces tracés sont RELEVÉS sur le lockup bitmap d'origine, faute de son
|
|
987
|
+
* fichier vectoriel. Ils sont fidèles à 0,1 % en largeur et nettement plus
|
|
988
|
+
* lisibles que le bitmap partout où le site les pose, mais ils portent les
|
|
989
|
+
* approximations du relevé : réservés à l'ÉCRAN, jusqu'à 120px de capitale.
|
|
990
|
+
* Au-delà — grand format, impression — exiger le fichier d'origine.
|
|
991
|
+
*
|
|
992
|
+
* Les proportions viennent du lockup, mesurées au pixel : la marque vaut
|
|
993
|
+
* 2,86 fois la capitale, l'écart entre les deux 0,64 fois, et les deux
|
|
994
|
+
* pièces se centrent l'une sur l'autre (elles ne partagent pas de ligne de
|
|
995
|
+
* base). Tout se pilote par --zv-cap, qui EST la hauteur de capitale : le
|
|
996
|
+
* SVG du mot a un viewBox dont la hauteur vaut la capitale, si bien qu'une
|
|
997
|
+
* hauteur CSS posée dessus se lit directement. */
|
|
998
|
+
|
|
999
|
+
.zv-lockup {
|
|
1000
|
+
--zv-cap: 15px;
|
|
1001
|
+
display: inline-flex;
|
|
1002
|
+
align-items: center;
|
|
1003
|
+
gap: calc(var(--zv-cap) * 0.64);
|
|
1004
|
+
color: var(--ink);
|
|
1005
|
+
text-decoration: none;
|
|
1006
|
+
}
|
|
1007
|
+
|
|
1008
|
+
.zv-lockup__mark {
|
|
1009
|
+
height: calc(var(--zv-cap) * 2.86);
|
|
1010
|
+
width: auto;
|
|
1011
|
+
display: block;
|
|
1012
|
+
}
|
|
1013
|
+
|
|
1014
|
+
.zv-lockup__word {
|
|
1015
|
+
height: var(--zv-cap);
|
|
1016
|
+
width: auto;
|
|
1017
|
+
display: block;
|
|
1018
|
+
}
|
|
1019
|
+
|
|
1020
|
+
/* Le seuil de lisibilité. Sous 11px de capitale, le mot ne se lit plus :
|
|
1021
|
+
* ce n'est plus une question de netteté mais de taille, et aucun fichier
|
|
1022
|
+
* n'y changera rien. On montre la marque, seule — c'est elle qui identifie.
|
|
1023
|
+
* Poser cette classe, ou masquer .zv-lockup__word sous un point de rupture
|
|
1024
|
+
* quand la barre se resserre. */
|
|
1025
|
+
.zv-lockup--marque-seule {
|
|
1026
|
+
gap: 0;
|
|
1027
|
+
}
|
|
1028
|
+
|
|
1029
|
+
.zv-lockup--marque-seule .zv-lockup__word {
|
|
1030
|
+
display: none;
|
|
1031
|
+
}
|
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
|
+
})()
|
package/src/tokens.css
CHANGED
|
@@ -68,7 +68,7 @@
|
|
|
68
68
|
--err-line: #efc9cd;
|
|
69
69
|
|
|
70
70
|
/* Typo — Google Fonts : Outfit (display), Public Sans (texte), Space Grotesk (labels) */
|
|
71
|
-
--font-display: "Outfit", "Helvetica Neue", Arial, sans-serif; /* titres 200-400
|
|
71
|
+
--font-display: "Outfit", "Helvetica Neue", Arial, sans-serif; /* titres 200-400 */
|
|
72
72
|
--font-text: "Public Sans", "Helvetica Neue", Arial, sans-serif; /* 400/600 */
|
|
73
73
|
--font-mono: "Space Grotesk", "Menlo", monospace; /* labels caps, 0.18-0.2em */
|
|
74
74
|
/* Chasse fixe RÉELLE, pour le code seulement. --font-mono (Space Grotesk)
|