feat(sprint-0.5-bis): AppLayout + primitives UI + refonte pages

- AppLayout (sidebar fixe, drawer mobile, BottomNav)
- MobileHeader sticky + Sidebar avec verrouillage hasAccess()
- Primitives src/shared/ui/ : Button, Card, Badge
- SimulationPage + DashboardPage : suppression headers internes
- TaskSelector : Card interactive + Badge EE/EO + eyebrow
- router.tsx : layout routes + ComingSoon inline
This commit is contained in:
Hermann_Kitio 2026-04-20 02:37:19 +03:00
parent 997f39bd33
commit 8450265449
11 changed files with 752 additions and 161 deletions

79
src/app/AppLayout.tsx Normal file
View file

@ -0,0 +1,79 @@
/**
* Layout applicatif enveloppe toutes les routes privées.
*
* Desktop ( 1024px) : Sidebar fixe 240px + zone contenu principale.
* Mobile (< 1024px) : MobileHeader sticky + drawer slide-in + BottomNav fixe.
*
* Le drawer mobile se ferme automatiquement à chaque changement de route
* (useEffect sur location.pathname).
*
* Règle L : tokens Direction H 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 { MobileHeader } from './MobileHeader'
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(() => {
setIsMobileMenuOpen(false)
}, [location.pathname])
return (
<div className="min-h-screen bg-canvas">
{/* ── DESKTOP — Sidebar fixe 240px ───────────────────────────── */}
<aside className="hidden lg:fixed lg:inset-y-0 lg:left-0 lg:flex lg:w-60 lg:flex-col">
<Sidebar plan={plan} />
</aside>
{/* ── MOBILE — Header sticky ─────────────────────────────────── */}
<MobileHeader onMenuOpen={() => setIsMobileMenuOpen(true)} />
{/* ── MOBILE — Drawer overlay ────────────────────────────────── */}
<div
aria-hidden="true"
onClick={() => setIsMobileMenuOpen(false)}
className={cn(
'fixed inset-0 z-40 bg-ink-1/30 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-60 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 ────────────────────────────────────────── */}
{/* pb-16 sur mobile pour ne pas être masqué par le BottomNav fixe */}
<div className="pb-16 lg:pl-60 lg:pb-0">
{children}
</div>
{/* ── MOBILE — BottomNav fixe ────────────────────────────────── */}
<BottomNav />
</div>
)
}