- 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>
389 lines
12 KiB
TypeScript
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>
|
|
)
|
|
}
|