hopefully fixed mobile nav

This commit is contained in:
fynks 2025-10-12 17:42:44 +05:00
parent 92bb628a2b
commit 4cf024c44b
2 changed files with 365 additions and 235 deletions

120
dist/css/styles.css vendored
View file

@ -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
View file

@ -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;
}
const isExpanded = trigger.getAttribute('aria-expanded') === 'true'; if (elements.navToggle) {
elements.navToggle.setAttribute('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 });
});
// 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');
// Prevent scroll on mobile when menu is open
if (window.innerWidth <= 900) {
document.body.style.overflow = 'hidden';
} }
const firstLink = nav.querySelector('a'); if (elements.navOverlay) {
firstLink && firstLink.focus(); elements.navOverlay.hidden = false;
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', 'false');
elements.nav.setAttribute('data-open', 'true');
}
};
// Close all dropdowns when closing menu // Close mobile navigation
const closeNav = () => {
if (!elements.nav || !state.isOpen) return;
state.isOpen = false;
elements.body.classList.remove('nav-open');
if (elements.navToggle) {
elements.navToggle.setAttribute('aria-expanded', 'false');
}
if (elements.navOverlay) {
elements.navOverlay.hidden = true;
}
if (elements.nav) {
elements.nav.setAttribute('aria-hidden', 'true');
elements.nav.removeAttribute('data-open');
}
// Close all dropdowns when closing nav
closeAllDropdowns(); closeAllDropdowns();
};
if (restoreFocus && lastFocusedBeforeMenu) { // Toggle navigation
lastFocusedBeforeMenu.focus(); const toggleNav = () => {
if (state.isOpen) {
closeNav();
} else {
openNav();
} }
updateNavAria(); };
}
if (navToggle) {
navToggle.addEventListener('click', () => {
if (bodyEl.classList.contains('nav-open')) closeMenu(); else openMenu();
});
}
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();
});
}
window.addEventListener('keydown', (e) => {
if (e.key === 'Escape' && bodyEl.classList.contains('nav-open')) closeMenu();
});
// Handle resize (debounced via rAF) // Handle dropdown toggle
let resizeRaf; const toggleDropdown = (trigger) => {
window.addEventListener('resize', () => { if (!trigger) return;
if (resizeRaf) cancelAnimationFrame(resizeRaf);
resizeRaf = requestAnimationFrame(updateNavAria);
});
updateNavAria();
// Scrollspy for active nav link const isExpanded = trigger.getAttribute('aria-expanded') === 'true';
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); // Close all other dropdowns first
if ('IntersectionObserver' in window && sections.length) { elements.dropdownTriggers.forEach(t => {
const observer = new IntersectionObserver(entries => { if (t !== trigger) {
entries.forEach(entry => { t.setAttribute('aria-expanded', 'false');
const id = '#' + entry.target.id; }
const link = nav.querySelector(`a[href="${id}"]`); });
if (link) {
if (entry.isIntersecting) { // Toggle current dropdown
nav.querySelectorAll('.nav-link.active').forEach(l => { l.classList.remove('active'); l.removeAttribute('aria-current'); }); trigger.setAttribute('aria-expanded', String(!isExpanded));
link.classList.add('active'); state.activeDropdown = !isExpanded ? trigger : null;
link.setAttribute('aria-current', 'page'); };
// 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');
});
// 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 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 target = id && document.querySelector(id);
if (!target) return;
e.preventDefault(); const init = () => {
if (bodyEl.classList.contains('nav-open')) closeMenu(); const nav = document.getElementById('primary-navigation');
target.scrollIntoView({ if (!nav) return;
behavior: prefersReduced ? 'auto' : 'smooth',
block: 'start'
});
const focusEl = target.querySelector('h1,h2,h3,h4,h5,h6') || target; // Scrollspy for active nav link
focusEl.setAttribute('tabindex', '-1'); const sectionIds = Array.from(nav.querySelectorAll('a[href^="#"]') || [])
focusEl.focus({ preventScroll: true }); .map(a => a.getAttribute('href'))
setTimeout(() => focusEl.removeAttribute('tabindex'), 400); .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));
}
// 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;
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
(function() {
'use strict';
const init = () => {
const backToTop = document.getElementById('backToTop');
if (!backToTop) return;
const prefersReduced = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
// Back to top button
const backToTop = document.getElementById('backToTop');
if (backToTop) {
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');