"use client"; import React, { useEffect, useRef, useState } from "react"; import Footer from "./components/Footer"; import "./assets/main.css"; import "./globals.css"; import NavLink from "./components/NavLink"; import GrasBotFab from "./components/GrasBotFab"; import { manrope, newsreader } from "./fonts"; export default function RootLayout({ children }: { children: React.ReactNode }) { const [isMenuOpen, setIsMenuOpen] = useState(false); const menuRef = useRef(null); const burgerRef = useRef(null); const closeTimerRef = useRef | null>(null); const AUTO_CLOSE_MS = 4000; const clearAutoClose = () => { if (closeTimerRef.current) { clearTimeout(closeTimerRef.current); closeTimerRef.current = null; } }; const scheduleAutoClose = () => { clearAutoClose(); closeTimerRef.current = setTimeout(() => setIsMenuOpen(false), AUTO_CLOSE_MS); }; const closeMenu = () => setIsMenuOpen(false); const toggleMenu = () => setIsMenuOpen((v) => !v); useEffect(() => { if (!isMenuOpen) { clearAutoClose(); return; } scheduleAutoClose(); const handleKey = (e: KeyboardEvent) => { if (e.key === "Escape") setIsMenuOpen(false); }; const handleResize = () => { if (window.innerWidth >= 768) setIsMenuOpen(false); }; window.addEventListener("keydown", handleKey); window.addEventListener("resize", handleResize); return () => { clearAutoClose(); window.removeEventListener("keydown", handleKey); window.removeEventListener("resize", handleResize); }; }, [isMenuOpen]); useEffect(() => { if (!isMenuOpen && burgerRef.current) { burgerRef.current.focus({ preventScroll: true }); } }, [isMenuOpen]); // Classes communes pour les liens du drawer mobile : fond container primaire, // radius "tile", hover qui inverse vers fond clair + texte primaire (palette Stitch). const drawerLinkClass = "block px-4 py-2 rounded-tile bg-primary-container/60 text-white transition-colors duration-200 hover:bg-primary-fixed hover:text-primary"; const drawerLinkActive = "bg-primary-fixed text-primary"; // NavLink desktop : état actif souligné, inactif discret (palette Stitch). const desktopLinkActive = "text-primary border-b-2 border-primary-fixed pb-0.5"; const desktopLinkInactive = "text-on-surface-variant hover:text-primary transition-colors"; return ( {/* Material Symbols : chargés via plutôt que via @import CSS qui est strippé par la chaîne PostCSS + Tailwind de Next 15 (diagnostic 2026-04-22). Ressource critique côté UI → preload pour éviter un flash de texte brut sur les icônes des CTAs, du burger et des cartes de la home. */} {/* Wallpaper plein écran (fondation). `fixed inset-0` plutôt qu'`absolute` dans le grid : le wallpaper est désormais calé sur le viewport, pas sur la hauteur totale de la page. Sans ça, sur les pages longues (portfolio, compétences, fiches) le conteneur grid atteignait 2-3 viewports de haut et `background-size: cover` zoomait l'image pour couvrir cette hauteur — d'où le rendu incohérent entre home (courte) et listes (longues). Corrigé le 2026-04-22. */}
{/* Cercles animés : repalette en ton indigo-ardoise (Stitch "Digital Atelier"). */}
{/* Header "No-Line" : pas de bordure pleine, juste un shift tonal + ombre ambient diffuse. */}

Portfolio Gras-Calvet Fernand

{/* Burger ghost (Material Symbols) : plus sobre, couleur primaire, hover tonal. */} {/* Menu desktop : NavLink avec états actif/inactif éditoriaux. */}
{/* Drawer mobile (tiroir gauche, 70 %, fond primaire translucide). */}
{children}
{/* GrasBot : FAB global Stitch (étape 7.e). Accessible depuis toutes les pages, écoute aussi `CustomEvent("grasbot:open")` dispatché depuis les fiches compétences quand l'utilisateur clique sur « IA locale ». */} ); }