// React port of ../../drawer.ts (#35): the mobile off-canvas sidebar. On // desktop the `drawer-open` class is inert — CSS gates the drawer styling to // narrow viewports (styles.css, `.app-shell`). // // Anything that puts a new view in the content area must close the drawer, or // it renders behind the sidebar and the tap reads as a dead button — this // listens for the same TOGGLE_DRAWER_EVENT/OPEN_SPAWN_EVENT/SPAWN_SIMILAR_EVENT // the rest of the shell already raises, so it needs no wiring from callers. import type { ReactNode } from "react"; import { useEffect, useRef, useState } from "react"; import { OPEN_SPAWN_EVENT, SPAWN_SIMILAR_EVENT, TOGGLE_DRAWER_EVENT } from "../../uiEvents.js"; export interface DrawerProps { sidebar: ReactNode; children: ReactNode; className?: string; } export function Drawer({ sidebar, children, className }: DrawerProps) { const [open, setOpen] = useState(false); const sidebarRef = useRef(null); useEffect(() => { const onToggle = () => setOpen((o) => !o); const onClose = () => setOpen(false); document.addEventListener(TOGGLE_DRAWER_EVENT, onToggle); document.addEventListener(OPEN_SPAWN_EVENT, onClose); document.addEventListener(SPAWN_SIMILAR_EVENT, onClose); return () => { document.removeEventListener(TOGGLE_DRAWER_EVENT, onToggle); document.removeEventListener(OPEN_SPAWN_EVENT, onClose); document.removeEventListener(SPAWN_SIMILAR_EVENT, onClose); }; }, []); // Horizontal swipes drive the drawer (right opens, left closes) rather than // the browser's back/forward navigation. Only a clearly horizontal gesture // past a threshold counts, so vertical terminal scrolling (which owns its // own touch handler) is untouched. useEffect(() => { const mobileMq = window.matchMedia("(max-width: 859px)"); let sx = 0; let sy = 0; let tracking = false; const onTouchStart = (e: TouchEvent) => { tracking = mobileMq.matches && e.touches.length === 1; if (tracking) { sx = e.touches[0].clientX; sy = e.touches[0].clientY; } }; const onTouchEnd = (e: TouchEvent) => { if (!tracking) return; tracking = false; const t = e.changedTouches[0]; const dx = t.clientX - sx; const dy = t.clientY - sy; if (Math.abs(dx) < 60 || Math.abs(dx) < Math.abs(dy) * 1.5) return; setOpen((o) => { if (dx > 0 && !o) return true; if (dx < 0 && o) return false; return o; }); }; document.addEventListener("touchstart", onTouchStart, { passive: true }); document.addEventListener("touchend", onTouchEnd, { passive: true }); return () => { document.removeEventListener("touchstart", onTouchStart); document.removeEventListener("touchend", onTouchEnd); }; }, []); return (
{ if ((e.target as HTMLElement).closest(".tree-leaf")) setOpen(false); }} > {sidebar}
{children}
setOpen(false)} />
); }