import { BookOpen, CircleHelp, Files, Home, Library, Menu, Network, PanelLeftClose, Search, Settings, Shapes, X, } from "lucide-react"; import { useEffect, useRef, useState } from "react"; export type Section = | "home" | "notes" | "learning" | "problems" | "shelf" | "graph" | "whiteboards" | "settings"; const PRIMARY_NAV: Array<{ id: Section; label: string; icon: React.ReactNode; }> = [ { id: "home", label: "Overview", icon: }, { id: "notes", label: "Notes", icon: }, { id: "learning", label: "Learning", icon: }, { id: "problems", label: "Problems", icon: }, { id: "shelf", label: "Library", icon: }, ]; const SPATIAL_NAV: Array<{ id: Section; label: string; icon: React.ReactNode; }> = [ { id: "graph", label: "Graph", icon: }, { id: "whiteboards", label: "Whiteboards", icon: }, ]; const SECTION_LABELS: Record = { home: "Overview", notes: "Notes", learning: "Learning", problems: "Problems", shelf: "Library", graph: "Graph", whiteboards: "Whiteboards", settings: "Settings", }; export function AppShell({ section, onNavigate, onSearchNotes, boardEditor = false, children, }: { section: Section; onNavigate: (section: Section) => void; onSearchNotes?: () => void; boardEditor?: boolean; children: React.ReactNode; }) { const [mobileOpen, setMobileOpen] = useState(false); const mobileNavigationTriggerRef = useRef(null); const mobileNavigationCloseRef = useRef(null); const [shortcutLabel] = useState(() => { if (typeof navigator === "undefined") return "Ctrl K"; return /Mac|iPhone|iPad/.test(navigator.platform) ? "⌘ K" : "Ctrl K"; }); const [collapsed, setCollapsed] = useState( () => localStorage.getItem("forge.sidebar.collapsed") === "true", ); useEffect(() => { localStorage.setItem("forge.sidebar.collapsed", String(collapsed)); }, [collapsed]); useEffect(() => { if (!onSearchNotes) return; const handleShortcut = (event: KeyboardEvent) => { if ((event.metaKey || event.ctrlKey) && event.key.toLowerCase() === "k") { event.preventDefault(); setMobileOpen(false); onSearchNotes(); } }; document.addEventListener("keydown", handleShortcut); return () => document.removeEventListener("keydown", handleShortcut); }, [onSearchNotes]); useEffect(() => { if (!mobileOpen) return; const sidebar = document.getElementById("forge-navigation"); if (!sidebar) return; const getFocusable = () => Array.from( sidebar.querySelectorAll( 'button:not([disabled]), a[href], input:not([disabled]), select:not([disabled]), textarea:not([disabled]), summary, [contenteditable="true"], [tabindex]:not([tabindex="-1"])', ), ).filter((element) => { const style = window.getComputedStyle(element); const rect = element.getBoundingClientRect(); return style.visibility !== "hidden" && style.display !== "none" && rect.width > 0 && rect.height > 0; }); window.requestAnimationFrame(() => mobileNavigationCloseRef.current?.focus()); const handleKeyDown = (event: KeyboardEvent) => { if (event.key === "Escape") { event.preventDefault(); setMobileOpen(false); window.requestAnimationFrame(() => mobileNavigationTriggerRef.current?.focus()); return; } if (event.key !== "Tab") return; const focusable = getFocusable(); if (!focusable.length) return; const first = focusable[0]; const last = focusable[focusable.length - 1]; if (event.shiftKey && document.activeElement === first) { event.preventDefault(); last.focus(); } else if (!event.shiftKey && document.activeElement === last) { event.preventDefault(); first.focus(); } }; document.addEventListener("keydown", handleKeyDown); return () => document.removeEventListener("keydown", handleKeyDown); }, [mobileOpen]); const compact = boardEditor || collapsed; const navigate = (next: Section) => { onNavigate(next); setMobileOpen(false); if (window.matchMedia("(max-width: 1100px)").matches) { window.requestAnimationFrame(() => mobileNavigationTriggerRef.current?.focus()); } }; const navGroup = ( label: string, entries: typeof PRIMARY_NAV, ) => (
{label} {entries.map((item) => ( ))}
); return (
{SECTION_LABELS[section]}
{children}
); }