Inclut le retrait du padding de AppLayout et le wrapper standardisé (mx-auto w-full max-w-[1100px] px-5 py-6 lg:px-9 lg:py-9) ajouté sur 11 pages (Dashboard, Progression, 9 pages Simulation EE/EO/T1) pour laisser chaque page gérer son max-width. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
89 lines
3.5 KiB
TypeScript
89 lines
3.5 KiB
TypeScript
/**
|
|
* Layout applicatif — enveloppe toutes les routes privées.
|
|
*
|
|
* Desktop (≥ 1024px) : Sidebar fixe 230px + Topbar sticky + zone contenu.
|
|
* Mobile (< 1024px) : Topbar avec hamburger + drawer slide-in + BottomNav fixe.
|
|
*
|
|
* Le drawer mobile se ferme automatiquement à chaque changement de route
|
|
* (useEffect sur location.pathname).
|
|
*
|
|
* Règle L : tokens du design system exclusivement.
|
|
* Règle H : aucune logique métier — plan lu depuis le cache TanStack Query.
|
|
*/
|
|
|
|
import { useState, useEffect } from 'react'
|
|
import { useLocation } from 'react-router-dom'
|
|
import { Sidebar } from './Sidebar'
|
|
import { Topbar } from './Topbar'
|
|
import { BottomNav } from './BottomNav'
|
|
import { usePlan } from '@/features/dashboard/hooks/usePlan'
|
|
import { cn } from '@/shared/lib/utils'
|
|
import type { Plan } from '@/entities/user/lib'
|
|
|
|
interface AppLayoutProps {
|
|
children: React.ReactNode
|
|
}
|
|
|
|
export function AppLayout({ children }: AppLayoutProps) {
|
|
const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false)
|
|
const location = useLocation()
|
|
const { data } = usePlan()
|
|
const plan: Plan = data?.plan ?? 'free'
|
|
|
|
// Ferme le drawer à chaque changement de route.
|
|
useEffect(() => {
|
|
// eslint-disable-next-line react-hooks/set-state-in-effect
|
|
setIsMobileMenuOpen(false)
|
|
}, [location.pathname])
|
|
|
|
const mainBackground = `
|
|
radial-gradient(ellipse at 35% 0%, var(--color-gradient-a), transparent 55%),
|
|
radial-gradient(ellipse at 80% 100%, var(--color-gradient-b), transparent 50%),
|
|
var(--color-canvas)
|
|
`
|
|
|
|
return (
|
|
<div className="min-h-screen">
|
|
{/* ── DESKTOP — Sidebar fixe 230px ───────────────────────────── */}
|
|
<aside className="hidden lg:fixed lg:inset-y-0 lg:left-0 lg:flex lg:w-[230px] lg:flex-col">
|
|
<Sidebar plan={plan} />
|
|
</aside>
|
|
|
|
{/* ── MOBILE — Drawer overlay ────────────────────────────────── */}
|
|
<div
|
|
aria-hidden="true"
|
|
onClick={() => setIsMobileMenuOpen(false)}
|
|
className={cn(
|
|
'fixed inset-0 z-40 bg-black/40 transition-opacity duration-200 ease-out lg:hidden',
|
|
isMobileMenuOpen ? 'opacity-100' : 'pointer-events-none opacity-0',
|
|
)}
|
|
/>
|
|
|
|
{/* ── MOBILE — Drawer panel ──────────────────────────────────── */}
|
|
<div
|
|
className={cn(
|
|
'fixed inset-y-0 left-0 z-50 flex w-[230px] flex-col transition-transform duration-200 ease-out lg:hidden',
|
|
isMobileMenuOpen ? 'translate-x-0' : '-translate-x-full',
|
|
)}
|
|
aria-hidden={!isMobileMenuOpen}
|
|
>
|
|
<Sidebar plan={plan} />
|
|
</div>
|
|
|
|
{/* ── Zone de contenu principale ─────────────────────────────── */}
|
|
{/* pb-16 sur mobile pour ne pas être masqué par le BottomNav fixe */}
|
|
<main
|
|
className="min-h-screen pb-16 lg:pb-0 lg:pl-[230px]"
|
|
style={{ background: mainBackground }}
|
|
>
|
|
<Topbar onMobileMenuOpen={() => setIsMobileMenuOpen(true)} />
|
|
{/* Pas de padding ni de max-width ici : chaque page gère sa propre
|
|
largeur de contenu et son propre padding (cf. HistoriquePage). */}
|
|
{children}
|
|
</main>
|
|
|
|
{/* ── MOBILE — BottomNav fixe ────────────────────────────────── */}
|
|
<BottomNav />
|
|
</div>
|
|
)
|
|
}
|