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);
|
font-weight: var(--font-medium);
|
||||||
color: var(--text-secondary);
|
color: var(--text-secondary);
|
||||||
min-height: 44px;
|
min-height: 44px;
|
||||||
|
-webkit-tap-highlight-color: transparent;
|
||||||
|
touch-action: manipulation;
|
||||||
}
|
}
|
||||||
|
|
||||||
.nav-dropdown-trigger:hover {
|
.nav-dropdown-trigger:hover {
|
||||||
|
|
@ -977,30 +979,34 @@ button:focus-visible {
|
||||||
visibility: visible;
|
visibility: visible;
|
||||||
border: none;
|
border: none;
|
||||||
box-shadow: none;
|
box-shadow: none;
|
||||||
padding: var(--space-xs) 0 var(--space-md) var(--space-2xl);
|
padding: 0;
|
||||||
margin-top: 0;
|
margin: 0;
|
||||||
background: rgba(99, 102, 241, 0.02);
|
background: transparent;
|
||||||
border-left: 2px solid var(--border-primary);
|
|
||||||
margin-left: var(--space-lg);
|
|
||||||
border-radius: 0;
|
border-radius: 0;
|
||||||
backdrop-filter: none;
|
backdrop-filter: none;
|
||||||
display: none;
|
display: none;
|
||||||
max-height: 0;
|
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
transition: max-height 0.3s ease;
|
/* Remove problematic animations */
|
||||||
/* Prevent layout shift and improve performance */
|
transition: none;
|
||||||
will-change: max-height;
|
will-change: auto;
|
||||||
contain: layout;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[data-theme="dark"] .nav-dropdown-menu {
|
[data-theme="dark"] .nav-dropdown-menu {
|
||||||
background: rgba(99, 102, 241, 0.03);
|
background: transparent;
|
||||||
border-left-color: var(--border-secondary);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.nav-dropdown-trigger[aria-expanded="true"] + .nav-dropdown-menu {
|
.nav-dropdown-trigger[aria-expanded="true"] + .nav-dropdown-menu {
|
||||||
display: block;
|
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 {
|
.nav-dropdown-item {
|
||||||
|
|
@ -1009,80 +1015,91 @@ button:focus-visible {
|
||||||
min-height: 44px;
|
min-height: 44px;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
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);
|
transform: translateX(4px);
|
||||||
background: rgba(99, 102, 241, 0.05);
|
background: rgba(99, 102, 241, 0.05);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ===== MOBILE NAVIGATION OVERLAY ===== */
|
||||||
.nav-overlay {
|
.nav-overlay {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
inset: 0;
|
inset: 0;
|
||||||
background: rgba(0, 0, 0, .4);
|
background: rgba(0, 0, 0, 0.4);
|
||||||
backdrop-filter: blur(2px);
|
|
||||||
z-index: 90;
|
z-index: 90;
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
visibility: hidden;
|
visibility: hidden;
|
||||||
transition: opacity .3s ease;
|
transition: opacity 0.3s ease, visibility 0.3s ease;
|
||||||
pointer-events: none
|
pointer-events: none;
|
||||||
|
-webkit-tap-highlight-color: transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
.nav-open #navOverlay {
|
.nav-open .nav-overlay {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
visibility: visible;
|
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 {
|
.nav-toggle {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
width: 48px !important;
|
width: 48px !important;
|
||||||
height: 48px !important
|
height: 48px !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Hide nav by default on mobile */
|
||||||
.header-nav {
|
.header-nav {
|
||||||
display: none
|
display: none;
|
||||||
}
|
|
||||||
|
|
||||||
.nav-open .header-nav {
|
|
||||||
display: flex;
|
|
||||||
position: fixed;
|
position: fixed;
|
||||||
top: var(--header-height);
|
top: var(--header-height);
|
||||||
right: 0;
|
right: 0;
|
||||||
width: min(340px, 85%);
|
width: min(340px, 85%);
|
||||||
background: rgba(255, 255, 255, .98);
|
height: calc(100vh - var(--header-height));
|
||||||
backdrop-filter: blur(24px) saturate(180%);
|
background: rgba(255, 255, 255, 0.98);
|
||||||
-webkit-backdrop-filter: blur(24px) saturate(180%);
|
|
||||||
border-left: 1px solid var(--border-primary);
|
border-left: 1px solid var(--border-primary);
|
||||||
border-bottom: 1px solid var(--border-primary);
|
box-shadow: -4px 0 20px rgba(0, 0, 0, 0.1);
|
||||||
box-shadow: 0 8px 32px 0 rgba(0, 0, 0, .08), 0 4px 16px 0 rgba(0, 0, 0, .04);
|
|
||||||
padding: var(--space-2xl) var(--space-xl) var(--space-3xl);
|
padding: var(--space-2xl) var(--space-xl) var(--space-3xl);
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: var(--space-sm);
|
gap: var(--space-sm);
|
||||||
max-height: calc(100vh - var(--header-height));
|
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
|
overflow-x: hidden;
|
||||||
overscroll-behavior: contain;
|
overscroll-behavior: contain;
|
||||||
transform: translate3d(100%, 0, 0);
|
-webkit-overflow-scrolling: touch;
|
||||||
transition: transform .4s cubic-bezier(.22, 1, .36, 1);
|
transform: translateX(100%);
|
||||||
z-index: 120
|
transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
}
|
z-index: 100;
|
||||||
|
will-change: transform;
|
||||||
[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)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Show nav when open */
|
||||||
.nav-open .header-nav {
|
.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 {
|
body.nav-open {
|
||||||
overflow: hidden
|
overflow: hidden;
|
||||||
|
position: fixed;
|
||||||
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Mobile nav links */
|
||||||
.nav-open .nav-link {
|
.nav-open .nav-link {
|
||||||
font-size: var(--text-base);
|
font-size: var(--text-base);
|
||||||
padding: var(--space-lg) var(--space-xl);
|
padding: var(--space-lg) var(--space-xl);
|
||||||
|
|
@ -1092,16 +1109,19 @@ button:focus-visible {
|
||||||
min-height: 44px;
|
min-height: 44px;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
-webkit-tap-highlight-color: transparent;
|
||||||
|
touch-action: manipulation;
|
||||||
}
|
}
|
||||||
|
|
||||||
.nav-open .nav-link::before {
|
.nav-open .nav-link::before {
|
||||||
display: none
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.nav-open .nav-link::after {
|
.nav-open .nav-link::after {
|
||||||
border-radius: var(--radius-lg)
|
border-radius: var(--radius-lg);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Dark mode toggle positioning on mobile */
|
||||||
.dark-mode-toggle {
|
.dark-mode-toggle {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex: 0 0 auto;
|
flex: 0 0 auto;
|
||||||
|
|
@ -1109,18 +1129,20 @@ button:focus-visible {
|
||||||
height: 40px !important;
|
height: 40px !important;
|
||||||
order: 3;
|
order: 3;
|
||||||
align-self: center;
|
align-self: center;
|
||||||
z-index: 110
|
z-index: 110;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Nav toggle sizing */
|
||||||
.nav-toggle {
|
.nav-toggle {
|
||||||
width: 40px !important;
|
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) .header-nav,
|
||||||
body:not(.js-ready) .nav-toggle-bar,
|
body:not(.js-ready) .nav-toggle-bar,
|
||||||
body:not(.js-ready) .nav-overlay {
|
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 -->
|
<!-- JavaScript -->
|
||||||
<script>
|
<script>
|
||||||
// Dark mode toggle
|
// Mobile Navigation Manager - Lightweight & Performance Optimized
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
(function() {
|
||||||
const btn = document.getElementById('darkModeToggle');
|
'use strict';
|
||||||
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;
|
|
||||||
|
|
||||||
// Navigation dropdowns
|
const state = {
|
||||||
const dropdownTriggers = document.querySelectorAll('.nav-dropdown-trigger');
|
isOpen: false,
|
||||||
|
isMobile: false,
|
||||||
|
activeDropdown: null
|
||||||
|
};
|
||||||
|
|
||||||
// Function to close all dropdowns
|
const elements = {
|
||||||
function closeAllDropdowns() {
|
body: document.body,
|
||||||
dropdownTriggers.forEach(trigger => {
|
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');
|
trigger.setAttribute('aria-expanded', 'false');
|
||||||
});
|
});
|
||||||
}
|
state.activeDropdown = null;
|
||||||
|
};
|
||||||
|
|
||||||
dropdownTriggers.forEach(trigger => {
|
// Open mobile navigation
|
||||||
trigger.addEventListener('click', (e) => {
|
const openNav = () => {
|
||||||
e.preventDefault();
|
if (!elements.nav || state.isOpen) return;
|
||||||
|
|
||||||
// On mobile, only allow dropdowns when nav menu is open
|
state.isOpen = true;
|
||||||
if (window.innerWidth <= 900 && !bodyEl.classList.contains('nav-open')) {
|
elements.body.classList.add('nav-open');
|
||||||
return;
|
|
||||||
}
|
if (elements.navToggle) {
|
||||||
|
elements.navToggle.setAttribute('aria-expanded', 'true');
|
||||||
const isExpanded = trigger.getAttribute('aria-expanded') === 'true';
|
}
|
||||||
|
|
||||||
// Close all other dropdowns
|
if (elements.navOverlay) {
|
||||||
dropdownTriggers.forEach(otherTrigger => {
|
elements.navOverlay.hidden = false;
|
||||||
if (otherTrigger !== trigger) {
|
}
|
||||||
otherTrigger.setAttribute('aria-expanded', 'false');
|
|
||||||
}
|
if (elements.nav) {
|
||||||
});
|
elements.nav.setAttribute('aria-hidden', 'false');
|
||||||
|
elements.nav.setAttribute('data-open', 'true');
|
||||||
// Toggle current dropdown
|
}
|
||||||
trigger.setAttribute('aria-expanded', String(!isExpanded));
|
};
|
||||||
}, { passive: false });
|
|
||||||
});
|
|
||||||
|
|
||||||
// Close dropdowns when clicking outside (only on desktop)
|
// Close mobile navigation
|
||||||
document.addEventListener('click', (e) => {
|
const closeNav = () => {
|
||||||
// Don't close if clicking on a dropdown trigger or menu
|
if (!elements.nav || !state.isOpen) return;
|
||||||
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');
|
|
||||||
|
|
||||||
// Prevent scroll on mobile when menu is open
|
state.isOpen = false;
|
||||||
if (window.innerWidth <= 900) {
|
elements.body.classList.remove('nav-open');
|
||||||
document.body.style.overflow = 'hidden';
|
|
||||||
|
if (elements.navToggle) {
|
||||||
|
elements.navToggle.setAttribute('aria-expanded', 'false');
|
||||||
}
|
}
|
||||||
|
|
||||||
const firstLink = nav.querySelector('a');
|
if (elements.navOverlay) {
|
||||||
firstLink && firstLink.focus();
|
elements.navOverlay.hidden = true;
|
||||||
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');
|
|
||||||
|
|
||||||
// Restore scroll
|
if (elements.nav) {
|
||||||
document.body.style.overflow = '';
|
elements.nav.setAttribute('aria-hidden', 'true');
|
||||||
|
elements.nav.removeAttribute('data-open');
|
||||||
|
}
|
||||||
|
|
||||||
// Close all dropdowns when closing menu
|
// Close all dropdowns when closing nav
|
||||||
closeAllDropdowns();
|
closeAllDropdowns();
|
||||||
|
};
|
||||||
if (restoreFocus && lastFocusedBeforeMenu) {
|
|
||||||
lastFocusedBeforeMenu.focus();
|
// Toggle navigation
|
||||||
|
const toggleNav = () => {
|
||||||
|
if (state.isOpen) {
|
||||||
|
closeNav();
|
||||||
|
} else {
|
||||||
|
openNav();
|
||||||
}
|
}
|
||||||
updateNavAria();
|
};
|
||||||
}
|
|
||||||
if (navToggle) {
|
// Handle dropdown toggle
|
||||||
navToggle.addEventListener('click', () => {
|
const toggleDropdown = (trigger) => {
|
||||||
if (bodyEl.classList.contains('nav-open')) closeMenu(); else openMenu();
|
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) {
|
// Toggle current dropdown
|
||||||
navOverlay.addEventListener('click', closeMenu);
|
trigger.setAttribute('aria-expanded', String(!isExpanded));
|
||||||
}
|
state.activeDropdown = !isExpanded ? trigger : null;
|
||||||
if (nav) {
|
};
|
||||||
nav.addEventListener('click', (event) => {
|
|
||||||
// Only close menu when clicking actual links, not dropdown triggers
|
// Handle window resize
|
||||||
const target = event.target instanceof Element ? event.target.closest('a.nav-link, a.nav-dropdown-item') : null;
|
const handleResize = () => {
|
||||||
if (!target) return;
|
const wasMobile = state.isMobile;
|
||||||
if (window.innerWidth <= 900) closeMenu();
|
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) => {
|
// Initialize dropdowns
|
||||||
if (e.key === 'Escape' && bodyEl.classList.contains('nav-open')) closeMenu();
|
elements.dropdownTriggers = Array.from(document.querySelectorAll('.nav-dropdown-trigger'));
|
||||||
});
|
|
||||||
|
// Set initial aria-hidden state
|
||||||
// Handle resize (debounced via rAF)
|
if (elements.nav && state.isMobile) {
|
||||||
let resizeRaf;
|
elements.nav.setAttribute('aria-hidden', 'true');
|
||||||
window.addEventListener('resize', () => {
|
}
|
||||||
if (resizeRaf) cancelAnimationFrame(resizeRaf);
|
|
||||||
resizeRaf = requestAnimationFrame(updateNavAria);
|
// Nav toggle click handler
|
||||||
});
|
if (elements.navToggle) {
|
||||||
updateNavAria();
|
elements.navToggle.addEventListener('click', (e) => {
|
||||||
|
e.preventDefault();
|
||||||
// Scrollspy for active nav link
|
toggleNav();
|
||||||
const sectionIds = Array.from(nav?.querySelectorAll('a[href^="#"]') || []).map(a => a.getAttribute('href')).filter(Boolean);
|
}, { passive: false });
|
||||||
const sections = sectionIds.map(id => document.querySelector(id)).filter(Boolean);
|
}
|
||||||
if ('IntersectionObserver' in window && sections.length) {
|
|
||||||
const observer = new IntersectionObserver(entries => {
|
// Overlay click handler
|
||||||
entries.forEach(entry => {
|
if (elements.navOverlay) {
|
||||||
const id = '#' + entry.target.id;
|
elements.navOverlay.addEventListener('click', (e) => {
|
||||||
const link = nav.querySelector(`a[href="${id}"]`);
|
e.preventDefault();
|
||||||
if (link) {
|
closeNav();
|
||||||
if (entry.isIntersecting) {
|
}, { passive: false });
|
||||||
nav.querySelectorAll('.nav-link.active').forEach(l => { l.classList.remove('active'); l.removeAttribute('aria-current'); });
|
}
|
||||||
link.classList.add('active');
|
|
||||||
link.setAttribute('aria-current', 'page');
|
// 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 getTheme = () => document.documentElement.getAttribute('data-theme') || 'light';
|
||||||
const setTheme = (theme) => {
|
const setTheme = (theme) => {
|
||||||
document.documentElement.setAttribute('data-theme', theme);
|
document.documentElement.setAttribute('data-theme', theme);
|
||||||
|
|
@ -1925,34 +1971,87 @@
|
||||||
btn.setAttribute('aria-pressed', String(getTheme() === 'dark'));
|
btn.setAttribute('aria-pressed', String(getTheme() === 'dark'));
|
||||||
btn.addEventListener('click', () => {
|
btn.addEventListener('click', () => {
|
||||||
setTheme(getTheme() === 'dark' ? 'light' : 'dark');
|
setTheme(getTheme() === 'dark' ? 'light' : 'dark');
|
||||||
});
|
}, { passive: true });
|
||||||
|
};
|
||||||
|
|
||||||
|
if (document.readyState === 'loading') {
|
||||||
|
document.addEventListener('DOMContentLoaded', init);
|
||||||
|
} else {
|
||||||
|
init();
|
||||||
}
|
}
|
||||||
|
})();
|
||||||
|
|
||||||
// Smooth scroll for anchor links
|
// Scrollspy & Smooth Scroll - Separate Module
|
||||||
const prefersReduced = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
|
(function() {
|
||||||
document.querySelectorAll('a[href^="#"]').forEach(anchor => {
|
'use strict';
|
||||||
anchor.addEventListener('click', (e) => {
|
|
||||||
const id = anchor.getAttribute('href');
|
const init = () => {
|
||||||
const target = id && document.querySelector(id);
|
const nav = document.getElementById('primary-navigation');
|
||||||
if (!target) return;
|
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();
|
// Smooth scroll for anchor links
|
||||||
if (bodyEl.classList.contains('nav-open')) closeMenu();
|
const prefersReduced = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
|
||||||
target.scrollIntoView({
|
document.querySelectorAll('a[href^="#"]').forEach(anchor => {
|
||||||
behavior: prefersReduced ? 'auto' : 'smooth',
|
anchor.addEventListener('click', (e) => {
|
||||||
block: 'start'
|
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;
|
e.preventDefault();
|
||||||
focusEl.setAttribute('tabindex', '-1');
|
target.scrollIntoView({
|
||||||
focusEl.focus({ preventScroll: true });
|
behavior: prefersReduced ? 'auto' : 'smooth',
|
||||||
setTimeout(() => focusEl.removeAttribute('tabindex'), 400);
|
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
|
// Back to top button
|
||||||
const backToTop = document.getElementById('backToTop');
|
(function() {
|
||||||
if (backToTop) {
|
'use strict';
|
||||||
|
|
||||||
|
const init = () => {
|
||||||
|
const backToTop = document.getElementById('backToTop');
|
||||||
|
if (!backToTop) return;
|
||||||
|
|
||||||
|
const prefersReduced = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
|
||||||
|
|
||||||
window.addEventListener('scroll', () => {
|
window.addEventListener('scroll', () => {
|
||||||
if (window.pageYOffset > 300) {
|
if (window.pageYOffset > 300) {
|
||||||
backToTop.classList.add('visible');
|
backToTop.classList.add('visible');
|
||||||
|
|
@ -1966,9 +2065,18 @@
|
||||||
top: 0,
|
top: 0,
|
||||||
behavior: prefersReduced ? 'auto' : 'smooth'
|
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
|
// Search functionality for file hosts
|
||||||
const hostSearchInput = document.getElementById('host-search-input');
|
const hostSearchInput = document.getElementById('host-search-input');
|
||||||
const clearHostSearch = document.getElementById('clear-host-search');
|
const clearHostSearch = document.getElementById('clear-host-search');
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue