JavaScript 56.7%
TypeScript 42.6%
1"use client";23import { createContext, useCallback, useContext, useEffect, useMemo, useState } from "react";4import { PREF_KEYS } from "@/lib/prefs";56export interface Crumb {7 id: string;8 name: string;9}1011export interface ShellHeader {12 title?: string;13 /** Fil d'Ariane (pages Fichiers / Dossier) — l'accueil "Fichiers" est ajouté automatiquement. */14 crumbs?: Crumb[];15}1617interface ShellContextType {18 sidebarCollapsed: boolean;19 toggleSidebar: () => void;20 mobileNavOpen: boolean;21 setMobileNavOpen: (v: boolean) => void;22 commandOpen: boolean;23 openCommand: (initialQuery?: string) => void;24 closeCommand: () => void;25 commandQuery: string;26 prefsOpen: boolean;27 setPrefsOpen: (v: boolean) => void;28 header: ShellHeader;29 setHeader: (h: ShellHeader) => void;30 /** Utilisateur de session (chargé une fois pour tout le shell) */31 session: SessionUser | null;32 isAdmin: boolean;33 refreshSession: () => void;34}3536export interface SessionUser {37 id: string;38 name?: string | null;39 email?: string | null;40 role?: string;41 avatarPath?: string | null;42 twoFactorEnabled?: boolean;43}4445const ShellContext = createContext<ShellContextType | null>(null);4647export function useShell(): ShellContextType {48 const ctx = useContext(ShellContext);49 if (!ctx) {50 // Contexte absent (page hors AppShell) → no-op sûr51 return {52 sidebarCollapsed: false, toggleSidebar: () => {},53 mobileNavOpen: false, setMobileNavOpen: () => {},54 commandOpen: false, openCommand: () => {}, closeCommand: () => {}, commandQuery: "",55 prefsOpen: false, setPrefsOpen: () => {},56 header: {}, setHeader: () => {},57 session: null, isAdmin: false, refreshSession: () => {},58 };59 }60 return ctx;61}6263/** Déclare l'en-tête de la barre supérieure pour la page courante. */64export function useShellHeader(header: ShellHeader) {65 const { setHeader } = useShell();66 const key = JSON.stringify(header);67 useEffect(() => {68 setHeader(header);69 return () => setHeader({});70 // eslint-disable-next-line react-hooks/exhaustive-deps71 }, [key]);72}7374export function ShellProvider({ children }: { children: React.ReactNode }) {75 const [sidebarCollapsed, setSidebarCollapsed] = useState(false);76 const [mobileNavOpen, setMobileNavOpen] = useState(false);77 const [commandOpen, setCommandOpen] = useState(false);78 const [commandQuery, setCommandQuery] = useState("");79 const [prefsOpen, setPrefsOpen] = useState(false);80 const [header, setHeader] = useState<ShellHeader>({});81 const [session, setSession] = useState<SessionUser | null>(null);8283 useEffect(() => {84 try {85 setSidebarCollapsed(localStorage.getItem(PREF_KEYS.sidebar) === "collapsed");86 } catch {}87 }, []);8889 const refreshSession = useCallback(() => {90 fetch("/api/auth/session")91 .then((r) => r.json())92 .then((d) => {93 if (d?.isLoggedIn) {94 setSession({ id: d.userId, name: d.name, email: d.email, role: d.role || d.userRole, avatarPath: d.avatarPath, twoFactorEnabled: d.twoFactorEnabled });95 } else setSession(null);96 })97 .catch(() => {});98 }, []);99100 useEffect(() => { refreshSession(); }, [refreshSession]);101102 const toggleSidebar = useCallback(() => {103 setSidebarCollapsed((c) => {104 const next = !c;105 try { localStorage.setItem(PREF_KEYS.sidebar, next ? "collapsed" : "expanded"); } catch {}106 return next;107 });108 }, []);109110 const openCommand = useCallback((q = "") => {111 setCommandQuery(q);112 setCommandOpen(true);113 }, []);114 const closeCommand = useCallback(() => setCommandOpen(false), []);115116 // Raccourcis globaux : ⌘K / Ctrl+K, "/" hors champ de saisie, ⌘, pour les préférences117 useEffect(() => {118 const onKey = (e: KeyboardEvent) => {119 const t = e.target as HTMLElement | null;120 const typing = t && (t.tagName === "INPUT" || t.tagName === "TEXTAREA" || t.isContentEditable);121 if ((e.metaKey || e.ctrlKey) && e.key.toLowerCase() === "k") {122 e.preventDefault();123 setCommandOpen((o) => !o);124 return;125 }126 if ((e.metaKey || e.ctrlKey) && e.key === ",") {127 e.preventDefault();128 setPrefsOpen(true);129 return;130 }131 if (!typing && e.key === "/" && !e.metaKey && !e.ctrlKey && !e.altKey) {132 e.preventDefault();133 openCommand("");134 }135 };136 window.addEventListener("keydown", onKey);137 // Événements custom (boutons, autres composants)138 const onOpen = (e: Event) => openCommand((e as CustomEvent<string>).detail || "");139 const onPrefs = () => setPrefsOpen(true);140 window.addEventListener("spb:command", onOpen);141 window.addEventListener("spb:prefs", onPrefs);142 return () => {143 window.removeEventListener("keydown", onKey);144 window.removeEventListener("spb:command", onOpen);145 window.removeEventListener("spb:prefs", onPrefs);146 };147 }, [openCommand]);148149 const value = useMemo<ShellContextType>(() => ({150 sidebarCollapsed, toggleSidebar,151 mobileNavOpen, setMobileNavOpen,152 commandOpen, openCommand, closeCommand, commandQuery,153 prefsOpen, setPrefsOpen,154 header, setHeader,155 session, isAdmin: session?.role === "admin", refreshSession,156 }), [sidebarCollapsed, toggleSidebar, mobileNavOpen, commandOpen, openCommand, closeCommand, commandQuery, prefsOpen, header, session, refreshSession]);157158 return <ShellContext.Provider value={value}>{children}</ShellContext.Provider>;159}160161/** Ouvre la palette de commandes depuis n'importe où (sans contexte). */162export function openCommandPalette(query = "") {163 window.dispatchEvent(new CustomEvent("spb:command", { detail: query }));164}165export function openPreferences() {166 window.dispatchEvent(new Event("spb:prefs"));167}168/** Bascule le panneau Assistant IA (écouté par AiPanel). */169export function toggleAiPanel() {170 window.dispatchEvent(new Event("spb:ai-toggle"));171}172