diff --git a/frontend/src/components/BottomSheet.tsx b/frontend/src/components/BottomSheet.tsx new file mode 100644 index 0000000..37de293 --- /dev/null +++ b/frontend/src/components/BottomSheet.tsx @@ -0,0 +1,88 @@ +import { ChevronUp, X } from "lucide-react"; +import { useEffect, useState, type ReactNode } from "react"; + +import { useIsMobile } from "@/lib/useMediaQuery"; + +/** + * DESIGN.md responsive breakpoints: below 768px metadata panels collapse into + * a sliding bottom drawer. Render the panel content inline on wider screens + * and pass it here instead on mobile. + */ +export function BottomSheet({ + title, + children, +}: { + title: string; + children: ReactNode; +}) { + const [open, setOpen] = useState(false); + + useEffect(() => { + if (!open) return; + const previous = document.body.style.overflow; + document.body.style.overflow = "hidden"; + function handleKeyDown(event: KeyboardEvent) { + if (event.key === "Escape") setOpen(false); + } + window.addEventListener("keydown", handleKeyDown); + return () => { + document.body.style.overflow = previous; + window.removeEventListener("keydown", handleKeyDown); + }; + }, [open]); + + return ( + <> + + + {open ? ( +
+
setOpen(false)} + aria-hidden="true" + /> +
+
+ + {title} + + +
+
{children}
+
+
+ ) : null} + + ); +} + +/** + * Desktop side panel, mobile bottom sheet. Use in place of an +
diff --git a/frontend/src/lib/useMediaQuery.ts b/frontend/src/lib/useMediaQuery.ts new file mode 100644 index 0000000..1b1561b --- /dev/null +++ b/frontend/src/lib/useMediaQuery.ts @@ -0,0 +1,21 @@ +import { useEffect, useState } from "react"; + +/** Track a CSS media query, e.g. `useMediaQuery("(min-width: 768px)")`. */ +export function useMediaQuery(query: string): boolean { + const [matches, setMatches] = useState(() => window.matchMedia(query).matches); + + useEffect(() => { + const list = window.matchMedia(query); + const update = () => setMatches(list.matches); + update(); + list.addEventListener("change", update); + return () => list.removeEventListener("change", update); + }, [query]); + + return matches; +} + +/** The design system's mobile breakpoint: metadata panels become bottom sheets. */ +export function useIsMobile(): boolean { + return !useMediaQuery("(min-width: 768px)"); +}