hopefully fixed mobile nav
This commit is contained in:
parent
92bb628a2b
commit
4cf024c44b
2 changed files with 365 additions and 235 deletions
120
dist/css/styles.css
vendored
120
dist/css/styles.css
vendored
|
|
@ -963,6 +963,8 @@ button:focus-visible {
|
|||
font-weight: var(--font-medium);
|
||||
color: var(--text-secondary);
|
||||
min-height: 44px;
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
touch-action: manipulation;
|
||||
}
|
||||
|
||||
.nav-dropdown-trigger:hover {
|
||||
|
|
@ -977,30 +979,34 @@ button:focus-visible {
|
|||
visibility: visible;
|
||||
border: none;
|
||||
box-shadow: none;
|
||||
padding: var(--space-xs) 0 var(--space-md) var(--space-2xl);
|
||||
margin-top: 0;
|
||||
background: rgba(99, 102, 241, 0.02);
|
||||
border-left: 2px solid var(--border-primary);
|
||||
margin-left: var(--space-lg);
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
background: transparent;
|
||||
border-radius: 0;
|
||||
backdrop-filter: none;
|
||||
display: none;
|
||||
max-height: 0;
|
||||
overflow: hidden;
|
||||
transition: max-height 0.3s ease;
|
||||
/* Prevent layout shift and improve performance */
|
||||
will-change: max-height;
|
||||
contain: layout;
|
||||
/* Remove problematic animations */
|
||||
transition: none;
|
||||
will-change: auto;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .nav-dropdown-menu {
|
||||
background: rgba(99, 102, 241, 0.03);
|
||||
border-left-color: var(--border-secondary);
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.nav-dropdown-trigger[aria-expanded="true"] + .nav-dropdown-menu {
|
||||
display: block;
|
||||
max-height: 500px;
|
||||
padding: var(--space-xs) 0 var(--space-md) var(--space-2xl);
|
||||
margin-top: var(--space-xs);
|
||||
margin-left: var(--space-lg);
|
||||
border-left: 2px solid var(--border-primary);
|
||||
background: rgba(99, 102, 241, 0.02);
|
||||
}
|
||||
|
||||
[data-theme="dark"] .nav-dropdown-trigger[aria-expanded="true"] + .nav-dropdown-menu {
|
||||
background: rgba(99, 102, 241, 0.03);
|
||||
border-left-color: var(--border-secondary);
|
||||
}
|
||||
|
||||
.nav-dropdown-item {
|
||||
|
|
@ -1009,80 +1015,91 @@ button:focus-visible {
|
|||
min-height: 44px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
touch-action: manipulation;
|
||||
transition: transform 0.2s ease, background 0.2s ease;
|
||||
}
|
||||
|
||||
.nav-dropdown-item:hover {
|
||||
.nav-dropdown-item:hover,
|
||||
.nav-dropdown-item:active {
|
||||
transform: translateX(4px);
|
||||
background: rgba(99, 102, 241, 0.05);
|
||||
}
|
||||
}
|
||||
|
||||
/* ===== MOBILE NAVIGATION OVERLAY ===== */
|
||||
.nav-overlay {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background: rgba(0, 0, 0, .4);
|
||||
backdrop-filter: blur(2px);
|
||||
background: rgba(0, 0, 0, 0.4);
|
||||
z-index: 90;
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
transition: opacity .3s ease;
|
||||
pointer-events: none
|
||||
transition: opacity 0.3s ease, visibility 0.3s ease;
|
||||
pointer-events: none;
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
}
|
||||
|
||||
.nav-open #navOverlay {
|
||||
.nav-open .nav-overlay {
|
||||
opacity: 1;
|
||||
visibility: visible;
|
||||
pointer-events: auto
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
@media (max-width:900px) {
|
||||
/* ===== MOBILE NAVIGATION STYLES ===== */
|
||||
@media (max-width: 900px) {
|
||||
/* Show nav toggle button */
|
||||
.nav-toggle {
|
||||
display: inline-flex;
|
||||
width: 48px !important;
|
||||
height: 48px !important
|
||||
height: 48px !important;
|
||||
}
|
||||
|
||||
/* Hide nav by default on mobile */
|
||||
.header-nav {
|
||||
display: none
|
||||
}
|
||||
|
||||
.nav-open .header-nav {
|
||||
display: flex;
|
||||
display: none;
|
||||
position: fixed;
|
||||
top: var(--header-height);
|
||||
right: 0;
|
||||
width: min(340px, 85%);
|
||||
background: rgba(255, 255, 255, .98);
|
||||
backdrop-filter: blur(24px) saturate(180%);
|
||||
-webkit-backdrop-filter: blur(24px) saturate(180%);
|
||||
height: calc(100vh - var(--header-height));
|
||||
background: rgba(255, 255, 255, 0.98);
|
||||
border-left: 1px solid var(--border-primary);
|
||||
border-bottom: 1px solid var(--border-primary);
|
||||
box-shadow: 0 8px 32px 0 rgba(0, 0, 0, .08), 0 4px 16px 0 rgba(0, 0, 0, .04);
|
||||
box-shadow: -4px 0 20px rgba(0, 0, 0, 0.1);
|
||||
padding: var(--space-2xl) var(--space-xl) var(--space-3xl);
|
||||
flex-direction: column;
|
||||
gap: var(--space-sm);
|
||||
max-height: calc(100vh - var(--header-height));
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
overscroll-behavior: contain;
|
||||
transform: translate3d(100%, 0, 0);
|
||||
transition: transform .4s cubic-bezier(.22, 1, .36, 1);
|
||||
z-index: 120
|
||||
}
|
||||
|
||||
[data-theme="dark"] .nav-open .header-nav {
|
||||
background: rgba(15, 23, 42, .98);
|
||||
border-color: rgba(52, 65, 85, .6);
|
||||
box-shadow: 0 8px 32px 0 rgba(0, 0, 0, .2), 0 4px 16px 0 rgba(0, 0, 0, .1)
|
||||
-webkit-overflow-scrolling: touch;
|
||||
transform: translateX(100%);
|
||||
transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
z-index: 100;
|
||||
will-change: transform;
|
||||
}
|
||||
|
||||
/* Show nav when open */
|
||||
.nav-open .header-nav {
|
||||
transform: translate3d(0, 0, 0)
|
||||
display: flex;
|
||||
transform: translateX(0);
|
||||
}
|
||||
|
||||
/* Dark theme mobile nav */
|
||||
[data-theme="dark"] .header-nav {
|
||||
background: rgba(15, 23, 42, 0.98);
|
||||
border-color: rgba(52, 65, 85, 0.6);
|
||||
box-shadow: -4px 0 20px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
/* Prevent body scroll when nav is open */
|
||||
body.nav-open {
|
||||
overflow: hidden
|
||||
overflow: hidden;
|
||||
position: fixed;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* Mobile nav links */
|
||||
.nav-open .nav-link {
|
||||
font-size: var(--text-base);
|
||||
padding: var(--space-lg) var(--space-xl);
|
||||
|
|
@ -1092,16 +1109,19 @@ button:focus-visible {
|
|||
min-height: 44px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
touch-action: manipulation;
|
||||
}
|
||||
|
||||
.nav-open .nav-link::before {
|
||||
display: none
|
||||
display: none;
|
||||
}
|
||||
|
||||
.nav-open .nav-link::after {
|
||||
border-radius: var(--radius-lg)
|
||||
border-radius: var(--radius-lg);
|
||||
}
|
||||
|
||||
/* Dark mode toggle positioning on mobile */
|
||||
.dark-mode-toggle {
|
||||
display: flex;
|
||||
flex: 0 0 auto;
|
||||
|
|
@ -1109,18 +1129,20 @@ button:focus-visible {
|
|||
height: 40px !important;
|
||||
order: 3;
|
||||
align-self: center;
|
||||
z-index: 110
|
||||
z-index: 110;
|
||||
}
|
||||
|
||||
/* Nav toggle sizing */
|
||||
.nav-toggle {
|
||||
width: 40px !important;
|
||||
height: 40px !important
|
||||
height: 40px !important;
|
||||
}
|
||||
|
||||
/* Disable transitions before JS is ready */
|
||||
body:not(.js-ready) .header-nav,
|
||||
body:not(.js-ready) .nav-toggle-bar,
|
||||
body:not(.js-ready) .nav-overlay {
|
||||
transition: none !important
|
||||
transition: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
480
dist/index.html
vendored
480
dist/index.html
vendored
|
|
@ -1733,187 +1733,233 @@
|
|||
|
||||
<!-- JavaScript -->
|
||||
<script>
|
||||
// Dark mode toggle
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const btn = document.getElementById('darkModeToggle');
|
||||
const bodyEl = document.body;
|
||||
const navToggle = document.getElementById('navToggle');
|
||||
const nav = document.getElementById('primary-navigation');
|
||||
const navOverlay = document.getElementById('navOverlay');
|
||||
const focusableSelectors = 'a[href], button:not([disabled]), select, textarea, input, [tabindex]:not([tabindex="-1"])';
|
||||
let lastFocusedBeforeMenu = null;
|
||||
// Mobile Navigation Manager - Lightweight & Performance Optimized
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
// Navigation dropdowns
|
||||
const dropdownTriggers = document.querySelectorAll('.nav-dropdown-trigger');
|
||||
const state = {
|
||||
isOpen: false,
|
||||
isMobile: false,
|
||||
activeDropdown: null
|
||||
};
|
||||
|
||||
// Function to close all dropdowns
|
||||
function closeAllDropdowns() {
|
||||
dropdownTriggers.forEach(trigger => {
|
||||
const elements = {
|
||||
body: document.body,
|
||||
navToggle: document.getElementById('navToggle'),
|
||||
nav: document.getElementById('primary-navigation'),
|
||||
navOverlay: document.getElementById('navOverlay'),
|
||||
darkModeToggle: document.getElementById('darkModeToggle'),
|
||||
dropdownTriggers: []
|
||||
};
|
||||
|
||||
// Check if mobile
|
||||
const checkMobile = () => window.innerWidth <= 900;
|
||||
|
||||
// Update state
|
||||
const updateMobileState = () => {
|
||||
state.isMobile = checkMobile();
|
||||
};
|
||||
|
||||
// Close all dropdowns
|
||||
const closeAllDropdowns = () => {
|
||||
elements.dropdownTriggers.forEach(trigger => {
|
||||
trigger.setAttribute('aria-expanded', 'false');
|
||||
});
|
||||
}
|
||||
state.activeDropdown = null;
|
||||
};
|
||||
|
||||
dropdownTriggers.forEach(trigger => {
|
||||
trigger.addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
// On mobile, only allow dropdowns when nav menu is open
|
||||
if (window.innerWidth <= 900 && !bodyEl.classList.contains('nav-open')) {
|
||||
return;
|
||||
}
|
||||
|
||||
const isExpanded = trigger.getAttribute('aria-expanded') === 'true';
|
||||
|
||||
// Close all other dropdowns
|
||||
dropdownTriggers.forEach(otherTrigger => {
|
||||
if (otherTrigger !== trigger) {
|
||||
otherTrigger.setAttribute('aria-expanded', 'false');
|
||||
}
|
||||
});
|
||||
|
||||
// Toggle current dropdown
|
||||
trigger.setAttribute('aria-expanded', String(!isExpanded));
|
||||
}, { passive: false });
|
||||
});
|
||||
// Open mobile navigation
|
||||
const openNav = () => {
|
||||
if (!elements.nav || state.isOpen) return;
|
||||
|
||||
state.isOpen = true;
|
||||
elements.body.classList.add('nav-open');
|
||||
|
||||
if (elements.navToggle) {
|
||||
elements.navToggle.setAttribute('aria-expanded', 'true');
|
||||
}
|
||||
|
||||
if (elements.navOverlay) {
|
||||
elements.navOverlay.hidden = false;
|
||||
}
|
||||
|
||||
if (elements.nav) {
|
||||
elements.nav.setAttribute('aria-hidden', 'false');
|
||||
elements.nav.setAttribute('data-open', 'true');
|
||||
}
|
||||
};
|
||||
|
||||
// Close dropdowns when clicking outside (only on desktop)
|
||||
document.addEventListener('click', (e) => {
|
||||
// Don't close if clicking on a dropdown trigger or menu
|
||||
const isDropdownClick = e.target.closest('.nav-dropdown');
|
||||
if (!isDropdownClick && window.innerWidth > 900) {
|
||||
closeAllDropdowns();
|
||||
}
|
||||
}, { passive: true });
|
||||
|
||||
// Close dropdowns on escape key
|
||||
document.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Escape') {
|
||||
closeAllDropdowns();
|
||||
}
|
||||
}, { passive: true });
|
||||
|
||||
// Mark JS ready to enable transitions after initial paint (prevents slide-in glitch)
|
||||
requestAnimationFrame(() => bodyEl.classList.add('js-ready'));
|
||||
|
||||
function trapFocus(container) {
|
||||
if (!container || container.dataset.trapBound === 'true') return;
|
||||
const focusables = Array.from(container.querySelectorAll(focusableSelectors));
|
||||
if (!focusables.length) return;
|
||||
let first = focusables[0];
|
||||
let last = focusables[focusables.length - 1];
|
||||
function handle(e) {
|
||||
if (e.key === 'Tab') {
|
||||
if (e.shiftKey && document.activeElement === first) { e.preventDefault(); last.focus(); }
|
||||
else if (!e.shiftKey && document.activeElement === last) { e.preventDefault(); first.focus(); }
|
||||
}
|
||||
}
|
||||
container.addEventListener('keydown', handle);
|
||||
container.dataset.trapBound = 'true';
|
||||
}
|
||||
|
||||
function updateNavAria() {
|
||||
if (!nav) return;
|
||||
if (window.innerWidth <= 900) {
|
||||
nav.setAttribute('aria-hidden', bodyEl.classList.contains('nav-open') ? 'false' : 'true');
|
||||
} else {
|
||||
nav.removeAttribute('aria-hidden');
|
||||
// Restore scroll when switching to desktop
|
||||
document.body.style.overflow = '';
|
||||
if (bodyEl.classList.contains('nav-open')) {
|
||||
closeMenu({ restoreFocus: false });
|
||||
} else {
|
||||
nav.removeAttribute('data-open');
|
||||
navToggle?.setAttribute('aria-expanded', 'false');
|
||||
if (navOverlay) navOverlay.hidden = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function openMenu() {
|
||||
if (!nav) return;
|
||||
lastFocusedBeforeMenu = document.activeElement;
|
||||
bodyEl.classList.add('nav-open');
|
||||
navToggle?.setAttribute('aria-expanded', 'true');
|
||||
if (navOverlay) navOverlay.hidden = false;
|
||||
nav.setAttribute('data-open', 'true');
|
||||
// Close mobile navigation
|
||||
const closeNav = () => {
|
||||
if (!elements.nav || !state.isOpen) return;
|
||||
|
||||
// Prevent scroll on mobile when menu is open
|
||||
if (window.innerWidth <= 900) {
|
||||
document.body.style.overflow = 'hidden';
|
||||
state.isOpen = false;
|
||||
elements.body.classList.remove('nav-open');
|
||||
|
||||
if (elements.navToggle) {
|
||||
elements.navToggle.setAttribute('aria-expanded', 'false');
|
||||
}
|
||||
|
||||
const firstLink = nav.querySelector('a');
|
||||
firstLink && firstLink.focus();
|
||||
trapFocus(nav);
|
||||
updateNavAria();
|
||||
}
|
||||
function closeMenu(options = {}) {
|
||||
if (!nav) return;
|
||||
const { restoreFocus = true } = options;
|
||||
bodyEl.classList.remove('nav-open');
|
||||
navToggle?.setAttribute('aria-expanded', 'false');
|
||||
if (navOverlay) navOverlay.hidden = true;
|
||||
nav.removeAttribute('data-open');
|
||||
if (elements.navOverlay) {
|
||||
elements.navOverlay.hidden = true;
|
||||
}
|
||||
|
||||
// Restore scroll
|
||||
document.body.style.overflow = '';
|
||||
if (elements.nav) {
|
||||
elements.nav.setAttribute('aria-hidden', 'true');
|
||||
elements.nav.removeAttribute('data-open');
|
||||
}
|
||||
|
||||
// Close all dropdowns when closing menu
|
||||
// Close all dropdowns when closing nav
|
||||
closeAllDropdowns();
|
||||
|
||||
if (restoreFocus && lastFocusedBeforeMenu) {
|
||||
lastFocusedBeforeMenu.focus();
|
||||
};
|
||||
|
||||
// Toggle navigation
|
||||
const toggleNav = () => {
|
||||
if (state.isOpen) {
|
||||
closeNav();
|
||||
} else {
|
||||
openNav();
|
||||
}
|
||||
updateNavAria();
|
||||
}
|
||||
if (navToggle) {
|
||||
navToggle.addEventListener('click', () => {
|
||||
if (bodyEl.classList.contains('nav-open')) closeMenu(); else openMenu();
|
||||
};
|
||||
|
||||
// Handle dropdown toggle
|
||||
const toggleDropdown = (trigger) => {
|
||||
if (!trigger) return;
|
||||
|
||||
const isExpanded = trigger.getAttribute('aria-expanded') === 'true';
|
||||
|
||||
// Close all other dropdowns first
|
||||
elements.dropdownTriggers.forEach(t => {
|
||||
if (t !== trigger) {
|
||||
t.setAttribute('aria-expanded', 'false');
|
||||
}
|
||||
});
|
||||
}
|
||||
if (navOverlay) {
|
||||
navOverlay.addEventListener('click', closeMenu);
|
||||
}
|
||||
if (nav) {
|
||||
nav.addEventListener('click', (event) => {
|
||||
// Only close menu when clicking actual links, not dropdown triggers
|
||||
const target = event.target instanceof Element ? event.target.closest('a.nav-link, a.nav-dropdown-item') : null;
|
||||
if (!target) return;
|
||||
if (window.innerWidth <= 900) closeMenu();
|
||||
|
||||
// Toggle current dropdown
|
||||
trigger.setAttribute('aria-expanded', String(!isExpanded));
|
||||
state.activeDropdown = !isExpanded ? trigger : null;
|
||||
};
|
||||
|
||||
// Handle window resize
|
||||
const handleResize = () => {
|
||||
const wasMobile = state.isMobile;
|
||||
updateMobileState();
|
||||
|
||||
// If switched from mobile to desktop and nav was open, close it
|
||||
if (wasMobile && !state.isMobile && state.isOpen) {
|
||||
closeNav();
|
||||
}
|
||||
|
||||
// Update aria-hidden based on viewport
|
||||
if (elements.nav) {
|
||||
if (state.isMobile) {
|
||||
elements.nav.setAttribute('aria-hidden', state.isOpen ? 'false' : 'true');
|
||||
} else {
|
||||
elements.nav.removeAttribute('aria-hidden');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Initialize navigation
|
||||
const init = () => {
|
||||
updateMobileState();
|
||||
|
||||
// Mark JS ready for animations
|
||||
requestAnimationFrame(() => {
|
||||
elements.body.classList.add('js-ready');
|
||||
});
|
||||
}
|
||||
window.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Escape' && bodyEl.classList.contains('nav-open')) closeMenu();
|
||||
});
|
||||
|
||||
// Handle resize (debounced via rAF)
|
||||
let resizeRaf;
|
||||
window.addEventListener('resize', () => {
|
||||
if (resizeRaf) cancelAnimationFrame(resizeRaf);
|
||||
resizeRaf = requestAnimationFrame(updateNavAria);
|
||||
});
|
||||
updateNavAria();
|
||||
|
||||
// Scrollspy for active nav link
|
||||
const sectionIds = Array.from(nav?.querySelectorAll('a[href^="#"]') || []).map(a => a.getAttribute('href')).filter(Boolean);
|
||||
const sections = sectionIds.map(id => document.querySelector(id)).filter(Boolean);
|
||||
if ('IntersectionObserver' in window && sections.length) {
|
||||
const observer = new IntersectionObserver(entries => {
|
||||
entries.forEach(entry => {
|
||||
const id = '#' + entry.target.id;
|
||||
const link = nav.querySelector(`a[href="${id}"]`);
|
||||
if (link) {
|
||||
if (entry.isIntersecting) {
|
||||
nav.querySelectorAll('.nav-link.active').forEach(l => { l.classList.remove('active'); l.removeAttribute('aria-current'); });
|
||||
link.classList.add('active');
|
||||
link.setAttribute('aria-current', 'page');
|
||||
|
||||
// Initialize dropdowns
|
||||
elements.dropdownTriggers = Array.from(document.querySelectorAll('.nav-dropdown-trigger'));
|
||||
|
||||
// Set initial aria-hidden state
|
||||
if (elements.nav && state.isMobile) {
|
||||
elements.nav.setAttribute('aria-hidden', 'true');
|
||||
}
|
||||
|
||||
// Nav toggle click handler
|
||||
if (elements.navToggle) {
|
||||
elements.navToggle.addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
toggleNav();
|
||||
}, { passive: false });
|
||||
}
|
||||
|
||||
// Overlay click handler
|
||||
if (elements.navOverlay) {
|
||||
elements.navOverlay.addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
closeNav();
|
||||
}, { passive: false });
|
||||
}
|
||||
|
||||
// Dropdown click handlers
|
||||
elements.dropdownTriggers.forEach(trigger => {
|
||||
trigger.addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
toggleDropdown(trigger);
|
||||
}, { passive: false });
|
||||
});
|
||||
|
||||
// Link click handler - close nav on mobile when clicking links
|
||||
if (elements.nav) {
|
||||
const links = elements.nav.querySelectorAll('a.nav-link:not(.nav-dropdown-trigger), a.nav-dropdown-item');
|
||||
links.forEach(link => {
|
||||
link.addEventListener('click', () => {
|
||||
if (state.isMobile && state.isOpen) {
|
||||
// Small delay to allow navigation to start
|
||||
setTimeout(() => closeNav(), 100);
|
||||
}
|
||||
}
|
||||
}, { passive: true });
|
||||
});
|
||||
}, { rootMargin: '-40% 0px -50% 0px', threshold: [0, 0.25, 0.5, 1] });
|
||||
sections.forEach(sec => observer.observe(sec));
|
||||
}
|
||||
|
||||
// Escape key handler
|
||||
document.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Escape') {
|
||||
if (state.activeDropdown) {
|
||||
closeAllDropdowns();
|
||||
} else if (state.isOpen) {
|
||||
closeNav();
|
||||
}
|
||||
}
|
||||
}, { passive: true });
|
||||
|
||||
// Close dropdowns when clicking outside (desktop only)
|
||||
document.addEventListener('click', (e) => {
|
||||
if (!state.isMobile && state.activeDropdown) {
|
||||
const clickedDropdown = e.target.closest('.nav-dropdown');
|
||||
if (!clickedDropdown) {
|
||||
closeAllDropdowns();
|
||||
}
|
||||
}
|
||||
}, { passive: true });
|
||||
|
||||
// Resize handler with debounce
|
||||
let resizeTimeout;
|
||||
window.addEventListener('resize', () => {
|
||||
clearTimeout(resizeTimeout);
|
||||
resizeTimeout = setTimeout(handleResize, 150);
|
||||
}, { passive: true });
|
||||
};
|
||||
|
||||
// Initialize on DOM ready
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', init);
|
||||
} else {
|
||||
init();
|
||||
}
|
||||
if (btn) {
|
||||
})();
|
||||
|
||||
// Dark Mode Toggle - Separate Module
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
const init = () => {
|
||||
const btn = document.getElementById('darkModeToggle');
|
||||
if (!btn) return;
|
||||
|
||||
const getTheme = () => document.documentElement.getAttribute('data-theme') || 'light';
|
||||
const setTheme = (theme) => {
|
||||
document.documentElement.setAttribute('data-theme', theme);
|
||||
|
|
@ -1925,34 +1971,87 @@
|
|||
btn.setAttribute('aria-pressed', String(getTheme() === 'dark'));
|
||||
btn.addEventListener('click', () => {
|
||||
setTheme(getTheme() === 'dark' ? 'light' : 'dark');
|
||||
});
|
||||
}, { passive: true });
|
||||
};
|
||||
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', init);
|
||||
} else {
|
||||
init();
|
||||
}
|
||||
})();
|
||||
|
||||
// Smooth scroll for anchor links
|
||||
const prefersReduced = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
|
||||
document.querySelectorAll('a[href^="#"]').forEach(anchor => {
|
||||
anchor.addEventListener('click', (e) => {
|
||||
const id = anchor.getAttribute('href');
|
||||
const target = id && document.querySelector(id);
|
||||
if (!target) return;
|
||||
// Scrollspy & Smooth Scroll - Separate Module
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
const init = () => {
|
||||
const nav = document.getElementById('primary-navigation');
|
||||
if (!nav) return;
|
||||
|
||||
// Scrollspy for active nav link
|
||||
const sectionIds = Array.from(nav.querySelectorAll('a[href^="#"]') || [])
|
||||
.map(a => a.getAttribute('href'))
|
||||
.filter(Boolean);
|
||||
const sections = sectionIds.map(id => document.querySelector(id)).filter(Boolean);
|
||||
|
||||
if ('IntersectionObserver' in window && sections.length) {
|
||||
const observer = new IntersectionObserver(entries => {
|
||||
entries.forEach(entry => {
|
||||
const id = '#' + entry.target.id;
|
||||
const link = nav.querySelector(`a[href="${id}"]`);
|
||||
if (link && entry.isIntersecting) {
|
||||
nav.querySelectorAll('.nav-link.active').forEach(l => {
|
||||
l.classList.remove('active');
|
||||
l.removeAttribute('aria-current');
|
||||
});
|
||||
link.classList.add('active');
|
||||
link.setAttribute('aria-current', 'page');
|
||||
}
|
||||
});
|
||||
}, { rootMargin: '-40% 0px -50% 0px', threshold: [0, 0.25, 0.5, 1] });
|
||||
sections.forEach(sec => observer.observe(sec));
|
||||
}
|
||||
|
||||
e.preventDefault();
|
||||
if (bodyEl.classList.contains('nav-open')) closeMenu();
|
||||
target.scrollIntoView({
|
||||
behavior: prefersReduced ? 'auto' : 'smooth',
|
||||
block: 'start'
|
||||
});
|
||||
// Smooth scroll for anchor links
|
||||
const prefersReduced = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
|
||||
document.querySelectorAll('a[href^="#"]').forEach(anchor => {
|
||||
anchor.addEventListener('click', (e) => {
|
||||
const id = anchor.getAttribute('href');
|
||||
const target = id && document.querySelector(id);
|
||||
if (!target) return;
|
||||
|
||||
const focusEl = target.querySelector('h1,h2,h3,h4,h5,h6') || target;
|
||||
focusEl.setAttribute('tabindex', '-1');
|
||||
focusEl.focus({ preventScroll: true });
|
||||
setTimeout(() => focusEl.removeAttribute('tabindex'), 400);
|
||||
e.preventDefault();
|
||||
target.scrollIntoView({
|
||||
behavior: prefersReduced ? 'auto' : 'smooth',
|
||||
block: 'start'
|
||||
});
|
||||
|
||||
const focusEl = target.querySelector('h1,h2,h3,h4,h5,h6') || target;
|
||||
focusEl.setAttribute('tabindex', '-1');
|
||||
focusEl.focus({ preventScroll: true });
|
||||
setTimeout(() => focusEl.removeAttribute('tabindex'), 400);
|
||||
}, { passive: false });
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', init);
|
||||
} else {
|
||||
init();
|
||||
}
|
||||
})();
|
||||
|
||||
// Back to top button
|
||||
const backToTop = document.getElementById('backToTop');
|
||||
if (backToTop) {
|
||||
// Back to top button
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
const init = () => {
|
||||
const backToTop = document.getElementById('backToTop');
|
||||
if (!backToTop) return;
|
||||
|
||||
const prefersReduced = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
|
||||
|
||||
window.addEventListener('scroll', () => {
|
||||
if (window.pageYOffset > 300) {
|
||||
backToTop.classList.add('visible');
|
||||
|
|
@ -1966,9 +2065,18 @@
|
|||
top: 0,
|
||||
behavior: prefersReduced ? 'auto' : 'smooth'
|
||||
});
|
||||
});
|
||||
}, { passive: true });
|
||||
};
|
||||
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', init);
|
||||
} else {
|
||||
init();
|
||||
}
|
||||
})();
|
||||
|
||||
// Remaining initialization code
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
// Search functionality for file hosts
|
||||
const hostSearchInput = document.getElementById('host-search-input');
|
||||
const clearHostSearch = document.getElementById('clear-host-search');
|
||||
|
|
|
|||
Loading…
Reference in a new issue