spb/chat-spboucher-ai
Public
1"use client";2import { useEffect, useState, createContext, useContext, type ReactNode } from "react";3import { useRouter, usePathname } from "next/navigation";4import { Menu, PanelLeft } from "lucide-react";5import { Sidebar } from "./sidebar";6import { CommandPalette } from "./command-palette";7import { usePrefs, setPrefs, applyTheme } from "@/lib/client/prefs";8import { resetDraft } from "@/lib/client/use-chat";9import { cn } from "./ui";10import { Logo } from "./logo";1112const ShellCtx = createContext<{ openPalette: () => void; openDrawer: () => void }>({ openPalette: () => {}, openDrawer: () => {} });13export const useShell = () => useContext(ShellCtx);1415export function AppShell({ children }: { children: ReactNode }) {16 const prefs = usePrefs(); const router = useRouter(); const pathname = usePathname();17 const [drawer, setDrawer] = useState(false); const [palette, setPalette] = useState(false);18 useEffect(() => { applyTheme(prefs.theme); }, [prefs.theme]);19 useEffect(() => { document.documentElement.dataset.fontsize = prefs.fontSize; }, [prefs.fontSize]);20 useEffect(() => { setDrawer(false); }, [pathname]);21 useEffect(() => { if ("serviceWorker" in navigator && process.env.NODE_ENV === "production") navigator.serviceWorker.register("/sw.js").catch(() => {}); }, []);22 useEffect(() => {23 const onKey = (e: KeyboardEvent) => {24 const mod = e.metaKey || e.ctrlKey;25 if (mod && e.key.toLowerCase() === "k") { e.preventDefault(); setPalette(p => !p); }26 else if (mod && e.shiftKey && e.key.toLowerCase() === "o") { e.preventDefault(); resetDraft(); router.push("/"); }27 else if (mod && e.key === "\\") { e.preventDefault(); setPrefs({ sidebarCollapsed: !prefs.sidebarCollapsed }); }28 };29 window.addEventListener("keydown", onKey); return () => window.removeEventListener("keydown", onKey);30 }, [prefs.sidebarCollapsed, router]);31 const collapsed = prefs.sidebarCollapsed;32 return (33 <ShellCtx.Provider value={{ openPalette: () => setPalette(true), openDrawer: () => setDrawer(true) }}>34 <div className="flex h-dvh w-full overflow-hidden bg-bg">35 {/* desktop sidebar */}36 <aside className={cn("hidden md:flex h-full shrink-0 flex-col border-r border-border bg-bg-elev transition-[width] duration-200", collapsed ? "w-0 overflow-hidden border-r-0" : "w-[var(--sidebar-w)]")}>37 <Sidebar onOpenPalette={() => setPalette(true)} />38 </aside>39 {/* mobile drawer */}40 {drawer && <div className="fixed inset-0 z-40 md:hidden" onClick={() => setDrawer(false)}><div className="absolute inset-0 bg-black/60" /><aside onClick={e => e.stopPropagation()} className="absolute inset-y-0 left-0 w-[86vw] max-w-[320px] anim-fade-up bg-bg-elev shadow-soft safe-t"><Sidebar onOpenPalette={() => { setDrawer(false); setPalette(true); }} onNavigate={() => setDrawer(false)} /></aside></div>}41 <div className="relative flex min-w-0 flex-1 flex-col">42 {/* top bar (mobile + collapsed) */}43 <header className={cn("flex items-center gap-2 px-2 py-2 md:px-3 safe-t", !collapsed && "md:hidden")}>44 <button className="md:hidden inline-flex h-9 w-9 items-center justify-center rounded-lg hover:bg-surface-2" aria-label="Menu" onClick={() => setDrawer(true)}><Menu className="h-5 w-5" /></button>45 <button className="hidden md:inline-flex h-9 w-9 items-center justify-center rounded-lg text-fg-2 hover:bg-surface-2" aria-label="Afficher le panneau" onClick={() => setPrefs({ sidebarCollapsed: false })}><PanelLeft className="h-4.5 w-4.5" /></button>46 <div className="flex items-center gap-2 md:hidden"><Logo className="h-6 w-6" /><span className="text-sm font-semibold tracking-tight">Chat SPBoucher AI</span></div>47 </header>48 <main className="flex min-h-0 flex-1 flex-col">{children}</main>49 </div>50 </div>51 <CommandPalette open={palette} onClose={() => setPalette(false)} />52 </ShellCtx.Provider>53 );54}55