Files
pbakaus_impeccable/public/js/components/section-nav.js
T
Paul BakausandClaude Opus 4.6 5fee3148be Fix section-nav using wrong positions for nested sections
The changelog and FAQ sections are inside a positioned .changelog-faq-row
wrapper, so their offsetTop was 0 (relative to parent) instead of their
actual document position. This broke current-section detection and caused
both pills to appear permanently active. Use getBoundingClientRect()
instead, which returns correct absolute positions regardless of nesting.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 16:14:51 -07:00

87 lines
2.4 KiB
JavaScript

/**
* Sticky Section Nav
* Shows/hides based on scroll position and highlights current section.
*/
export function initSectionNav() {
const nav = document.getElementById('section-nav');
if (!nav) return;
const items = nav.querySelectorAll('.section-nav-item');
const sectionIds = Array.from(items).map(item => item.dataset.section);
// Show/hide nav based on scroll position
const hero = document.getElementById('hero');
const footer = document.querySelector('.site-footer');
if (!hero) return;
let ticking = false;
// Returns the element's top position relative to the document,
// which works even when the element is inside a positioned parent.
function docTop(el) {
return el.getBoundingClientRect().top + window.scrollY;
}
function updateNav() {
const scrollY = window.scrollY;
const heroBottom = hero.offsetTop + hero.offsetHeight - 100;
const footerTop = footer ? docTop(footer) : Infinity;
const viewportBottom = scrollY + window.innerHeight;
// Show nav after hero, hide when footer is visible
if (scrollY > heroBottom && viewportBottom < footerTop + 60) {
nav.classList.add('is-visible');
} else {
nav.classList.remove('is-visible');
}
// Find current section
let currentSection = null;
const viewportMiddle = scrollY + window.innerHeight * 0.4;
for (let i = sectionIds.length - 1; i >= 0; i--) {
const section = document.getElementById(sectionIds[i]);
if (section && docTop(section) <= viewportMiddle) {
currentSection = sectionIds[i];
break;
}
}
// If the current section shares its top row with siblings (e.g. side-by-side
// changelog + FAQ on desktop), treat all of them as active.
const activeSections = new Set();
if (currentSection) {
const currentEl = document.getElementById(currentSection);
const currentTop = currentEl ? docTop(currentEl) : 0;
sectionIds.forEach(id => {
const el = document.getElementById(id);
if (el && Math.abs(docTop(el) - currentTop) < 4) {
activeSections.add(id);
}
});
}
// Update active state
items.forEach(item => {
if (activeSections.has(item.dataset.section)) {
item.classList.add('is-active');
} else {
item.classList.remove('is-active');
}
});
ticking = false;
}
window.addEventListener('scroll', () => {
if (!ticking) {
requestAnimationFrame(updateNav);
ticking = true;
}
}, { passive: true });
// Initial check
updateNav();
}