section-nav.js

 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	function updateNav() {
21		const scrollY = window.scrollY;
22		const heroBottom = hero.offsetTop + hero.offsetHeight - 100;
23		const footerTop = footer ? footer.offsetTop : Infinity;
24		const viewportBottom = scrollY + window.innerHeight;
25
26		// Show nav after hero, hide when footer is visible
27		if (scrollY > heroBottom && viewportBottom < footerTop + 60) {
28			nav.classList.add('is-visible');
29		} else {
30			nav.classList.remove('is-visible');
31		}
32
33		// Find current section
34		let currentSection = null;
35		const viewportMiddle = scrollY + window.innerHeight * 0.4;
36
37		for (let i = sectionIds.length - 1; i >= 0; i--) {
38			const section = document.getElementById(sectionIds[i]);
39			if (section && section.offsetTop <= viewportMiddle) {
40				currentSection = sectionIds[i];
41				break;
42			}
43		}
44
45		// If the current section shares its top row with siblings (e.g. side-by-side
46		// changelog + FAQ on desktop), treat all of them as active.
47		const activeSections = new Set();
48		if (currentSection) {
49			const currentEl = document.getElementById(currentSection);
50			const currentTop = currentEl?.offsetTop ?? 0;
51			sectionIds.forEach(id => {
52				const el = document.getElementById(id);
53				if (el && Math.abs(el.offsetTop - currentTop) < 4) {
54					activeSections.add(id);
55				}
56			});
57		}
58
59		// Update active state
60		items.forEach(item => {
61			if (activeSections.has(item.dataset.section)) {
62				item.classList.add('is-active');
63			} else {
64				item.classList.remove('is-active');
65			}
66		});
67
68		ticking = false;
69	}
70
71	window.addEventListener('scroll', () => {
72		if (!ticking) {
73			requestAnimationFrame(updateNav);
74			ticking = true;
75		}
76	}, { passive: true });
77
78	// Initial check
79	updateNav();
80}