# DESIGN_SYSTEM.md — Expria Frontend > **Document de référence — Version 2.0 — 24 avril 2026** > Source de vérité unique pour l'identité visuelle, les tokens de design et les primitives UI. > Toute décision de DA doit être consignée ici avant d'être implémentée. > **Remplace intégralement la v1.0 (Direction Boréal) du 17 avril 2026.** --- ## 1. Direction artistique — verrouillée **Nom :** Charcoal **Positionnement :** outil pro sérieux, premium sans scolaire, immersif sans austère. **Référence mentale :** Linear, Notion Desktop, Primo TCF — sidebar sombre permanente, contenu aéré. ### Parti pris fondateurs | Principe | Décision | |---|---| | Mode par défaut | **Dark** (charcoal chaud `#111111`) | | Mode clair | Activé — fond gris froid `#F3F4F6`, cartes blanches | | Détection thème | `prefers-color-scheme` au chargement, toggle manuel, persistance `localStorage` | | Sidebar | **Navy `#0C1528` permanent** — identique dark et light. C'est l'ancre visuelle de la marque. | | Fond principal (dark) | `#111111` avec deux halos bleus subtils (`radial-gradient` à 4–5% opacité) | | Fond principal (light) | `#F3F4F6` avec deux halos bleus très discrets (2–3% opacité) | | Bleu de marque | `#1B4FD8` **sacro-saint** — invariant entre les modes | | Bleu texte accent | `#7da4f0` en dark, `#1B4FD8` en light (lisibilité adaptée au fond) | | Surfaces (dark) | Semi-transparentes `rgba(255,255,255,0.035)` — jamais de gris opaque | | Surfaces (light) | Blanc pur `#FFFFFF` avec ombre subtile `shadow-card` | | Angles | Rayons généreux mais retenus : 8 / 12 / 16 px | | Ombres (dark) | Aucune — la bordure 1px et la transparence suffisent | | Ombres (light) | Minimales. `shadow-card` subtile sur les surfaces élevées | | Animations | 150–200 ms, `ease-out`, respect de `prefers-reduced-motion` | | Icônes | `lucide-react` pour les icônes standard. SVG inline dans `shared/ui/icons/` pour les icônes custom | | Typographie | Plus Jakarta Sans exclusivement (via Google Fonts, fallback système) | | Approche responsive | **Desktop-first** pour l'app (usage quotidien sur ordinateur). Mobile-first uniquement pour le funnel d'acquisition (landing, pricing, inscription) | ### Ce qu'on refuse explicitement - Gradients criards — le seul acceptable est le dégradé `accent → accent-dark` sur le CTA primaire. - Glassmorphism ou `backdrop-filter` généralisé — réservé à la topbar et à la bottom nav mobile. - Emojis dans les éléments interactifs ou les labels fonctionnels. - Ombres lourdes, "drop shadows" style Material Design 2. - Plus de 3 niveaux d'élévation visuelle (fond → surface → surface-raised → modal). - Toute police autre que Plus Jakarta Sans. - Les motifs SaaS génériques : illustrations 3D, dégradés violet-rose, glass blobs. - Fond blanc pur (`#FFFFFF`) en tant que fond de page — toujours `--color-canvas`. - Couleurs hexadécimales en dur dans les composants — toujours via token. --- ## 2. Tokens — `src/index.css` Remplacer intégralement le contenu actuel. Tailwind 4 lit automatiquement les tokens déclarés dans `@theme`. Les deux thèmes sont actifs dès maintenant. ```css @import 'tailwindcss'; @import url('https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@400;500;600;700;800&display=swap'); @theme { /* ══════════════════════════════════════════════════════════════════════ INVARIANTS — identiques dark et light ══════════════════════════════════════════════════════════════════════ */ /* ── Sidebar (navy permanent) ── */ --color-sidebar-bg: #0C1528; --color-sidebar-border: rgba(255, 255, 255, 0.07); --color-sidebar-text: rgba(255, 255, 255, 0.6); --color-sidebar-text-hover: rgba(255, 255, 255, 0.9); --color-sidebar-text-active: #ffffff; --color-sidebar-nav-hover: rgba(255, 255, 255, 0.07); --color-sidebar-nav-active: rgba(255, 255, 255, 0.1); --color-sidebar-section-label: rgba(255, 255, 255, 0.3); /* ── Brand ── */ --color-brand: #1B4FD8; --color-brand-hover: #1744B8; --color-brand-active: #13379C; --color-brand-dark: #1740b0; --color-brand-ink: #FFFFFF; /* ── Semantic ── */ --color-warning: #f59e0b; --color-warning-soft: rgba(245, 158, 11, 0.12); --color-danger: #ef4444; --color-danger-soft: rgba(239, 68, 68, 0.12); /* ── Typography ── */ --font-sans: "Plus Jakarta Sans", system-ui, -apple-system, "Segoe UI", sans-serif; --font-mono: ui-monospace, "SF Mono", "JetBrains Mono", Menlo, monospace; --text-xs: 11px; --text-sm: 13px; --text-base: 14px; --text-md: 15px; --text-lg: 17px; --text-xl: 20px; --text-2xl: 24px; --text-3xl: 32px; --text-display: 40px; /* ── Rayons ── */ --radius-xs: 6px; --radius-sm: 8px; --radius-md: 12px; --radius-lg: 16px; --radius-xl: 20px; --radius-pill: 999px; /* ── Focus ── */ --shadow-focus: 0 0 0 3px rgba(27, 79, 216, 0.18); /* ══════════════════════════════════════════════════════════════════════ DARK MODE (default) — tokens de contenu ══════════════════════════════════════════════════════════════════════ */ --color-canvas: #111111; --color-surface: rgba(255, 255, 255, 0.035); --color-surface-hover: rgba(255, 255, 255, 0.055); --color-surface-solid: #1e1e1e; --color-surface-raised: #222222; --color-border: rgba(255, 255, 255, 0.06); --color-border-strong: rgba(255, 255, 255, 0.12); --color-ink-primary: #e5e5e5; --color-ink-secondary: rgba(255, 255, 255, 0.55); --color-ink-tertiary: rgba(255, 255, 255, 0.3); --color-ink-inverse: #111111; --color-brand-soft: rgba(27, 79, 216, 0.1); --color-brand-text: #7da4f0; --color-success: #4ade80; --color-success-soft: rgba(74, 222, 128, 0.12); --color-topbar-bg: rgba(17, 17, 17, 0.88); --color-gradient-a: rgba(27, 79, 216, 0.05); --color-gradient-b: rgba(27, 79, 216, 0.03); --shadow-card: none; --shadow-raised: none; } /* ══════════════════════════════════════════════════════════════════════ LIGHT MODE — override .light sur
══════════════════════════════════════════════════════════════════════ */ .light { --color-canvas: #F3F4F6; --color-surface: #ffffff; --color-surface-hover: #f8f9fb; --color-surface-solid: #ffffff; --color-surface-raised: #ffffff; --color-border: rgba(0, 0, 0, 0.07); --color-border-strong: rgba(0, 0, 0, 0.14); --color-ink-primary: #0f0f1a; --color-ink-secondary: rgba(0, 0, 0, 0.55); --color-ink-tertiary: rgba(0, 0, 0, 0.3); --color-ink-inverse: #ffffff; --color-brand-soft: rgba(27, 79, 216, 0.06); --color-brand-text: #1B4FD8; --color-success: #16a34a; --color-success-soft: rgba(22, 163, 74, 0.1); --color-topbar-bg: rgba(243, 244, 246, 0.88); --color-gradient-a: rgba(27, 79, 216, 0.025); --color-gradient-b: rgba(27, 79, 216, 0.01); --shadow-card: 0 1px 2px rgba(15, 18, 32, 0.04), 0 1px 8px rgba(15, 18, 32, 0.03); --shadow-raised: 0 4px 16px rgba(15, 18, 32, 0.06), 0 1px 2px rgba(15, 18, 32, 0.04); } /* ── Globals ── */ html, body { background: var(--color-canvas); color: var(--color-ink-primary); font-family: var(--font-sans); -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; font-variant-numeric: tabular-nums; } *:focus-visible { outline: none; box-shadow: var(--shadow-focus); border-radius: var(--radius-xs); } @media (prefers-reduced-motion: reduce) { *, *::before, *::after { animation-duration: 0ms !important; transition-duration: 0ms !important; } } ``` ### Règles d'usage des tokens 1. **Aucune valeur hexadécimale en dur** dans les composants. Toute couleur passe par un token `var(--color-*)`. 2. **Nommage sémantique obligatoire.** On écrit `bg-[var(--color-surface)]`, pas `bg-white` ni `bg-gray-50`. 3. **Ne jamais utiliser** `bg-white`, `bg-gray-*`, `text-gray-*` — ces classes Tailwind cassent le dual-theme. 4. Si un cas d'usage exige une teinte hors charte, **le documenter ici avant de l'ajouter**. Pas de token orphelin. 5. La sidebar utilise ses propres tokens `--color-sidebar-*` — ils ne changent **jamais** entre les modes. 6. Le fond principal utilise toujours les deux `radial-gradient` subtils — jamais un aplat uni. --- ## 3. Gestion du thème — `src/shared/lib/theme.ts` ```typescript export type Theme = 'dark' | 'light'; export function getInitialTheme(): Theme { const stored = localStorage.getItem('expria-theme') as Theme | null; if (stored === 'dark' || stored === 'light') return stored; if (window.matchMedia('(prefers-color-scheme: light)').matches) return 'light'; return 'dark'; } export function applyTheme(theme: Theme): void { document.documentElement.classList.toggle('light', theme === 'light'); } export function persistTheme(theme: Theme): void { localStorage.setItem('expria-theme', theme); } ``` **Script anti-FOUC** — à insérer inline dans `` de `index.html` : ```html ``` --- ## 4. Typographie | Usage | Taille | Poids | Tracking | Ligne | Token | |---|---|---|---|---|---| | Display (NCLC hero) | 40px | 800 | -0.02em | 1.0 | `text-display` | | H1 page | 32px | 700 | -0.02em | 1.1 | `text-3xl` | | H2 section | 24px | 700 | -0.015em | 1.2 | `text-2xl` | | H3 card title | 20px | 700 | -0.01em | 1.3 | `text-xl` | | Lead / intro | 17px | 500 | -0.005em | 1.5 | `text-lg` | | Body | 14px | 400 | 0 | 1.6 | `text-base` | | Body renforcé | 15px | 500 | 0 | 1.55 | `text-md` | | Small / meta | 13px | 500 | 0 | 1.5 | `text-sm` | | Eyebrow / label | 11px | 600 | 0.1em (uppercase) | 1.4 | `text-xs` | **Règles :** - Tout nombre métier (score 16/20, NCLC 7,5, compteur 3/5) est rendu en `font-variant-numeric: tabular-nums`. - `Plus Jakarta Sans` chargée via Google Fonts CDN avec fallback système. - Les chiffres français utilisent la **virgule** comme séparateur décimal (`7,5`, jamais `7.5`). --- ## 5. Primitives UI À créer dans `src/shared/ui/` en FSD, une primitive par dossier (`button/`, `card/`, etc.) avec `index.ts` pour l'export. ### Inventaire | Composant | Variants | Usage | |---|---|---| | `Button` | `primary` / `secondary` / `ghost` / `upgrade` | CTA, actions tertiaires | | `Card` | `default` / `raised` / `interactive` | Cadre métriques, item simulation | | `MetricCard` | `default` / `hero` | Bloc NCLC, compteur simulations | | `ProgressBar` | `default` | Progression vers NCLC 9 | | `Badge` | `plan` / `nclc` / `brand` / `success` / `warning` / `danger` | Plan, niveau, chips sémantiques | | `Sidebar` | — | Nav desktop (≥ 1024px), navy permanent | | `BottomNav` | — | Nav mobile (< 1024px), 4–5 items max | | `ThemeToggle` | — | Bouton soleil/lune dans le footer sidebar | | `PageHeader` | — | Greeting + plan badge | | `SectionHeader` | — | Titre de section + action optionnelle | ### Patterns de référence — copier, ne pas réinterpréter **Sidebar NavItem actif :** ```tsx {isActive && ( )}