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
53
src/shared/ui/Badge.tsx
Normal file
53
src/shared/ui/Badge.tsx
Normal 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
77
src/shared/ui/Button.tsx
Normal 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
57
src/shared/ui/Card.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue