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

148
src/app/BottomNav.tsx Normal file
View file

@ -0,0 +1,148 @@
/**
* Navigation mobile fixe affichée uniquement en dessous de 1024px.
*
* 4 items : Accueil / Simuler / Progression / Compte.
* "Simuler" ouvre une bottom sheet (EE / EO / Examen blanc).
* Tap target 44×44px minimum (DESIGN_SYSTEM.md §7).
*
* Règle L : tokens Direction H exclusivement.
* Règle H : aucune logique métier navigation uniquement.
*/
import { useState } from 'react'
import { Link, useLocation, useNavigate } from 'react-router-dom'
import { Home, BookOpen, TrendingUp, User } from 'lucide-react'
import { cn } from '@/shared/lib/utils'
const SHEET_ITEMS = [
{ label: 'Expression Écrite', to: '/simulation/ee' },
{ label: 'Expression Orale', to: '/simulation/eo' },
{ label: 'Examen blanc', to: '/examen' },
] as const
export function BottomNav() {
const [isSheetOpen, setIsSheetOpen] = useState(false)
const location = useLocation()
const navigate = useNavigate()
const isActive = (prefix: string) => location.pathname.startsWith(prefix)
function handleSheetNavigate(to: string) {
setIsSheetOpen(false)
navigate(to)
}
return (
<>
{/* Bottom sheet overlay */}
{isSheetOpen && (
<div
className="fixed inset-0 z-40 lg:hidden"
aria-hidden="true"
onClick={() => setIsSheetOpen(false)}
/>
)}
{/* Bottom sheet */}
{isSheetOpen && (
<div
role="dialog"
aria-label="Choisir une simulation"
className="fixed bottom-16 left-0 right-0 z-50 rounded-t-xl border-t border-line bg-surface px-2 py-2 shadow-md lg:hidden"
>
<p className="px-3 pb-2 pt-1 text-[11px] font-semibold uppercase tracking-widest text-ink-5">
Simuler
</p>
<ul role="list">
{SHEET_ITEMS.map((item) => (
<li key={item.to}>
<button
type="button"
onClick={() => handleSheetNavigate(item.to)}
className="flex min-h-[44px] w-full items-center rounded-md px-3 text-sm text-ink-2 transition-colors duration-150 hover:bg-canvas hover:text-ink-1"
>
{item.label}
</button>
</li>
))}
</ul>
</div>
)}
{/* Bottom nav bar */}
<nav
aria-label="Navigation mobile"
className="fixed bottom-0 left-0 right-0 z-30 flex h-16 items-center border-t border-line bg-surface lg:hidden"
>
{/* Accueil */}
<Link
to="/dashboard"
aria-label="Accueil"
className={cn(
'flex min-h-[44px] flex-1 flex-col items-center justify-center gap-0.5 text-[10px] font-medium transition-colors duration-150',
isActive('/dashboard') ? 'text-expria' : 'text-ink-4 hover:text-ink-2',
)}
>
<Home
className={cn('size-5', isActive('/dashboard') && 'text-expria')}
aria-hidden="true"
/>
Accueil
</Link>
{/* Simuler */}
<button
type="button"
aria-label="Simuler"
aria-expanded={isSheetOpen}
onClick={() => setIsSheetOpen((v) => !v)}
className={cn(
'flex min-h-[44px] flex-1 flex-col items-center justify-center gap-0.5 text-[10px] font-medium transition-colors duration-150',
isActive('/simulation') || isSheetOpen ? 'text-expria' : 'text-ink-4 hover:text-ink-2',
)}
>
<BookOpen
className={cn(
'size-5',
(isActive('/simulation') || isSheetOpen) && 'text-expria',
)}
aria-hidden="true"
/>
Simuler
</button>
{/* Progression */}
<Link
to="/progression"
aria-label="Progression"
className={cn(
'flex min-h-[44px] flex-1 flex-col items-center justify-center gap-0.5 text-[10px] font-medium transition-colors duration-150',
isActive('/progression') ? 'text-expria' : 'text-ink-4 hover:text-ink-2',
)}
>
<TrendingUp
className={cn('size-5', isActive('/progression') && 'text-expria')}
aria-hidden="true"
/>
Progression
</Link>
{/* Compte */}
<Link
to="/parametres"
aria-label="Compte"
className={cn(
'flex min-h-[44px] flex-1 flex-col items-center justify-center gap-0.5 text-[10px] font-medium transition-colors duration-150',
isActive('/parametres') ? 'text-expria' : 'text-ink-4 hover:text-ink-2',
)}
>
<User
className={cn('size-5', isActive('/parametres') && 'text-expria')}
aria-hidden="true"
/>
Compte
</Link>
</nav>
</>
)
}