142 lines
5.5 KiB
JavaScript
142 lines
5.5 KiB
JavaScript
|
|
(function(){
|
||
|
|
// Progress bar - debounced to prevent layout thrashing
|
||
|
|
const bar = document.getElementById('progress-bar');
|
||
|
|
let scrollTimeout;
|
||
|
|
const onScroll = () => {
|
||
|
|
if (!bar) return;
|
||
|
|
clearTimeout(scrollTimeout);
|
||
|
|
scrollTimeout = setTimeout(() => {
|
||
|
|
const scrollTop = window.scrollY;
|
||
|
|
const docHeight = document.documentElement.scrollHeight - window.innerHeight;
|
||
|
|
const progress = docHeight > 0 ? (scrollTop / docHeight) * 100 : 0;
|
||
|
|
bar.style.width = progress + '%';
|
||
|
|
}, 10);
|
||
|
|
};
|
||
|
|
window.addEventListener('scroll', onScroll, { passive: true });
|
||
|
|
onScroll();
|
||
|
|
|
||
|
|
// Dark mode toggle
|
||
|
|
const darkToggle = document.getElementById('dark-toggle');
|
||
|
|
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)');
|
||
|
|
const setToggleUI = (isDark) => {
|
||
|
|
if (!darkToggle) return;
|
||
|
|
darkToggle.textContent = isDark ? '☀️' : '🌙';
|
||
|
|
darkToggle.setAttribute('aria-pressed', isDark ? 'true' : 'false');
|
||
|
|
darkToggle.setAttribute('aria-label', isDark ? 'Switch to light mode' : 'Switch to dark mode');
|
||
|
|
};
|
||
|
|
const applyTheme = (isDark) => {
|
||
|
|
document.documentElement.classList.toggle('dark', isDark);
|
||
|
|
localStorage.setItem('theme', isDark ? 'dark' : 'light');
|
||
|
|
setToggleUI(isDark);
|
||
|
|
};
|
||
|
|
const savedTheme = localStorage.getItem('theme');
|
||
|
|
const initialDark = savedTheme ? savedTheme === 'dark' : prefersDark.matches;
|
||
|
|
applyTheme(initialDark);
|
||
|
|
if (darkToggle) {
|
||
|
|
darkToggle.addEventListener('click', () => applyTheme(!document.documentElement.classList.contains('dark')));
|
||
|
|
}
|
||
|
|
|
||
|
|
// Accessibility controls
|
||
|
|
const fontInc = document.getElementById('font-inc');
|
||
|
|
const fontDec = document.getElementById('font-dec');
|
||
|
|
const contrastToggle = document.getElementById('contrast-toggle');
|
||
|
|
const motionToggle = document.getElementById('motion-toggle');
|
||
|
|
|
||
|
|
const applyA11y = () => {
|
||
|
|
const size = parseFloat(localStorage.getItem('fontScale') || '1');
|
||
|
|
const highContrast = localStorage.getItem('highContrast') === '1';
|
||
|
|
const reduceMotion = localStorage.getItem('reduceMotion') === '1';
|
||
|
|
|
||
|
|
// Apply all changes in a single batch to prevent layout thrashing
|
||
|
|
requestAnimationFrame(() => {
|
||
|
|
document.documentElement.style.setProperty('font-size', (size * 100) + '%');
|
||
|
|
document.documentElement.classList.toggle('contrast', highContrast);
|
||
|
|
document.documentElement.classList.toggle('reduce-motion', reduceMotion);
|
||
|
|
});
|
||
|
|
};
|
||
|
|
applyA11y();
|
||
|
|
|
||
|
|
if (fontInc) fontInc.addEventListener('click', () => {
|
||
|
|
const size = Math.min(1.5, (parseFloat(localStorage.getItem('fontScale') || '1') + 0.1));
|
||
|
|
localStorage.setItem('fontScale', String(size));
|
||
|
|
applyA11y();
|
||
|
|
});
|
||
|
|
if (fontDec) fontDec.addEventListener('click', () => {
|
||
|
|
const size = Math.max(0.8, (parseFloat(localStorage.getItem('fontScale') || '1') - 0.1));
|
||
|
|
localStorage.setItem('fontScale', String(size));
|
||
|
|
applyA11y();
|
||
|
|
});
|
||
|
|
if (contrastToggle) contrastToggle.addEventListener('click', () => {
|
||
|
|
const v = localStorage.getItem('highContrast') === '1' ? '0' : '1';
|
||
|
|
localStorage.setItem('highContrast', v);
|
||
|
|
applyA11y();
|
||
|
|
});
|
||
|
|
if (motionToggle) motionToggle.addEventListener('click', () => {
|
||
|
|
const v = localStorage.getItem('reduceMotion') === '1' ? '0' : '1';
|
||
|
|
localStorage.setItem('reduceMotion', v);
|
||
|
|
applyA11y();
|
||
|
|
});
|
||
|
|
|
||
|
|
// Mobile navigation toggle
|
||
|
|
const hamburger = document.getElementById('hamburger');
|
||
|
|
const primaryNav = document.getElementById('primary-nav');
|
||
|
|
if (hamburger && primaryNav) {
|
||
|
|
// Ensure initial aria state
|
||
|
|
hamburger.setAttribute('aria-expanded', primaryNav.classList.contains('hidden') ? 'false' : 'true');
|
||
|
|
hamburger.addEventListener('click', () => {
|
||
|
|
const isHidden = primaryNav.classList.toggle('hidden');
|
||
|
|
hamburger.classList.toggle('open', !isHidden);
|
||
|
|
hamburger.setAttribute('aria-expanded', isHidden ? 'false' : 'true');
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
// A11y popover (button + menu)
|
||
|
|
const a11yToggle = document.getElementById('a11y-toggle');
|
||
|
|
const a11yMenu = document.getElementById('a11y-menu');
|
||
|
|
if (a11yToggle && a11yMenu) {
|
||
|
|
const closeMenu = () => {
|
||
|
|
a11yMenu.classList.add('hidden');
|
||
|
|
a11yToggle.setAttribute('aria-expanded', 'false');
|
||
|
|
};
|
||
|
|
const openMenu = () => {
|
||
|
|
a11yMenu.classList.remove('hidden');
|
||
|
|
a11yToggle.setAttribute('aria-expanded', 'true');
|
||
|
|
};
|
||
|
|
a11yToggle.addEventListener('click', (e) => {
|
||
|
|
e.stopPropagation();
|
||
|
|
const isHidden = a11yMenu.classList.contains('hidden');
|
||
|
|
if (isHidden) openMenu(); else closeMenu();
|
||
|
|
});
|
||
|
|
document.addEventListener('click', (e) => {
|
||
|
|
if (!a11yMenu.classList.contains('hidden')) {
|
||
|
|
const within = a11yMenu.contains(e.target) || a11yToggle.contains(e.target);
|
||
|
|
if (!within) closeMenu();
|
||
|
|
}
|
||
|
|
});
|
||
|
|
document.addEventListener('keydown', (e) => {
|
||
|
|
if (e.key === 'Escape') closeMenu();
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
// Font toggle for footer
|
||
|
|
const fontToggle = document.getElementById('font-toggle');
|
||
|
|
if (fontToggle) {
|
||
|
|
const applyFontPref = (enabled) => {
|
||
|
|
document.documentElement.classList.toggle('system-fonts', enabled);
|
||
|
|
fontToggle.textContent = enabled ? 'Web Fonts' : 'System Fonts';
|
||
|
|
fontToggle.setAttribute('aria-pressed', enabled ? 'true' : 'false');
|
||
|
|
};
|
||
|
|
|
||
|
|
fontToggle.addEventListener('click', () => {
|
||
|
|
const currentlyEnabled = document.documentElement.classList.contains('system-fonts');
|
||
|
|
const next = !currentlyEnabled;
|
||
|
|
applyFontPref(next);
|
||
|
|
localStorage.setItem('systemFonts', next ? '1' : '0');
|
||
|
|
});
|
||
|
|
|
||
|
|
// Apply saved font preference
|
||
|
|
const savedFontPref = localStorage.getItem('systemFonts');
|
||
|
|
applyFontPref(savedFontPref === '1');
|
||
|
|
}
|
||
|
|
})();
|