feat(design-system): page /design-system dev-only — palette live + composants (Sprint 0.5 étape 7)

This commit is contained in:
Hermann_Kitio 2026-04-18 01:25:54 +03:00
parent 9a4e22b533
commit 7dfd0df6b3
2 changed files with 263 additions and 0 deletions

View file

@ -1,9 +1,24 @@
import React, { Suspense } from 'react'
import { Routes, Route } from 'react-router-dom'
const DesignSystemPage = import.meta.env.DEV
? React.lazy(() => import('@/features/design-system/DesignSystemPage'))
: () => null
export function AppRouter() {
return (
<Routes>
<Route path="/" element={<ScaffoldPlaceholder />} />
{import.meta.env.DEV && (
<Route
path="/design-system"
element={
<Suspense fallback={<div className="p-6 text-ink-4">Loading</div>}>
<DesignSystemPage />
</Suspense>
}
/>
)}
</Routes>
)
}

View file

@ -0,0 +1,248 @@
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 ────────────────────────────────────────────────────────────
const PALETTE: { token: string; var: string; light: string; dark: string }[] = [
{ token: 'canvas', var: '--color-canvas', light: '#EEF2F8', dark: '#0D1220' },
{ token: 'canvas-2', var: '--color-canvas-2', light: '#E6EBF4', dark: '#121A2D' },
{ token: 'surface', var: '--color-surface', light: '#FFFFFF', dark: '#182238' },
{ token: 'surface-hover',var: '--color-surface-hover',light: '#F8FAFD', dark: '#1E2A42' },
{ token: 'line', var: '--color-line', light: '#DDE3ED', dark: '#27324B' },
{ token: 'line-strong', var: '--color-line-strong', light: '#C7D0E0', dark: '#364363' },
{ token: 'ink-1', var: '--color-ink-1', light: '#0F172A', dark: '#F1F4FA' },
{ token: 'ink-2', var: '--color-ink-2', light: '#1E293B', dark: '#DDE3EF' },
{ token: 'ink-3', var: '--color-ink-3', light: '#475569', dark: '#A8B2C7' },
{ token: 'ink-4', var: '--color-ink-4', light: '#64748B', dark: '#7A8499' },
{ token: 'ink-5', var: '--color-ink-5', light: '#94A3B8', dark: '#525C73' },
{ token: 'expria', var: '--color-expria', light: '#1B4FD8', dark: '#5B7FFF' },
{ token: 'expria-hover', var: '--color-expria-hover', light: '#1741B8', dark: '#6F8EFF' },
{ token: 'expria-50', var: '--color-expria-50', light: '#EEF3FF', dark: 'rgba(91,127,255,.12)' },
{ token: 'expria-100', var: '--color-expria-100', light: '#DCE6FF', dark: '—' },
{ token: 'expria-200', var: '--color-expria-200', light: '#B8CDFF', dark: '—' },
{ token: 'deep', var: '--color-deep', light: '#0B1F5C', dark: '#060B1A' },
{ token: 'success', var: '--color-success', light: '#0E9F6E', dark: '#3DD68C' },
{ token: 'success-bg', var: '--color-success-bg', light: '#E6F6F0', dark: 'rgba(61,214,140,.12)' },
{ token: 'warning', var: '--color-warning', light: '#C77A00', dark: '#F5B849' },
{ token: 'warning-bg', var: '--color-warning-bg', light: '#FEF3E2', dark: 'rgba(245,184,73,.12)' },
{ token: 'danger', var: '--color-danger', light: '#C53030', dark: '#F06B6B' },
{ token: 'danger-bg', var: '--color-danger-bg', light: '#FDECEC', dark: 'rgba(240,107,107,.12)' },
]
// ─── section wrapper ─────────────────────────────────────────────────────────
function Section({ title, children }: { title: string; children: React.ReactNode }) {
return (
<section className="space-y-4">
<h2 className="text-base font-semibold text-ink-3 uppercase tracking-wider">{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 bg-canvas px-6 py-10 space-y-14">
{/* ── header ── */}
<header className="flex items-center justify-between">
<div>
<h1 className="text-2xl font-bold text-ink-1">Design System</h1>
<p className="text-sm text-ink-4 mt-0.5">Expria Direction H palette · Sprint 0.5</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 sm:grid-cols-3 lg:grid-cols-4 gap-3">
{PALETTE.map(({ token, var: cssVar, light, dark }) => (
<div key={token} className="flex flex-col gap-1.5">
<div
className="h-12 w-full rounded-md border border-line shadow-sm"
style={{ background: `var(${cssVar})` }}
/>
<div className="space-y-0.5">
<p className="text-xs font-mono font-medium text-ink-2">{token}</p>
<p className="text-xs font-mono text-ink-4 leading-tight">
{light}
</p>
<p className="text-xs font-mono text-ink-4 leading-tight">
{dark}
</p>
</div>
</div>
))}
</div>
</Section>
{/* ── typography ── */}
<Section title="Typography">
<div className="space-y-3 bg-surface rounded-lg p-6 border border-line">
<p className="text-4xl font-bold text-ink-1">Display / 36px Bold</p>
<p className="text-2xl font-semibold text-ink-1">Heading 1 / 24px Semibold</p>
<p className="text-xl font-semibold text-ink-1">Heading 2 / 20px Semibold</p>
<p className="text-lg font-medium text-ink-2">Heading 3 / 18px Medium</p>
<p className="text-base text-ink-2">Body / 16px Regular Plus Jakarta Sans</p>
<p className="text-sm text-ink-3">Small / 14px Regular secondary copy</p>
<p className="text-xs text-ink-4">Caption / 12px Regular labels, metadata</p>
<p className="text-xs font-mono text-ink-3">Mono / 12px token names, code</p>
</div>
</Section>
{/* ── buttons ── */}
<Section title="Button">
<div className="space-y-4 bg-surface rounded-lg p-6 border border-line">
<div className="flex flex-wrap gap-2 items-center">
<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 gap-2 items-center">
<Button size="lg">Large</Button>
<Button>Default</Button>
<Button size="sm">Small</Button>
<Button size="icon">+</Button>
</div>
<div className="flex flex-wrap gap-2 items-center">
<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 bg-surface rounded-lg p-6 border border-line">
<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="space-y-5 bg-surface rounded-lg p-6 border border-line max-w-md">
<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-4">Content below separator</p>
</div>
</Section>
{/* ── avatar ── */}
<Section title="Avatar">
<div className="flex flex-wrap items-end gap-6 bg-surface rounded-lg p-6 border border-line">
<div className="flex flex-col items-center gap-2">
<Avatar size="sm">
<AvatarImage src="" />
<AvatarFallback>HK</AvatarFallback>
</Avatar>
<span className="text-xs text-ink-4">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-4">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-4">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-4">group</span>
</div>
</div>
</Section>
{/* ── dialog ── */}
<Section title="Dialog">
<div className="bg-surface rounded-lg p-6 border border-line">
<Dialog open={dialogOpen} onOpenChange={setDialogOpen}>
<DialogTrigger asChild>
<Button variant="outline">Open dialog</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>Example dialog</DialogTitle>
<DialogDescription>
This dialog uses Direction H tokens bg-surface, border-line, text-ink-4.
Toggle the theme to see it adapt.
</DialogDescription>
</DialogHeader>
<DialogFooter showCloseButton>
<Button onClick={() => setDialogOpen(false)}>Confirm</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</div>
</Section>
</div>
)
}