@zevra/ui 0.15.0 → 0.16.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 +63 -0
- package/dist/catalogue.d.ts.map +1 -1
- package/dist/catalogue.js +15 -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 +21 -0
- package/dist/layout.d.ts.map +1 -1
- package/dist/layout.js +17 -0
- package/dist/layout.js.map +1 -1
- package/package.json +4 -2
- package/react/catalogue.tsx +39 -1
- package/react/css-inline.ts +1 -1
- package/react/layout.tsx +53 -0
- package/src/layout.css +101 -82
- package/src/navigation.css +89 -0
- package/src/rail.js +94 -0
package/react/layout.tsx
CHANGED
|
@@ -354,6 +354,59 @@ export function LockupWord({
|
|
|
354
354
|
return <img alt={alt} className={cx('zv-lockup__word', className)} {...reste} />
|
|
355
355
|
}
|
|
356
356
|
|
|
357
|
+
/* ─── Rail de lecture ──────────────────────────────────────────────── */
|
|
358
|
+
|
|
359
|
+
/** La table des matières d'une page longue, posée sur le côté. Elle navigue
|
|
360
|
+
* (chaque entrée est une ancre) et elle suit — poser `data-zv-rail` et
|
|
361
|
+
* importer `@zevra/ui/rail.js` pour que l'entrée courante s'allume. Sans le
|
|
362
|
+
* module, le rail navigue toujours : rien ne casse, le suivi manque.
|
|
363
|
+
*
|
|
364
|
+
* ⚠️ Elle demande de la place à côté du texte, et disparaît sous le point
|
|
365
|
+
* de rupture : un rail fixe sur un téléphone se pose sur la lecture. */
|
|
366
|
+
export function Rail({
|
|
367
|
+
gauche,
|
|
368
|
+
className,
|
|
369
|
+
...reste
|
|
370
|
+
}: Omit<HTMLAttributes<HTMLElement>, 'className'> & {
|
|
371
|
+
gauche?: boolean
|
|
372
|
+
className?: string
|
|
373
|
+
'aria-label': string
|
|
374
|
+
}) {
|
|
375
|
+
return (
|
|
376
|
+
<nav
|
|
377
|
+
data-zv-rail
|
|
378
|
+
className={cx('zv-rail zv-layer-chrome', gauche && 'zv-rail--gauche', className)}
|
|
379
|
+
{...reste}
|
|
380
|
+
/>
|
|
381
|
+
)
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
/** ⚠️ `href` doit désigner une ancre de la page (`#section`) : c'est elle
|
|
385
|
+
* que le module surveille, et c'est le balisage qui fait foi — il n'y a
|
|
386
|
+
* pas de liste de sections à tenir à jour ailleurs. */
|
|
387
|
+
export function RailItem({
|
|
388
|
+
courant,
|
|
389
|
+
className,
|
|
390
|
+
children,
|
|
391
|
+
...reste
|
|
392
|
+
}: Omit<HTMLAttributes<HTMLAnchorElement>, 'className'> & {
|
|
393
|
+
courant?: boolean
|
|
394
|
+
href: string
|
|
395
|
+
className?: string
|
|
396
|
+
children?: ReactNode
|
|
397
|
+
}) {
|
|
398
|
+
return (
|
|
399
|
+
<a
|
|
400
|
+
aria-current={courant ? 'true' : undefined}
|
|
401
|
+
className={cx('zv-rail__item', courant && 'zv-rail__item--on', className)}
|
|
402
|
+
{...reste}
|
|
403
|
+
>
|
|
404
|
+
<span className="zv-mono zv-mono--micro">{children}</span>
|
|
405
|
+
<span className="zv-rail__dot" aria-hidden="true" />
|
|
406
|
+
</a>
|
|
407
|
+
)
|
|
408
|
+
}
|
|
409
|
+
|
|
357
410
|
/* ─── Frise d'étapes ───────────────────────────────────────────────── */
|
|
358
411
|
|
|
359
412
|
export interface StepsProps extends Div {
|
package/src/layout.css
CHANGED
|
@@ -313,88 +313,6 @@
|
|
|
313
313
|
color: var(--label);
|
|
314
314
|
}
|
|
315
315
|
|
|
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
316
|
/* ─── Sélecteur de langue ───────────────────────────────────────────────
|
|
399
317
|
* Des initiales au registre mono, pas un menu : trois langues se lisent
|
|
400
318
|
* d'un coup d'œil, et un menu pour trois entrées coûte un clic pour rien.
|
|
@@ -877,6 +795,107 @@
|
|
|
877
795
|
}
|
|
878
796
|
}
|
|
879
797
|
|
|
798
|
+
/* ─── Rail de lecture ───────────────────────────────────────────────────
|
|
799
|
+
*
|
|
800
|
+
* La table des matières d'une page longue, posée sur le côté : un libellé,
|
|
801
|
+
* un point, et le point courant qui s'allume. Elle NAVIGUE (chaque entrée
|
|
802
|
+
* est une ancre) et elle SUIT (le module rail.js marque la section en
|
|
803
|
+
* cours) — les deux, sinon ce n'est qu'un décor qui occupe la marge.
|
|
804
|
+
*
|
|
805
|
+
* ⚠️ Elle demande de la place à côté du texte. Sous le point de rupture
|
|
806
|
+
* elle disparaît : un rail fixe sur un téléphone se pose sur la lecture.
|
|
807
|
+
* Au-dessus, c'est la mesure de la page qui décide — une page dont le
|
|
808
|
+
* contenu court d'un bord à l'autre jusqu'à 1100px gagnera à relever le
|
|
809
|
+
* seuil chez elle.
|
|
810
|
+
*
|
|
811
|
+
* L'empilement vient de .zv-layer-chrome, à poser avec : le rail est du
|
|
812
|
+
* chrome, il passe au-dessus des décors de scène. */
|
|
813
|
+
|
|
814
|
+
.zv-rail {
|
|
815
|
+
position: fixed;
|
|
816
|
+
right: 40px;
|
|
817
|
+
top: 50%;
|
|
818
|
+
translate: 0 -50%;
|
|
819
|
+
display: none;
|
|
820
|
+
flex-direction: column;
|
|
821
|
+
align-items: flex-end;
|
|
822
|
+
gap: 16px;
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
@media (min-width: 768px) {
|
|
826
|
+
.zv-rail {
|
|
827
|
+
display: flex;
|
|
828
|
+
}
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
/* Sur la gauche, l'ordre s'inverse : le point précède le libellé, sinon il
|
|
832
|
+
* se retrouve au milieu de la colonne au lieu d'en tenir le bord. */
|
|
833
|
+
.zv-rail--gauche {
|
|
834
|
+
right: auto;
|
|
835
|
+
left: 40px;
|
|
836
|
+
align-items: flex-start;
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
.zv-rail--gauche .zv-rail__item {
|
|
840
|
+
flex-direction: row-reverse;
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
.zv-rail__item {
|
|
844
|
+
display: flex;
|
|
845
|
+
align-items: center;
|
|
846
|
+
gap: 10px;
|
|
847
|
+
padding: 3px 0;
|
|
848
|
+
color: var(--muted);
|
|
849
|
+
text-decoration: none;
|
|
850
|
+
/* Un rail flotte au-dessus de ce que la page met dans sa marge : un
|
|
851
|
+
* libellé gris pâle sur un décor pâle devient illisible par manque de
|
|
852
|
+
* contraste, sans qu'aucun empilement soit en cause. Le halo couleur
|
|
853
|
+
* papier le détache de tout ce qui passe dessous, sans lui donner de
|
|
854
|
+
* cadre. */
|
|
855
|
+
text-shadow:
|
|
856
|
+
0 0 6px var(--paper),
|
|
857
|
+
0 0 12px var(--paper);
|
|
858
|
+
transition: color 160ms var(--zv-easing, cubic-bezier(0.2, 0.7, 0.2, 1));
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
/* Le libellé est posé au registre mono par l'appelant : il doit suivre la
|
|
862
|
+
* couleur de son entrée, pas garder la sienne. */
|
|
863
|
+
.zv-rail__item .zv-mono {
|
|
864
|
+
color: inherit;
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
.zv-rail__item:hover {
|
|
868
|
+
color: var(--accent-deep);
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
.zv-rail__dot {
|
|
872
|
+
width: 8px;
|
|
873
|
+
height: 8px;
|
|
874
|
+
flex: none;
|
|
875
|
+
border-radius: 50%;
|
|
876
|
+
background: var(--accent-line);
|
|
877
|
+
transition:
|
|
878
|
+
background 160ms var(--zv-easing, cubic-bezier(0.2, 0.7, 0.2, 1)),
|
|
879
|
+
scale 160ms var(--zv-easing, cubic-bezier(0.2, 0.7, 0.2, 1));
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
.zv-rail__item:hover .zv-rail__dot {
|
|
883
|
+
background: var(--accent);
|
|
884
|
+
scale: 1.35;
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
/* L'entrée courante, posée par rail.js. Le halo du point est le seul
|
|
888
|
+
* signal dont une navigation discrète a besoin — pas de fond, pas de
|
|
889
|
+
* filet. */
|
|
890
|
+
.zv-rail__item--on {
|
|
891
|
+
color: var(--accent-deep);
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
.zv-rail__item--on .zv-rail__dot {
|
|
895
|
+
background: var(--iris);
|
|
896
|
+
box-shadow: 0 0 0 4px color-mix(in srgb, var(--iris) 18%, transparent);
|
|
897
|
+
}
|
|
898
|
+
|
|
880
899
|
/* ─── Le seuil d'une nav riche ──────────────────────────────────────────
|
|
881
900
|
*
|
|
882
901
|
* La bascule vers le tiroir est à 767px, réglée sur la nav de référence :
|
package/src/navigation.css
CHANGED
|
@@ -496,6 +496,95 @@
|
|
|
496
496
|
* clic extérieur, fermeture à Échap. Le CSS ci-dessus est prêt à les recevoir
|
|
497
497
|
* (aria-selected, :focus-visible) ; le composant doit les écrire. */
|
|
498
498
|
|
|
499
|
+
/* ─── Entrées à menu déroulant de la barre vitrine ──────────────────────
|
|
500
|
+
*
|
|
501
|
+
* ⚠️ CE BLOC DOIT RESTER APRÈS `.zv-menu`, et donc dans CE fichier. Le
|
|
502
|
+
* panneau d'une entrée porte les deux classes ; `.zv-navdd__menu` reprend à
|
|
503
|
+
* `.zv-menu` son display, sa position et son ancrage, à spécificité égale —
|
|
504
|
+
* c'est l'ORDRE qui tranche. Posé dans layout.css, qu'index.css importe
|
|
505
|
+
* AVANT navigation.css, il perdait : les trois menus d'une barre
|
|
506
|
+
* s'affichaient en même temps, ouverts en permanence.
|
|
507
|
+
*
|
|
508
|
+
* Une nav de vitrine qui porte l'architecture d'un site — des familles,
|
|
509
|
+
* pas quatre liens — a besoin de déplier. Ouverture au survol ET au focus
|
|
510
|
+
* (`:focus-within`) : en CSS seul, donc accessible au clavier sans une
|
|
511
|
+
* ligne de JavaScript et sans état à tenir.
|
|
512
|
+
*
|
|
513
|
+
* Le panneau est le .zv-menu du paquet, simplement ancré sous l'entrée. */
|
|
514
|
+
|
|
515
|
+
.zv-navdd {
|
|
516
|
+
position: relative;
|
|
517
|
+
display: inline-flex;
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
/* Le bouton porte .zv-navlink : il doit se rendre EXACTEMENT comme un lien
|
|
521
|
+
* simple de la barre. Piège à éviter — un `font: inherit`, réflexe habituel
|
|
522
|
+
* pour dénuder un bouton, remet la fonte du CONTENEUR (16px/400) là où
|
|
523
|
+
* .zv-navlink pose 14px/500 : les entrées à menu deviennent plus grosses
|
|
524
|
+
* que les autres. On ne reprend donc du bouton que ce qui doit disparaître. */
|
|
525
|
+
.zv-navdd__btn {
|
|
526
|
+
display: inline-flex;
|
|
527
|
+
align-items: center;
|
|
528
|
+
gap: 5px;
|
|
529
|
+
padding: 0;
|
|
530
|
+
border: 0;
|
|
531
|
+
background: none;
|
|
532
|
+
cursor: pointer;
|
|
533
|
+
font-family: inherit;
|
|
534
|
+
line-height: inherit;
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
.zv-navdd__chevron {
|
|
538
|
+
width: 12px;
|
|
539
|
+
height: 12px;
|
|
540
|
+
fill: none;
|
|
541
|
+
stroke: currentColor;
|
|
542
|
+
stroke-width: 2;
|
|
543
|
+
/* Bouts francs : le trait carré du jeu d'icônes (25/08/2026). */
|
|
544
|
+
stroke-linecap: square;
|
|
545
|
+
stroke-linejoin: miter;
|
|
546
|
+
transition: transform 160ms var(--zv-easing, cubic-bezier(0.2, 0.7, 0.2, 1));
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
.zv-navdd:hover .zv-navdd__chevron,
|
|
550
|
+
.zv-navdd:focus-within .zv-navdd__chevron {
|
|
551
|
+
transform: rotate(180deg);
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
/* Le pont invisible (::before) couvre l'interstice entre l'entrée et son
|
|
555
|
+
* panneau : sans lui, le survol se rompt en descendant vers le menu et le
|
|
556
|
+
* menu se referme sous le curseur. */
|
|
557
|
+
.zv-navdd__menu {
|
|
558
|
+
position: absolute;
|
|
559
|
+
top: 100%;
|
|
560
|
+
left: 50%;
|
|
561
|
+
translate: -50% 0;
|
|
562
|
+
padding-top: 14px;
|
|
563
|
+
display: none;
|
|
564
|
+
min-width: 250px;
|
|
565
|
+
z-index: 50;
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
.zv-navdd__menu::before {
|
|
569
|
+
content: '';
|
|
570
|
+
position: absolute;
|
|
571
|
+
inset: 0 0 auto 0;
|
|
572
|
+
height: 14px;
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
.zv-navdd:hover .zv-navdd__menu,
|
|
576
|
+
.zv-navdd:focus-within .zv-navdd__menu {
|
|
577
|
+
display: flex;
|
|
578
|
+
flex-direction: column;
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
.zv-navdd__sep {
|
|
582
|
+
display: block;
|
|
583
|
+
height: 1px;
|
|
584
|
+
margin: 6px 0;
|
|
585
|
+
background: var(--hairline);
|
|
586
|
+
}
|
|
587
|
+
|
|
499
588
|
/* ─── Popover ───────────────────────────────────────────────────────────
|
|
500
589
|
* HORS RELEVÉ (25/08/2026, voir DECISIONS.md). Le menu impose ses items ;
|
|
501
590
|
* il n'existait aucune surface ancrée LIBRE — aide contextuelle,
|
package/src/rail.js
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/* Zevra UI — rail.js : le rail de lecture suit la section en cours.
|
|
2
|
+
*
|
|
3
|
+
* Rôle exact, et rien d'autre : poser `.zv-rail__item--on` et
|
|
4
|
+
* `aria-current` sur l'entrée dont la section est à l'écran. La navigation
|
|
5
|
+
* elle-même est du HTML — chaque entrée est une ancre, et elle fonctionne
|
|
6
|
+
* sans ce module.
|
|
7
|
+
*
|
|
8
|
+
* Le contrat, en un attribut :
|
|
9
|
+
* [data-zv-rail] le rail (.zv-rail). Ses entrées sont ses <a href="#…">,
|
|
10
|
+
* et chaque href désigne la cible à surveiller.
|
|
11
|
+
*
|
|
12
|
+
* Rien à déclarer d'autre : l'ordre des entrées, leurs cibles et leur
|
|
13
|
+
* nombre se lisent dans le balisage. Un rail dont une ancre ne résout pas
|
|
14
|
+
* garde l'entrée cliquable et l'ignore au suivi, plutôt que de décaler
|
|
15
|
+
* tout le reste — c'est le décalage silencieux qui coûte cher.
|
|
16
|
+
*
|
|
17
|
+
* ─── Pourquoi pas un IntersectionObserver ───────────────────────────────
|
|
18
|
+
* Parce que les ancres d'un rail sont souvent des points MATÉRIELS de 1px
|
|
19
|
+
* posés en tête de section. Réduit à sa bande centrale, l'observateur ne
|
|
20
|
+
* les voit traverser que pendant quelques images — un défilement rapide
|
|
21
|
+
* saute la bande entière et l'entrée ne s'allume jamais. On lit donc la
|
|
22
|
+
* position : l'entrée courante est la DERNIÈRE dont l'ancre a passé la
|
|
23
|
+
* ligne de lecture. C'est déterministe, quelle que soit la vitesse.
|
|
24
|
+
*
|
|
25
|
+
* S'importe une fois par page : `import '@zevra/ui/rail.js'`. Sans lui, le
|
|
26
|
+
* rail navigue mais ne suit pas — dégradation douce, pas de page cassée.
|
|
27
|
+
*/
|
|
28
|
+
(() => {
|
|
29
|
+
if (typeof document === 'undefined') return
|
|
30
|
+
if (document.documentElement.dataset.zvRail === 'on') return
|
|
31
|
+
document.documentElement.dataset.zvRail = 'on'
|
|
32
|
+
|
|
33
|
+
/** Hauteur de la ligne de lecture, en fraction du viewport. 45 % : un peu
|
|
34
|
+
* au-dessus du milieu, là où l'œil se pose vraiment. */
|
|
35
|
+
const LIGNE = 0.45
|
|
36
|
+
|
|
37
|
+
const rails = () => Array.from(document.querySelectorAll('[data-zv-rail]'))
|
|
38
|
+
|
|
39
|
+
const suivre = (rail) => {
|
|
40
|
+
const entrees = Array.from(rail.querySelectorAll('a[href^="#"]'))
|
|
41
|
+
if (!entrees.length) return
|
|
42
|
+
|
|
43
|
+
const cibles = entrees.map((a) => {
|
|
44
|
+
const id = decodeURIComponent(a.getAttribute('href').slice(1))
|
|
45
|
+
return id ? document.getElementById(id) : null
|
|
46
|
+
})
|
|
47
|
+
if (!cibles.some(Boolean)) return
|
|
48
|
+
|
|
49
|
+
let courant = -1
|
|
50
|
+
const marquer = (i) => {
|
|
51
|
+
if (i === courant) return
|
|
52
|
+
courant = i
|
|
53
|
+
entrees.forEach((a, k) => {
|
|
54
|
+
a.classList.toggle('zv-rail__item--on', k === i)
|
|
55
|
+
if (k === i) a.setAttribute('aria-current', 'true')
|
|
56
|
+
else a.removeAttribute('aria-current')
|
|
57
|
+
})
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const relire = () => {
|
|
61
|
+
const ligne = window.innerHeight * LIGNE
|
|
62
|
+
let actif = 0
|
|
63
|
+
for (let i = 0; i < cibles.length; i++) {
|
|
64
|
+
const c = cibles[i]
|
|
65
|
+
if (c && c.getBoundingClientRect().top <= ligne) actif = i
|
|
66
|
+
}
|
|
67
|
+
marquer(actif)
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Une lecture par image au plus : le défilement émet bien plus souvent
|
|
71
|
+
// que l'écran ne se rafraîchit, et chaque lecture coûte un reflow.
|
|
72
|
+
let prevu = false
|
|
73
|
+
const planifier = () => {
|
|
74
|
+
if (prevu) return
|
|
75
|
+
prevu = true
|
|
76
|
+
requestAnimationFrame(() => {
|
|
77
|
+
prevu = false
|
|
78
|
+
relire()
|
|
79
|
+
})
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
addEventListener('scroll', planifier, { passive: true })
|
|
83
|
+
addEventListener('resize', planifier, { passive: true })
|
|
84
|
+
relire()
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const demarrer = () => rails().forEach(suivre)
|
|
88
|
+
|
|
89
|
+
if (document.readyState === 'loading') {
|
|
90
|
+
document.addEventListener('DOMContentLoaded', demarrer, { once: true })
|
|
91
|
+
} else {
|
|
92
|
+
demarrer()
|
|
93
|
+
}
|
|
94
|
+
})()
|