/** * MobileMenu.tsx * --------------------------------------------------------------------------- * React island that powers the hamburger menu on small screens. Renders the * same two-group nav structure as the desktop bar (imported via a data prop so * the source of truth stays in site.ts). * * Accessibility: the button is aria-expanded/aria-controls wired to the panel, * Escape closes, clicking a link closes, and focus is returned to the toggle. * Animations respect prefers-reduced-motion (Framer Motion reads the media query * automatically when we pass the transition). */ import { useEffect, useRef, useState } from 'react'; import { AnimatePresence, motion } from 'framer-motion'; export interface MobileNavItem { label: string; href: string; icon: string; } export interface MobileNavGroup { items: MobileNavItem[]; } interface Props { groups: MobileNavGroup[]; /** Current path for active link highlighting. */ currentPath: string; } // Small inline mdi-like icons keyed by name (keeps the menu self-contained). const ICON_PATHS: Record = { home: 'M10 20v-6h4v6h5v-8h3L12 3 2 12h3v8z', account: 'M12 4a4 4 0 1 0 0 8 4 4 0 0 0 0-8zm0 10c-4.4 0-8 2.2-8 5v1h16v-1c0-2.8-3.6-5-8-5z', briefcase: 'M10 16h4a1 1 0 0 0 1-1v-1H7a2 2 0 0 1-2-2V7a2 2 0 0 1 2-2h2V4a2 2 0 0 1 2-2h2a2 2 0 0 1 2 2v1h2a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2h-1v1a3 3 0 0 1-3 3h-4a3 3 0 0 1-3-3v-1z', 'dice-multiple': 'M6 3h5v5H6V3zm7 0h5v5h-5V3zM3 11h5v5H3v-5zm7 0h5v5h-5v-5zm7 0h-5v5h5v-5z', email: 'M4 4h16a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2zm8 7L4 6v1l8 5 8-5V6l-8 5z', }; function Icon({ name, className }: { name: string; className?: string }) { return ( ); } function isActive(href: string, path: string): boolean { if (href === '/') return path === '/'; return path === href || path.startsWith(href + '/'); } export default function MobileMenu({ groups, currentPath }: Props) { const [open, setOpen] = useState(false); const buttonRef = useRef(null); // Escape closes the menu; lock body scroll while open. useEffect(() => { if (!open) return; const onKey = (e: KeyboardEvent) => { if (e.key === 'Escape') { setOpen(false); buttonRef.current?.focus(); } }; document.addEventListener('keydown', onKey); document.body.style.overflow = 'hidden'; return () => { document.removeEventListener('keydown', onKey); document.body.style.overflow = ''; }; }, [open]); return (
{open && ( <> {/* Backdrop */} setOpen(false)} className="fixed inset-0 z-40 bg-black/40 backdrop-blur-sm" /> {/* Panel */} {groups.map((group, gi) => ( ))} )}
); }