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

53
src/shared/ui/Badge.tsx Normal file
View file

@ -0,0 +1,53 @@
/**
* Primitive Badge Design System Expria (DESIGN_SYSTEM.md §4).
*
* Variants :
* plan couleur selon le plan (free / standard / premium)
* nclc score NCLC (bleu Expria)
* neutral étiquette générique (gris)
*
* Taille fixe, text-xs uppercase tracking-wide (DESIGN_SYSTEM.md §3 eyebrow style).
* Règle L : tokens Direction H exclusivement.
*/
import { cn } from '@/shared/lib/utils'
export type BadgeVariant = 'plan' | 'nclc' | 'neutral'
export type BadgePlanValue = 'free' | 'standard' | 'premium'
export interface BadgeProps {
variant : BadgeVariant
planValue?: BadgePlanValue
className?: string
children : React.ReactNode
}
const planClasses: Record<BadgePlanValue, string> = {
free : 'bg-canvas-2 text-ink-4',
standard: 'bg-expria-50 text-expria',
premium : 'bg-deep text-white',
}
const variantClasses: Record<BadgeVariant, string> = {
plan : '', // résolu dynamiquement via planValue
nclc : 'bg-expria-50 text-expria',
neutral: 'bg-canvas-2 text-ink-4',
}
export function Badge({ variant, planValue, className, children }: BadgeProps) {
const colorClasses =
variant === 'plan' && planValue ? planClasses[planValue] : variantClasses[variant]
return (
<span
className={cn(
'inline-flex items-center rounded-full px-2 py-0.5',
'text-xs font-semibold uppercase tracking-wide',
colorClasses,
className,
)}
>
{children}
</span>
)
}

77
src/shared/ui/Button.tsx Normal file
View file

@ -0,0 +1,77 @@
/**
* Primitive Button Design System Expria (DESIGN_SYSTEM.md §4).
*
* Variants : primary / secondary / ghost / upgrade
* Sizes : sm / md (défaut) / lg
* États : loading (spinner + disabled auto), disabled
*
* Règle L : tokens Direction H exclusivement.
* DESIGN_SYSTEM.md §8 : pas de nouvelle dépendance lucide-react déjà présent.
*/
import { Loader2 } from 'lucide-react'
import { cn } from '@/shared/lib/utils'
export type ButtonVariant = 'primary' | 'secondary' | 'ghost' | 'upgrade'
export type ButtonSize = 'sm' | 'md' | 'lg'
export interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
variant? : ButtonVariant
size? : ButtonSize
icon? : React.ReactNode
loading? : boolean
className?: string
}
const variantClasses: Record<ButtonVariant, string> = {
primary:
'bg-expria text-white hover:bg-expria-hover active:bg-expria-hover disabled:bg-expria/50',
secondary:
'border border-line bg-surface text-ink-2 hover:bg-canvas hover:text-ink-1 disabled:text-ink-4',
ghost:
'bg-transparent text-ink-3 hover:bg-canvas hover:text-ink-1 disabled:text-ink-5',
upgrade:
'bg-deep text-white hover:bg-deep-2 active:bg-deep-2 disabled:bg-deep/50',
}
const sizeClasses: Record<ButtonSize, string> = {
sm: 'h-8 gap-1.5 rounded-md px-3 text-xs',
md: 'h-9 gap-2 rounded-md px-4 text-sm',
lg: 'h-11 gap-2 rounded-lg px-6 text-base',
}
export function Button({
variant = 'primary',
size = 'md',
icon,
loading = false,
disabled,
className,
children,
...props
}: ButtonProps) {
const isDisabled = disabled || loading
return (
<button
disabled={isDisabled}
className={cn(
// base
'inline-flex cursor-pointer items-center justify-center font-medium transition-colors duration-150',
'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-expria/20',
'disabled:cursor-not-allowed disabled:opacity-50',
variantClasses[variant],
sizeClasses[size],
className,
)}
{...props}
>
{loading ? (
<Loader2 className="size-4 animate-spin" aria-hidden="true" />
) : (
icon && <span className="shrink-0">{icon}</span>
)}
{children}
</button>
)
}

57
src/shared/ui/Card.tsx Normal file
View file

@ -0,0 +1,57 @@
/**
* Primitive Card Design System Expria (DESIGN_SYSTEM.md §4).
*
* Variants :
* default surface bordée, ombre légère
* raised élévation plus marquée (MetricCard hero, recommandations)
* interactive hover state + curseur pointer ; rendu en <button> si onClick fourni
*
* Règle L : tokens Direction H exclusivement.
*/
import { cn } from '@/shared/lib/utils'
export type CardVariant = 'default' | 'raised' | 'interactive'
interface CardBaseProps {
variant? : CardVariant
className?: string
children : React.ReactNode
}
interface CardDivProps extends CardBaseProps {
onClick?: undefined
}
interface CardButtonProps extends CardBaseProps {
onClick: () => void
}
export type CardProps = CardDivProps | CardButtonProps
const baseClasses = 'rounded-lg border border-line bg-surface'
const variantClasses: Record<CardVariant, string> = {
default : 'shadow-sm',
raised : 'shadow-md',
interactive:
'shadow-sm cursor-pointer transition-colors duration-150 hover:border-expria hover:bg-surface-hover focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-expria/20',
}
export function Card({ variant = 'default', className, children, onClick }: CardProps) {
const classes = cn(baseClasses, variantClasses[variant], className)
if (onClick) {
return (
<button type="button" onClick={onClick} className={classes}>
{children}
</button>
)
}
return (
<div className={classes}>
{children}
</div>
)
}