refactor: simplify menu and toast interactions with CSS-driven animations
- Moved menu hover logic from JavaScript to CSS selectors, reducing JS to minimal bridge code - Replaced JavaScript-based toast timing with pure CSS animation lifecycle (slide in → stay → fade out) - Removed unnecessary event handlers and legacy compatibility code for cleaner implementation
This commit is contained in:
+29
-14
@@ -1579,24 +1579,38 @@ a:focus {
|
||||
border-radius: 8px;
|
||||
border-left: 4px solid #dc2626;
|
||||
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
|
||||
display: flex;
|
||||
display: none; /* Hidden by default */
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
max-width: 400px;
|
||||
z-index: 1000;
|
||||
animation: slideIn 0.3s ease-out;
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
|
||||
@keyframes slideIn {
|
||||
from {
|
||||
/* Show toast with full animation cycle: slide in -> stay -> fade out */
|
||||
.error-toast.show {
|
||||
display: flex;
|
||||
animation: toastLifecycle 5.5s ease-out forwards;
|
||||
}
|
||||
|
||||
/* Full lifecycle animation: slide in (0-0.3s), stay (0.3-5s), fade out (5-5.5s) */
|
||||
@keyframes toastLifecycle {
|
||||
0% {
|
||||
transform: translateX(120%);
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
5.5% { /* 0.3s / 5.5s = 5.5% */
|
||||
transform: translateX(0);
|
||||
opacity: 1;
|
||||
}
|
||||
90.9% { /* 5s / 5.5s = 90.9% - stay visible */
|
||||
transform: translateX(0);
|
||||
opacity: 1;
|
||||
}
|
||||
100% { /* Final 0.5s - fade out and slide right */
|
||||
transform: translateX(120%);
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.error-icon {
|
||||
@@ -2233,8 +2247,10 @@ text-align: center;
|
||||
transition: background-color 0.2s ease;
|
||||
border-radius: 4px;
|
||||
margin: 0 0.5rem;
|
||||
position: relative; /* For CSS-only hover trigger */
|
||||
}
|
||||
|
||||
|
||||
.hamburger-btn:hover {
|
||||
background-color: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
@@ -2259,21 +2275,20 @@ text-align: center;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
/* Show menu when hovering menu itself OR when it has the hover class */
|
||||
/* Pure CSS Menu Activation - Show menu when hovering hamburger OR menu */
|
||||
/* Show when hovering the hamburger button (adjacent in DOM after site-title-left) */
|
||||
.hamburger-btn:hover ~ .navigation-menu,
|
||||
.hamburger-btn:focus ~ .navigation-menu,
|
||||
/* Show when hovering the menu itself */
|
||||
.navigation-menu:hover,
|
||||
.navigation-menu.menu-hover {
|
||||
/* Legacy class for backward compatibility */
|
||||
.navigation-menu.menu-hover,
|
||||
.navigation-menu.menu-open {
|
||||
max-height: calc(100vh - 60px); /* Viewport height minus header + some spacing */
|
||||
pointer-events: auto; /* Enable pointer events when visible */
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
/* Legacy class for JS compatibility - keep for backward compatibility */
|
||||
.navigation-menu.menu-open {
|
||||
max-height: calc(100vh - 60px);
|
||||
pointer-events: auto;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.menu-content {
|
||||
padding: 1rem 0;
|
||||
}
|
||||
|
||||
+27
-92
@@ -7,120 +7,58 @@
|
||||
// NAVIGATION & MENU SYSTEM
|
||||
// =============================================================================
|
||||
|
||||
// Hover-based menu control
|
||||
// Minimal menu control - CSS handles most logic, JS just bridges hamburger to menu
|
||||
function initMenuSystem() {
|
||||
const hamburgerBtn = document.querySelector('.hamburger-btn');
|
||||
const menu = document.getElementById('navigation-menu');
|
||||
|
||||
if (!hamburgerBtn || !menu) return;
|
||||
|
||||
// Show menu on hamburger hover
|
||||
hamburgerBtn.addEventListener('mouseenter', function() {
|
||||
menu.classList.add('menu-hover');
|
||||
hamburgerBtn.setAttribute('aria-expanded', 'true');
|
||||
});
|
||||
// Show menu on hamburger hover - CSS handles the rest
|
||||
hamburgerBtn.addEventListener('mouseenter', () => menu.classList.add('menu-hover'));
|
||||
|
||||
// Hide menu when leaving hamburger (only if not hovering menu)
|
||||
hamburgerBtn.addEventListener('mouseleave', function() {
|
||||
// Hide menu when leaving hamburger if not hovering menu
|
||||
hamburgerBtn.addEventListener('mouseleave', () => {
|
||||
setTimeout(() => {
|
||||
if (!menu.matches(':hover')) {
|
||||
menu.classList.remove('menu-hover');
|
||||
hamburgerBtn.setAttribute('aria-expanded', 'false');
|
||||
}
|
||||
if (!menu.matches(':hover')) menu.classList.remove('menu-hover');
|
||||
}, 100);
|
||||
});
|
||||
|
||||
// Hide menu when leaving menu itself
|
||||
menu.addEventListener('mouseleave', function() {
|
||||
menu.classList.remove('menu-hover');
|
||||
hamburgerBtn.setAttribute('aria-expanded', 'false');
|
||||
});
|
||||
// Hide menu when leaving menu
|
||||
menu.addEventListener('mouseleave', () => menu.classList.remove('menu-hover'));
|
||||
|
||||
// Position submenu dynamically
|
||||
// Position submenu dynamically (needed because fixed positioning)
|
||||
const submenuTrigger = document.querySelector('.menu-item-submenu');
|
||||
const submenuContent = document.querySelector('.submenu-content');
|
||||
|
||||
if (submenuTrigger && submenuContent) {
|
||||
submenuTrigger.addEventListener('mouseenter', function() {
|
||||
const triggerRect = submenuTrigger.getBoundingClientRect();
|
||||
submenuContent.style.top = `${triggerRect.top}px`;
|
||||
submenuContent.style.top = `${this.getBoundingClientRect().top}px`;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Legacy toggle function - kept for compatibility
|
||||
window.toggleMenu = function() {
|
||||
const menu = document.getElementById('navigation-menu');
|
||||
const btn = document.querySelector('.hamburger-btn');
|
||||
|
||||
if (menu.classList.contains('menu-open')) {
|
||||
menu.classList.remove('menu-open');
|
||||
btn.setAttribute('aria-expanded', 'false');
|
||||
} else {
|
||||
menu.classList.add('menu-open');
|
||||
btn.setAttribute('aria-expanded', 'true');
|
||||
}
|
||||
};
|
||||
|
||||
// Flag to keep header visible after navigation
|
||||
let keepHeaderVisible = false;
|
||||
|
||||
// Expand all sections
|
||||
// Expand/collapse all sections utility functions
|
||||
window.expandAllSections = function(event) {
|
||||
event.preventDefault();
|
||||
const allDetails = document.querySelectorAll('details');
|
||||
allDetails.forEach(detail => {
|
||||
detail.setAttribute('open', '');
|
||||
});
|
||||
document.querySelectorAll('details').forEach(d => d.setAttribute('open', ''));
|
||||
};
|
||||
|
||||
// Collapse all sections
|
||||
window.collapseAllSections = function(event) {
|
||||
event.preventDefault();
|
||||
const allDetails = document.querySelectorAll('details');
|
||||
allDetails.forEach(detail => {
|
||||
detail.removeAttribute('open');
|
||||
});
|
||||
document.querySelectorAll('details').forEach(d => d.removeAttribute('open'));
|
||||
};
|
||||
|
||||
// Toggle submenu - no longer needed for hover, but kept for compatibility
|
||||
window.toggleSubmenu = function(event) {
|
||||
event.preventDefault();
|
||||
const submenuContainer = event.currentTarget.parentElement;
|
||||
submenuContainer.classList.toggle('submenu-open');
|
||||
};
|
||||
|
||||
// Close menu when navigation links are clicked (native smooth scroll handles scrolling)
|
||||
// CSS scroll-padding-top handles the header offset automatically
|
||||
document.addEventListener('click', function(e) {
|
||||
// Check if clicked element is a navigation link inside the menu
|
||||
// Close menu when navigation links clicked - CSS handles scrolling
|
||||
document.addEventListener('click', (e) => {
|
||||
const navLink = e.target.closest('.submenu-content a[href^="#"]');
|
||||
if (navLink) {
|
||||
const navMenu = document.querySelector('.navigation-menu');
|
||||
const hamburgerBtn = document.querySelector('.hamburger-btn');
|
||||
|
||||
if (navMenu && hamburgerBtn) {
|
||||
navMenu.classList.remove('menu-open');
|
||||
hamburgerBtn.setAttribute('aria-expanded', 'false');
|
||||
}
|
||||
document.querySelector('.navigation-menu')?.classList.remove('menu-hover', 'menu-open');
|
||||
}
|
||||
});
|
||||
|
||||
// Close menu when clicking outside (only for legacy click-opened menus)
|
||||
function initClickOutsideHandler() {
|
||||
document.addEventListener('click', function(event) {
|
||||
const menu = document.getElementById('navigation-menu');
|
||||
const btn = document.querySelector('.hamburger-btn');
|
||||
|
||||
if (menu && btn && menu.classList.contains('menu-open')) {
|
||||
if (!menu.contains(event.target) && !btn.contains(event.target)) {
|
||||
menu.classList.remove('menu-open');
|
||||
btn.setAttribute('aria-expanded', 'false');
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// LANGUAGE & PREFERENCES
|
||||
// =============================================================================
|
||||
@@ -657,26 +595,24 @@
|
||||
// ERROR HANDLING
|
||||
// =============================================================================
|
||||
|
||||
// Error handling utility
|
||||
// Error handling utility - CSS handles auto-hide animation
|
||||
window.showError = function(message) {
|
||||
const errorToast = document.getElementById('error-toast');
|
||||
const errorMessage = document.getElementById('error-message');
|
||||
errorMessage.textContent = message;
|
||||
errorToast.style.display = 'flex';
|
||||
|
||||
// Auto-hide after 5 seconds
|
||||
setTimeout(() => {
|
||||
errorToast.style.display = 'none';
|
||||
}, 5000);
|
||||
errorMessage.textContent = message;
|
||||
errorToast.classList.remove('show'); // Reset if already showing
|
||||
|
||||
// Trigger reflow to restart animation
|
||||
void errorToast.offsetWidth;
|
||||
|
||||
errorToast.classList.add('show'); // CSS animation handles lifecycle
|
||||
};
|
||||
|
||||
// Add close button handler for error toast
|
||||
document.addEventListener('click', function(e) {
|
||||
// Close button handler for error toast - removes class to trigger hide
|
||||
document.addEventListener('click', (e) => {
|
||||
if (e.target.closest('.error-close')) {
|
||||
const errorToast = document.getElementById('error-toast');
|
||||
if (errorToast) {
|
||||
errorToast.style.display = 'none';
|
||||
}
|
||||
document.getElementById('error-toast')?.classList.remove('show');
|
||||
}
|
||||
});
|
||||
|
||||
@@ -744,7 +680,6 @@
|
||||
// Initialize everything when DOM is ready
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
initMenuSystem();
|
||||
initClickOutsideHandler();
|
||||
initPreferences();
|
||||
initScrollBehavior();
|
||||
initHTMXHandlers();
|
||||
|
||||
Reference in New Issue
Block a user