"use client"; import { createContext, useCallback, useContext, useEffect, useMemo, useState } from "react"; import { PREF_KEYS } from "@/lib/prefs"; export interface Crumb { id: string; name: string; } export interface ShellHeader { title?: string; /** Fil d'Ariane (pages Fichiers / Dossier) — l'accueil "Fichiers" est ajouté automatiquement. */ crumbs?: Crumb[]; } interface ShellContextType { sidebarCollapsed: boolean; toggleSidebar: () => void; mobileNavOpen: boolean; setMobileNavOpen: (v: boolean) => void; commandOpen: boolean; openCommand: (initialQuery?: string) => void; closeCommand: () => void; commandQuery: string; prefsOpen: boolean; setPrefsOpen: (v: boolean) => void; header: ShellHeader; setHeader: (h: ShellHeader) => void; /** Utilisateur de session (chargé une fois pour tout le shell) */ session: SessionUser | null; isAdmin: boolean; refreshSession: () => void; } export interface SessionUser { id: string; name?: string | null; email?: string | null; role?: string; avatarPath?: string | null; twoFactorEnabled?: boolean; } const ShellContext = createContext(null); export function useShell(): ShellContextType { const ctx = useContext(ShellContext); if (!ctx) { // Contexte absent (page hors AppShell) → no-op sûr return { sidebarCollapsed: false, toggleSidebar: () => {}, mobileNavOpen: false, setMobileNavOpen: () => {}, commandOpen: false, openCommand: () => {}, closeCommand: () => {}, commandQuery: "", prefsOpen: false, setPrefsOpen: () => {}, header: {}, setHeader: () => {}, session: null, isAdmin: false, refreshSession: () => {}, }; } return ctx; } /** Déclare l'en-tête de la barre supérieure pour la page courante. */ export function useShellHeader(header: ShellHeader) { const { setHeader } = useShell(); const key = JSON.stringify(header); useEffect(() => { setHeader(header); return () => setHeader({}); // eslint-disable-next-line react-hooks/exhaustive-deps }, [key]); } export function ShellProvider({ children }: { children: React.ReactNode }) { const [sidebarCollapsed, setSidebarCollapsed] = useState(false); const [mobileNavOpen, setMobileNavOpen] = useState(false); const [commandOpen, setCommandOpen] = useState(false); const [commandQuery, setCommandQuery] = useState(""); const [prefsOpen, setPrefsOpen] = useState(false); const [header, setHeader] = useState({}); const [session, setSession] = useState(null); useEffect(() => { try { setSidebarCollapsed(localStorage.getItem(PREF_KEYS.sidebar) === "collapsed"); } catch {} }, []); const refreshSession = useCallback(() => { fetch("/api/auth/session") .then((r) => r.json()) .then((d) => { if (d?.isLoggedIn) { setSession({ id: d.userId, name: d.name, email: d.email, role: d.role || d.userRole, avatarPath: d.avatarPath, twoFactorEnabled: d.twoFactorEnabled }); } else setSession(null); }) .catch(() => {}); }, []); useEffect(() => { refreshSession(); }, [refreshSession]); const toggleSidebar = useCallback(() => { setSidebarCollapsed((c) => { const next = !c; try { localStorage.setItem(PREF_KEYS.sidebar, next ? "collapsed" : "expanded"); } catch {} return next; }); }, []); const openCommand = useCallback((q = "") => { setCommandQuery(q); setCommandOpen(true); }, []); const closeCommand = useCallback(() => setCommandOpen(false), []); // Raccourcis globaux : ⌘K / Ctrl+K, "/" hors champ de saisie, ⌘, pour les préférences useEffect(() => { const onKey = (e: KeyboardEvent) => { const t = e.target as HTMLElement | null; const typing = t && (t.tagName === "INPUT" || t.tagName === "TEXTAREA" || t.isContentEditable); if ((e.metaKey || e.ctrlKey) && e.key.toLowerCase() === "k") { e.preventDefault(); setCommandOpen((o) => !o); return; } if ((e.metaKey || e.ctrlKey) && e.key === ",") { e.preventDefault(); setPrefsOpen(true); return; } if (!typing && e.key === "/" && !e.metaKey && !e.ctrlKey && !e.altKey) { e.preventDefault(); openCommand(""); } }; window.addEventListener("keydown", onKey); // Événements custom (boutons, autres composants) const onOpen = (e: Event) => openCommand((e as CustomEvent).detail || ""); const onPrefs = () => setPrefsOpen(true); window.addEventListener("spb:command", onOpen); window.addEventListener("spb:prefs", onPrefs); return () => { window.removeEventListener("keydown", onKey); window.removeEventListener("spb:command", onOpen); window.removeEventListener("spb:prefs", onPrefs); }; }, [openCommand]); const value = useMemo(() => ({ sidebarCollapsed, toggleSidebar, mobileNavOpen, setMobileNavOpen, commandOpen, openCommand, closeCommand, commandQuery, prefsOpen, setPrefsOpen, header, setHeader, session, isAdmin: session?.role === "admin", refreshSession, }), [sidebarCollapsed, toggleSidebar, mobileNavOpen, commandOpen, openCommand, closeCommand, commandQuery, prefsOpen, header, session, refreshSession]); return {children}; } /** Ouvre la palette de commandes depuis n'importe où (sans contexte). */ export function openCommandPalette(query = "") { window.dispatchEvent(new CustomEvent("spb:command", { detail: query })); } export function openPreferences() { window.dispatchEvent(new Event("spb:prefs")); } /** Bascule le panneau Assistant IA (écouté par AiPanel). */ export function toggleAiPanel() { window.dispatchEvent(new Event("spb:ai-toggle")); }