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:
parent
997f39bd33
commit
8450265449
11 changed files with 752 additions and 161 deletions
79
src/app/AppLayout.tsx
Normal file
79
src/app/AppLayout.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue