expria-frontend/src/app/AppLayout.tsx
Hermann_Kitio 3ce91aaa7b feat(historique): refonte pixel-perfect avec stats + filtres + tendance 30j (Sprint 4.7)
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>
2026-04-26 00:04:12 +03:00

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>
)
}