/** * 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'; import BrandIcon from '@components/brand/BrandIcon'; 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; } // Patiun brand icons, resolved by the same names the nav config uses. 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) => ( ))} )}
); }