spb/note-spboucher-ai
Public
1"use client";2import { useEffect, useState } from "react";3import { usePathname, useRouter } from "next/navigation";4import { useQuery } from "@tanstack/react-query";5import { useUI } from "@/lib/client/store";6import { Sidebar } from "./Sidebar";7import { TopBar } from "./TopBar";8import { MobileNav } from "./MobileNav";9import { CommandPalette } from "./CommandPalette";10import { AIPanel } from "./AIPanel";11import { ShortcutsDialog } from "./ShortcutsDialog";12import { Minimize2 } from "lucide-react";13import type { TreeNode, ShellUser } from "./types";14import { cn } from "@/lib/utils";15import { api } from "@/lib/client/api";16import { applyPrefs, readLocalPrefs } from "@/components/settings/prefs";1718export function Shell({ user, workspace, initialTree, children }: { user: ShellUser; workspace: { id: string; name: string }; initialTree: TreeNode[]; children: React.ReactNode }) {19 const ui = useUI(); const r = useRouter(); const pathname = usePathname(); const [help, setHelp] = useState(false);20 const { data: tree = initialTree } = useQuery({ queryKey: ["tree"], queryFn: () => api<TreeNode[]>("/api/tree"), initialData: initialTree, staleTime: 10_000 });21 useEffect(() => { ui.setTheme(ui.theme); try { applyPrefs(readLocalPrefs()); } catch {} /* eslint-disable-next-line */ }, []);22 useEffect(() => {23 const onKey = (e: KeyboardEvent) => {24 const mod = e.metaKey || e.ctrlKey;25 if (mod && e.key === "k") { e.preventDefault(); ui.set({ paletteOpen: !ui.paletteOpen }); }26 else if (mod && e.key === "/") { e.preventDefault(); ui.toggleAI(); }27 else if (mod && e.key === "\\") { e.preventDefault(); ui.toggleSidebar(); }28 else if (mod && e.shiftKey && e.key.toLowerCase() === "n") { e.preventDefault(); api("/api/pages", { method: "POST", json: { title: "" } }).then((p) => r.push(`/p/${p.id}`)); }29 else if (mod && e.key === ".") { e.preventDefault(); ui.set({ focusMode: !ui.focusMode }); }30 else if (mod && (e.key === "?" || (e.shiftKey && e.key === "/"))) { e.preventDefault(); setHelp((h) => !h); }31 else if (e.key === "Escape" && ui.focusMode && !(e.target as HTMLElement)?.closest?.(".ProseMirror")) ui.set({ focusMode: false });32 };33 window.addEventListener("keydown", onKey); return () => window.removeEventListener("keydown", onKey);34 }, [ui, r]);35 useEffect(() => { if ("serviceWorker" in navigator && process.env.NODE_ENV === "production") navigator.serviceWorker.register("/sw.js").catch(() => {}); }, []);36 const sidebar = ui.sidebarOpen && !ui.focusMode;37 return (38 <div className="flex h-dvh w-full overflow-hidden bg-bg">39 <aside className={cn("hidden md:flex shrink-0 flex-col border-r border-line bg-bg-sunk/60 transition-[width] duration-200", sidebar ? "w-64" : "w-0 overflow-hidden border-r-0")}><Sidebar tree={tree} user={user} workspace={workspace} /></aside>40 <div className="flex min-w-0 flex-1 flex-col">41 {!ui.focusMode && <TopBar tree={tree} />}42 <main className={cn("relative min-h-0 flex-1 overflow-y-auto overscroll-contain", "pb-16 md:pb-0")} id="main-scroll">{children}</main>43 <MobileNav />44 </div>45 {ui.aiOpen && !ui.focusMode && <AIPanel pathname={pathname} />}46 <CommandPalette tree={tree} />47 <ShortcutsDialog open={help} onOpenChange={setHelp} />48 {ui.focusMode && <button onClick={() => ui.set({ focusMode: false })} title="Exit focus mode (⌘.)" className="fixed right-3 top-3 z-30 grid size-8 place-items-center rounded-md border border-line bg-bg-elev/80 text-fg-muted shadow-card backdrop-blur hover:text-fg safe-top"><Minimize2 className="size-4" /></button>}49 </div>50 );51}52