'use client'; import { useEffect, useRef, type ReactNode } from 'react'; interface SidebarProps { id: string; title: string; open: boolean; onClose: () => void; footer?: ReactNode; children: ReactNode; } // Matches Tailwind's `lg` breakpoint, where the sidebar becomes a persistent column const DESKTOP_QUERY = '(min-width: 1024px)'; // Responsive sidebar: a modal drawer below `lg`, a sticky column at `lg` and up. // While the drawer is open the page behind it can't scroll, and scrolling inside // it never chains through to the page. export default function Sidebar({ id, title, open, onClose, footer, children }: SidebarProps) { const panelRef = useRef(null); // Lock page scroll, close on Escape, and move focus into the drawer while open useEffect(() => { if (!open) return; const root = document.documentElement; const previousOverflow = root.style.overflow; root.style.overflow = 'hidden'; const previousFocus = document.activeElement as HTMLElement | null; panelRef.current?.focus({ preventScroll: true }); const handleKeyDown = (e: KeyboardEvent) => { if (e.key === 'Escape') onClose(); }; document.addEventListener('keydown', handleKeyDown); return () => { root.style.overflow = previousOverflow; document.removeEventListener('keydown', handleKeyDown); previousFocus?.focus({ preventScroll: true }); }; }, [open, onClose]); // The drawer only exists below `lg`; close it if the viewport grows past that useEffect(() => { if (!open) return; const query = window.matchMedia(DESKTOP_QUERY); const handleChange = () => { if (query.matches) onClose(); }; handleChange(); query.addEventListener('change', handleChange); return () => query.removeEventListener('change', handleChange); }, [open, onClose]); return ( <> {open && (