1/**
2 * Sticky Section Nav
3 * Shows/hides based on scroll position and highlights current section.
4 */
5
6export function initSectionNav() {
7 const nav = document.getElementById('section-nav');
8 if (!nav) return;
9
10 const items = nav.querySelectorAll('.section-nav-item');
11 const sectionIds = Array.from(items).map(item => item.dataset.section);
12
13 // Show/hide nav based on scroll position
14 const hero = document.getElementById('hero');
15 const footer = document.querySelector('.site-footer');
16 if (!hero) return;
17
18 let ticking = false;
19
20 // Returns the element's top position relative to the document,
21 // which works even when the element is inside a positioned parent.
22 function docTop(el) {
23 return el.getBoundingClientRect().top + window.scrollY;
24 }
25
26 function updateNav() {
27 const scrollY = window.scrollY;
28 const heroBottom = hero.offsetTop + hero.offsetHeight - 100;
29 const footerTop = footer ? docTop(footer) : Infinity;
30 const viewportBottom = scrollY + window.innerHeight;
31
32 // Show nav after hero, hide when footer is visible
33 if (scrollY > heroBottom && viewportBottom < footerTop + 60) {
34 nav.classList.add('is-visible');
35 } else {
36 nav.classList.remove('is-visible');
37 }
38
39 // Find current section
40 let currentSection = null;
41 const viewportMiddle = scrollY + window.innerHeight * 0.4;
42
43 for (let i = sectionIds.length - 1; i >= 0; i--) {
44 const section = document.getElementById(sectionIds[i]);
45 if (section && docTop(section) <= viewportMiddle) {
46 currentSection = sectionIds[i];
47 break;
48 }
49 }
50
51 // If the current section shares its top row with siblings (e.g. side-by-side
52 // changelog + FAQ on desktop), treat all of them as active.
53 const activeSections = new Set();
54 if (currentSection) {
55 const currentEl = document.getElementById(currentSection);
56 const currentTop = currentEl ? docTop(currentEl) : 0;
57 sectionIds.forEach(id => {
58 const el = document.getElementById(id);
59 if (el && Math.abs(docTop(el) - currentTop) < 4) {
60 activeSections.add(id);
61 }
62 });
63 }
64
65 // Update active state
66 items.forEach(item => {
67 if (activeSections.has(item.dataset.section)) {
68 item.classList.add('is-active');
69 } else {
70 item.classList.remove('is-active');
71 }
72 });
73
74 ticking = false;
75 }
76
77 window.addEventListener('scroll', () => {
78 if (!ticking) {
79 requestAnimationFrame(updateNav);
80 ticking = true;
81 }
82 }, { passive: true });
83
84 // Initial check
85 updateNav();
86}