SPB Git
3.6 KB · 52 lines tsx
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
Files · main
Menu

Theme