170 lines
5.5 KiB
JavaScript
170 lines
5.5 KiB
JavaScript
// Smooth scroll for navigation links
|
|
function getHeaderHeight() {
|
|
const header = document.querySelector('header');
|
|
return header ? header.offsetHeight : 80;
|
|
}
|
|
|
|
function smoothScrollToSection(hash) {
|
|
const target = document.querySelector(hash);
|
|
if (!target) return;
|
|
|
|
// Recalculate header height at the moment of scrolling
|
|
const headerHeight = getHeaderHeight();
|
|
|
|
// Use getBoundingClientRect for robust position calc across layouts
|
|
const targetTopRelative = target.getBoundingClientRect().top;
|
|
const absoluteTargetTop = window.scrollY + targetTopRelative;
|
|
|
|
// Exact offset: align section start just below the header bottom
|
|
const top = Math.max(absoluteTargetTop - headerHeight, 0);
|
|
|
|
window.scrollTo({ top, behavior: 'smooth' });
|
|
}
|
|
|
|
function updateURLHash(hash) {
|
|
// Update URL hash without triggering scroll
|
|
if (hash && hash !== '#') {
|
|
history.pushState(null, null, hash);
|
|
}
|
|
}
|
|
|
|
function scrollToTop() {
|
|
// Remove hash from URL and scroll to top
|
|
history.pushState(null, null, window.location.pathname);
|
|
window.scrollTo({ top: 0, behavior: 'smooth' });
|
|
}
|
|
|
|
document.querySelectorAll('nav a[href^="#"]').forEach(anchor => {
|
|
anchor.addEventListener('click', function (e) {
|
|
e.preventDefault();
|
|
|
|
// If mobile menu is open, close it first so header height is correct
|
|
const siteNavEl = document.getElementById('site-nav');
|
|
const menuBtn = document.getElementById('menu-toggle');
|
|
const wasOpen = siteNavEl && siteNavEl.classList.contains('open');
|
|
if (wasOpen) {
|
|
siteNavEl.classList.remove('open');
|
|
if (menuBtn) menuBtn.setAttribute('aria-expanded', 'false');
|
|
// Wait for next frame so layout updates and header height is correct
|
|
requestAnimationFrame(() => {
|
|
smoothScrollToSection(this.getAttribute('href'));
|
|
updateURLHash(this.getAttribute('href'));
|
|
});
|
|
} else {
|
|
smoothScrollToSection(this.getAttribute('href'));
|
|
updateURLHash(this.getAttribute('href'));
|
|
}
|
|
});
|
|
});
|
|
|
|
// Add active class to navigation based on scroll position
|
|
const sections = document.querySelectorAll('section[id]');
|
|
const navLinks = document.querySelectorAll('nav a');
|
|
|
|
function setActiveNav() {
|
|
let current = '';
|
|
const headerHeight = getHeaderHeight();
|
|
|
|
sections.forEach(section => {
|
|
const sectionTop = section.offsetTop;
|
|
const sectionHeight = section.clientHeight;
|
|
// Account for fixed header in active link detection
|
|
if (window.scrollY + headerHeight + 30 >= sectionTop && window.scrollY < sectionTop + sectionHeight) {
|
|
current = section.getAttribute('id');
|
|
}
|
|
});
|
|
|
|
navLinks.forEach(link => {
|
|
link.classList.remove('active');
|
|
if (link.getAttribute('href') === `#${current}`) {
|
|
link.classList.add('active');
|
|
}
|
|
});
|
|
}
|
|
|
|
window.addEventListener('scroll', setActiveNav);
|
|
window.addEventListener('load', setActiveNav);
|
|
|
|
// Adjust on resize (header height can change)
|
|
window.addEventListener('resize', () => {
|
|
// Re-run active link calculation to keep alignment correct
|
|
setActiveNav();
|
|
});
|
|
|
|
// Handle direct hash navigation (e.g., page load with #section)
|
|
window.addEventListener('load', () => {
|
|
if (location.hash) {
|
|
// Use the same precise scroll logic on initial load
|
|
smoothScrollToSection(location.hash);
|
|
}
|
|
});
|
|
|
|
// Lightbox for screenshots (desktop only behavior)
|
|
const lightbox = document.getElementById('lightbox');
|
|
const lightboxImg = document.getElementById('lightbox-image');
|
|
const lightboxCaption = document.getElementById('lightbox-caption');
|
|
|
|
function openLightbox(src, caption) {
|
|
if (!lightbox) return;
|
|
lightboxImg.src = src;
|
|
lightboxCaption.textContent = caption || '';
|
|
lightbox.setAttribute('aria-hidden', 'false');
|
|
document.body.style.overflow = 'hidden';
|
|
}
|
|
|
|
function closeLightbox() {
|
|
if (!lightbox) return;
|
|
lightbox.setAttribute('aria-hidden', 'true');
|
|
lightboxImg.src = '';
|
|
document.body.style.overflow = '';
|
|
}
|
|
|
|
// Delegate clicks on screenshots
|
|
document.addEventListener('click', (e) => {
|
|
const img = e.target.closest('.screenshot img');
|
|
if (img) {
|
|
// Only open overlay on wider screens
|
|
if (window.innerWidth >= 768) {
|
|
openLightbox(img.src, img.alt || img.nextElementSibling?.textContent || '');
|
|
e.preventDefault();
|
|
}
|
|
}
|
|
|
|
if (e.target.matches('[data-close]')) {
|
|
closeLightbox();
|
|
}
|
|
});
|
|
|
|
// Close on ESC
|
|
window.addEventListener('keydown', (e) => {
|
|
if (e.key === 'Escape') closeLightbox();
|
|
});
|
|
|
|
// Mobile burger menu
|
|
const menuToggle = document.getElementById('menu-toggle');
|
|
const siteNav = document.getElementById('site-nav');
|
|
if (menuToggle && siteNav) {
|
|
menuToggle.addEventListener('click', () => {
|
|
const isOpen = siteNav.classList.toggle('open');
|
|
menuToggle.setAttribute('aria-expanded', isOpen ? 'true' : 'false');
|
|
});
|
|
|
|
// Close menu after navigating
|
|
siteNav.querySelectorAll('a').forEach(link => {
|
|
link.addEventListener('click', () => {
|
|
if (siteNav.classList.contains('open')) {
|
|
siteNav.classList.remove('open');
|
|
menuToggle.setAttribute('aria-expanded', 'false');
|
|
}
|
|
});
|
|
});
|
|
}
|
|
|
|
// Handle SPORE title click to scroll to top and remove hash
|
|
const sporeTitle = document.querySelector('.brand h1');
|
|
if (sporeTitle) {
|
|
sporeTitle.addEventListener('click', scrollToTop);
|
|
sporeTitle.style.cursor = 'pointer';
|
|
}
|
|
|