expria-frontend/src/features/design-system/DesignSystemPage.tsx
Hermann_Kitio b68f160bce feat(design-system): reskin Charcoal — tokens dark-default + sidebar navy permanent
- Remplacement intégral index.css par palette Charcoal (DESIGN_SYSTEM.md v2.0)
- Dark = thème par défaut, .light = override via @custom-variant light
- Sidebar navy #0C1528 permanent (identique dark+light)
- Script anti-FOUC inline dans index.html
- Layout : radial-gradient sur <main>, sidebar 230px, max-w-[1100px]
- Renommage tokens Boréal→Charcoal sur ~45 composants
- Inversion dark: → baseline + light: sur primitives shadcn
- Fix logo blanc forcé dans sidebar
- ADR 006 mis à jour

Typecheck: OK · Tests: 122/122 

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-24 23:09:15 +03:00

389 lines
12 KiB
TypeScript

import React, { useState } from 'react'
import { useTheme } from '@/shared/hooks/useTheme'
import { Button } from '@/shared/components/ui/button'
import { Badge } from '@/shared/components/ui/badge'
import { Input } from '@/shared/components/ui/input'
import { Label } from '@/shared/components/ui/label'
import { Separator } from '@/shared/components/ui/separator'
import { Progress } from '@/shared/components/ui/progress'
import {
Avatar,
AvatarFallback,
AvatarImage,
AvatarGroup,
AvatarGroupCount,
} from '@/shared/components/ui/avatar'
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from '@/shared/components/ui/dialog'
// ─── palette data — DA Charcoal ──────────────────────────────────────────────
interface PaletteEntry {
token: string
cssVar: string
dark: string
light: string
group: 'Invariants' | 'Dark default' | 'Light override'
}
const PALETTE: PaletteEntry[] = [
// Invariants
{
token: 'sidebar-bg',
cssVar: '--color-sidebar-bg',
dark: '#0C1528',
light: '#0C1528',
group: 'Invariants',
},
{
token: 'brand',
cssVar: '--color-brand',
dark: '#1B4FD8',
light: '#1B4FD8',
group: 'Invariants',
},
{
token: 'brand-hover',
cssVar: '--color-brand-hover',
dark: '#1744B8',
light: '#1744B8',
group: 'Invariants',
},
{
token: 'brand-active',
cssVar: '--color-brand-active',
dark: '#13379C',
light: '#13379C',
group: 'Invariants',
},
{
token: 'warning',
cssVar: '--color-warning',
dark: '#F59E0B',
light: '#F59E0B',
group: 'Invariants',
},
{
token: 'danger',
cssVar: '--color-danger',
dark: '#EF4444',
light: '#EF4444',
group: 'Invariants',
},
// Dual-theme (valeurs différentes dark/light)
{
token: 'canvas',
cssVar: '--color-canvas',
dark: '#111111',
light: '#F3F4F6',
group: 'Dark default',
},
{
token: 'surface',
cssVar: '--color-surface',
dark: 'rgba(255,255,255,.035)',
light: '#FFFFFF',
group: 'Dark default',
},
{
token: 'surface-hover',
cssVar: '--color-surface-hover',
dark: 'rgba(255,255,255,.055)',
light: '#F8F9FB',
group: 'Dark default',
},
{
token: 'surface-solid',
cssVar: '--color-surface-solid',
dark: '#1E1E1E',
light: '#FFFFFF',
group: 'Dark default',
},
{
token: 'surface-raised',
cssVar: '--color-surface-raised',
dark: '#222222',
light: '#FFFFFF',
group: 'Dark default',
},
{
token: 'border',
cssVar: '--color-border',
dark: 'rgba(255,255,255,.06)',
light: 'rgba(0,0,0,.07)',
group: 'Dark default',
},
{
token: 'border-strong',
cssVar: '--color-border-strong',
dark: 'rgba(255,255,255,.12)',
light: 'rgba(0,0,0,.14)',
group: 'Dark default',
},
{
token: 'ink-primary',
cssVar: '--color-ink-primary',
dark: '#E5E5E5',
light: '#0F0F1A',
group: 'Dark default',
},
{
token: 'ink-secondary',
cssVar: '--color-ink-secondary',
dark: 'rgba(255,255,255,.55)',
light: 'rgba(0,0,0,.55)',
group: 'Dark default',
},
{
token: 'ink-tertiary',
cssVar: '--color-ink-tertiary',
dark: 'rgba(255,255,255,.3)',
light: 'rgba(0,0,0,.3)',
group: 'Dark default',
},
{
token: 'brand-soft',
cssVar: '--color-brand-soft',
dark: 'rgba(27,79,216,.1)',
light: 'rgba(27,79,216,.06)',
group: 'Dark default',
},
{
token: 'brand-text',
cssVar: '--color-brand-text',
dark: '#7DA4F0',
light: '#1B4FD8',
group: 'Dark default',
},
{
token: 'success',
cssVar: '--color-success',
dark: '#4ADE80',
light: '#16A34A',
group: 'Dark default',
},
{
token: 'success-soft',
cssVar: '--color-success-soft',
dark: 'rgba(74,222,128,.12)',
light: 'rgba(22,163,74,.1)',
group: 'Dark default',
},
{
token: 'warning-soft',
cssVar: '--color-warning-soft',
dark: 'rgba(245,158,11,.12)',
light: 'rgba(245,158,11,.12)',
group: 'Dark default',
},
{
token: 'danger-soft',
cssVar: '--color-danger-soft',
dark: 'rgba(239,68,68,.12)',
light: 'rgba(239,68,68,.12)',
group: 'Dark default',
},
]
// ─── section wrapper ─────────────────────────────────────────────────────────
function Section({ title, children }: { title: string; children: React.ReactNode }) {
return (
<section className="space-y-4">
<h2 className="text-base font-semibold uppercase tracking-wider text-ink-secondary">
{title}
</h2>
<Separator />
{children}
</section>
)
}
// ─── main page ───────────────────────────────────────────────────────────────
export default function DesignSystemPage() {
const { theme, setTheme } = useTheme()
const [dialogOpen, setDialogOpen] = useState(false)
return (
<div className="min-h-screen space-y-14 bg-canvas px-6 py-10">
{/* ── header ── */}
<header className="flex items-center justify-between">
<div>
<h1 className="text-2xl font-bold text-ink-primary">Design System</h1>
<p className="mt-0.5 text-sm text-ink-secondary">
Expria DA Charcoal · dark-default + light override
</p>
</div>
<Button
variant="outline"
size="sm"
onClick={() => setTheme(theme === 'dark' ? 'light' : 'dark')}
>
{theme === 'dark' ? 'Light mode' : 'Dark mode'}
</Button>
</header>
{/* ── palette ── */}
<Section title="Palette">
<div className="grid grid-cols-2 gap-3 sm:grid-cols-3 lg:grid-cols-4">
{PALETTE.map(({ token, cssVar, light, dark }) => (
<div key={token} className="flex flex-col gap-1.5">
<div
className="h-12 w-full rounded-md border border-border shadow-card"
style={{ background: `var(${cssVar})` }}
/>
<div className="space-y-0.5">
<p className="font-mono text-xs font-medium text-ink-primary">{token}</p>
<p className="font-mono text-xs leading-tight text-ink-secondary"> {dark}</p>
<p className="font-mono text-xs leading-tight text-ink-secondary"> {light}</p>
</div>
</div>
))}
</div>
</Section>
{/* ── typography ── */}
<Section title="Typography">
<div className="space-y-3 rounded-lg border border-border bg-surface p-6">
<p className="text-4xl font-bold text-ink-primary">Display / 40px Bold</p>
<p className="text-2xl font-semibold text-ink-primary">Heading 1 / 24px Semibold</p>
<p className="text-xl font-semibold text-ink-primary">Heading 2 / 20px Semibold</p>
<p className="text-lg font-medium text-ink-primary">Heading 3 / 17px Medium</p>
<p className="text-base text-ink-primary">Body / 14px Regular Plus Jakarta Sans</p>
<p className="text-sm text-ink-secondary">Small / 13px Regular secondary copy</p>
<p className="text-xs text-ink-tertiary">Caption / 11px Regular labels, metadata</p>
<p className="font-mono text-xs text-ink-secondary">Mono / 11px token names, code</p>
</div>
</Section>
{/* ── buttons ── */}
<Section title="Button">
<div className="space-y-4 rounded-lg border border-border bg-surface p-6">
<div className="flex flex-wrap items-center gap-2">
<Button>Default</Button>
<Button variant="secondary">Secondary</Button>
<Button variant="outline">Outline</Button>
<Button variant="ghost">Ghost</Button>
<Button variant="destructive">Destructive</Button>
<Button variant="link">Link</Button>
</div>
<div className="flex flex-wrap items-center gap-2">
<Button size="lg">Large</Button>
<Button>Default</Button>
<Button size="sm">Small</Button>
<Button size="icon">+</Button>
</div>
<div className="flex flex-wrap items-center gap-2">
<Button disabled>Disabled</Button>
<Button variant="outline" disabled>
Outline disabled
</Button>
</div>
</div>
</Section>
{/* ── badges ── */}
<Section title="Badge">
<div className="flex flex-wrap gap-2 rounded-lg border border-border bg-surface p-6">
<Badge>Default</Badge>
<Badge variant="secondary">Secondary</Badge>
<Badge variant="outline">Outline</Badge>
<Badge variant="destructive">Destructive</Badge>
</div>
</Section>
{/* ── inputs / forms ── */}
<Section title="Input · Label · Progress · Separator">
<div className="max-w-md space-y-5 rounded-lg border border-border bg-surface p-6">
<div className="space-y-1.5">
<Label htmlFor="ds-email">Email</Label>
<Input id="ds-email" type="email" placeholder="you@expria.io" />
</div>
<div className="space-y-1.5">
<Label htmlFor="ds-pass">Password</Label>
<Input id="ds-pass" type="password" placeholder="••••••••" />
</div>
<div className="space-y-1.5">
<Label htmlFor="ds-invalid">Invalid field</Label>
<Input id="ds-invalid" aria-invalid="true" placeholder="Error state" />
</div>
<div className="space-y-1.5">
<Label>Progress 65%</Label>
<Progress value={65} />
</div>
<Separator />
<p className="text-sm text-ink-secondary">Content below separator</p>
</div>
</Section>
{/* ── avatar ── */}
<Section title="Avatar">
<div className="flex flex-wrap items-end gap-6 rounded-lg border border-border bg-surface p-6">
<div className="flex flex-col items-center gap-2">
<Avatar size="sm">
<AvatarImage src="" />
<AvatarFallback>HK</AvatarFallback>
</Avatar>
<span className="text-xs text-ink-secondary">sm</span>
</div>
<div className="flex flex-col items-center gap-2">
<Avatar>
<AvatarImage src="" />
<AvatarFallback>HK</AvatarFallback>
</Avatar>
<span className="text-xs text-ink-secondary">default</span>
</div>
<div className="flex flex-col items-center gap-2">
<Avatar size="lg">
<AvatarImage src="" />
<AvatarFallback>HK</AvatarFallback>
</Avatar>
<span className="text-xs text-ink-secondary">lg</span>
</div>
<div className="flex flex-col items-center gap-2">
<AvatarGroup>
{['AB', 'CD', 'EF'].map((initials) => (
<Avatar key={initials}>
<AvatarFallback>{initials}</AvatarFallback>
</Avatar>
))}
<AvatarGroupCount>+5</AvatarGroupCount>
</AvatarGroup>
<span className="text-xs text-ink-secondary">group</span>
</div>
</div>
</Section>
{/* ── dialog ── */}
<Section title="Dialog">
<div className="rounded-lg border border-border bg-surface p-6">
<Dialog open={dialogOpen} onOpenChange={setDialogOpen}>
<DialogTrigger asChild>
<Button variant="outline">Open dialog</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>Example dialog</DialogTitle>
<DialogDescription>
This dialog uses DA Charcoal tokens bg-surface-solid, border-border,
text-ink-secondary. Toggle the theme to see it adapt.
</DialogDescription>
</DialogHeader>
<DialogFooter showCloseButton>
<Button onClick={() => setDialogOpen(false)}>Confirm</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</div>
</Section>
</div>
)
}