13 KiB
DESIGN_SYSTEM.md — Expria Frontend
Document de référence — Version 1.0 — Sprint 1 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.
1. Direction artistique — verrouillée
Nom : Boréal Positionnement : institutionnel chaleureux, premium sans flashy, sérieux sans austère. Référence mentale : Stripe Dashboard, Linear, Notion Desktop — mais réchauffé d'un cran.
Parti pris fondateurs
| Principe | Décision |
|---|---|
| Mode canonique Sprint 1 | Clair uniquement (light chaud) |
| Mode sombre | Prévu Sprint 2+ (tokens écrits dual-theme-ready dès J1) |
| Fond principal | #F4F2EC (off-white calibré, ni froid ni saturé) |
| Surfaces élevées | Blanc pur #FFFFFF pour contraste subtil avec le fond |
| Bleu de marque | #1B4FD8 sacro-saint en mode clair — aucune variation |
| Bleu mode sombre | #7C9BFF prévu pour Sprint 2+ (pattern Apple system colors) |
| Accent chaleureux | Aucun en Sprint 1 — le bleu porte toute l'intentionnalité |
| Angles | Rayons généreux mais retenus : 8 / 12 / 16 px |
| Ombres | Minimales. 1 ombre-card unique, très subtile. Hairlines 1px privilégiées. |
| Animations | 150–200 ms, ease-out, respect de prefers-reduced-motion |
| Icônes | SVG inline dans shared/ui/icons/ — aucune dépendance externe |
| Typographie | Plus Jakarta Sans (via font-family, fallback système) |
Ce qu'on refuse explicitement
- Gradients criards (le seul acceptable : aucun).
- Glassmorphism ou
backdrop-filtergénéralisé — réservé à la bottom nav mobile si besoin. - Emojis dans les éléments interactifs ou les labels fonctionnels.
- Ombres lourdes, "drop shadows" style Material Design 2.
- Plus de 2 niveaux d'élévation visuelle (fond → card → modal).
- Toute police de display fantaisiste, serif décorative ou condensée.
- Les motifs SaaS génériques : illustrations 3D, dégradés violet-rose, glass blobs.
2. Tokens — src/index.css
Remplacer intégralement le contenu actuel (@import 'tailwindcss';) par le bloc ci-dessous. Tailwind 4 lit automatiquement les tokens déclarés dans @theme.
@import 'tailwindcss';
@theme {
/* ----- Brand ------------------------------------------------------- */
--color-brand: #1B4FD8;
--color-brand-hover: #1744B8;
--color-brand-active: #13379C;
--color-brand-soft: #E7EDFC;
--color-brand-ink: #FFFFFF;
/* ----- Surfaces (light — Sprint 1) --------------------------------- */
--color-bg: #F4F2EC;
--color-surface: #FBFAF6;
--color-surface-raised: #FFFFFF;
--color-surface-sunken: #EEECE4;
/* ----- Ink (texte) ------------------------------------------------- */
--color-ink-primary: #0F1220;
--color-ink-secondary: #4A4F5E;
--color-ink-tertiary: #8A8F9E;
--color-ink-inverse: #FBFAF6;
/* ----- Borders & dividers ------------------------------------------ */
--color-border: #E3E0D6;
--color-border-strong: #C9C5B7;
--color-border-focus: #1B4FD8;
/* ----- Feedback ---------------------------------------------------- */
--color-success: #1F7A4C;
--color-success-soft: #E3F2EA;
--color-warning: #B8741A;
--color-warning-soft: #F7EEDF;
--color-danger: #B8322D;
--color-danger-soft: #F7E1DF;
/* ----- Typographie ------------------------------------------------- */
--font-sans: "Plus Jakarta Sans", system-ui, -apple-system, "Segoe UI", sans-serif;
--font-mono: ui-monospace, "SF Mono", "JetBrains Mono", Menlo, monospace;
/* Échelle : mobile-first, les tailles desktop se gèrent via utilities Tailwind */
--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;
/* ----- Ombres ------------------------------------------------------ */
--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);
--shadow-focus: 0 0 0 3px rgba(27, 79, 216, 0.18);
}
/* --------------------------------------------------------------------- */
/* Globals — reset minimal, fond chaud par défaut */
/* --------------------------------------------------------------------- */
html, body {
background: var(--color-bg);
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;
}
}
/* --------------------------------------------------------------------- */
/* TODO Sprint 2+ — Dark theme (Cadence recalibré) */
/* --------------------------------------------------------------------- */
/*
@theme {
--color-bg-dark: #0F1320;
--color-surface-dark: #171B2B;
--color-surface-raised-dark: #1E2338;
--color-ink-primary-dark: #E6E4DB;
--color-ink-secondary-dark: #9CA0AC;
--color-brand-dark: #7C9BFF;
--color-brand-hover-dark: #9AB3FF;
--color-border-dark: #2A3048;
}
*/
Règles d'usage des tokens
- Aucune valeur hexadécimale en dur dans les composants. Toute couleur passe par un token.
- Nommage sémantique obligatoire. On écrit
bg-surface, pasbg-gray-50. - Si un cas d'usage exige une teinte hors charte, le documenter ici avant de l'ajouter. Pas de token orphelin.
- Les tokens marqués
*-darkne sont pas utilisés en Sprint 1. Leur présence en commentaire est intentionnelle pour faciliter la reprise Sprint 2+.
3. Typographie
| Usage | Taille | Poids | Tracking | Ligne | Token |
|---|---|---|---|---|---|
| Display (NCLC hero) | 40px | 700 | -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 | 400 | 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. Hérité par le body, mais à re-spécifier explicitement sur les tables et listes. Plus Jakarta Sansest déclarée enfont-familyavec fallback système. Aucune webfont chargée tant qu'on n'a pas validé la stratégie self-hosting (décision reportée).- Les chiffres français utilisent la virgule comme séparateur décimal (
7,5, jamais7.5).
4. Primitives UI — Sprint 1
À créer dans src/shared/ui/ en FSD, une primitive par dossier (button/, card/, etc.) avec index.ts pour l'export.
Inventaire minimal
| Composant | Variants | Usage dashboard |
|---|---|---|
Button |
primary / secondary / ghost / upgrade |
CTA "Nouvelle simulation", "Passer au plan Standard", actions tertiaires |
Card |
default / raised / interactive |
Cadre métriques, item simulation, recommandation |
MetricCard |
default / hero (pour le NCLC) |
Bloc NCLC, compteur simulations, dernier score |
ProgressBar |
default |
Progression vers NCLC 9 |
Badge |
plan / nclc / neutral |
Plan actuel dans header, niveau NCLC par simulation |
Sidebar |
— | Nav desktop (≥ 1024px) |
BottomNav |
— | Nav mobile (< 1024px), 4 items max |
PageHeader |
— | Greeting + plan pill |
SectionHeader |
— | Titre de section + action optionnelle |
Règles d'implémentation
- Chaque primitive accepte
classNameen plus de ses props typées, pour overrides ponctuels. - Chaque primitive expose ses props via un type exporté (
ButtonProps,CardProps, etc.). - Aucune primitive ne contient de logique métier ou d'appel API. Elles reçoivent tout par props.
- Les icônes sont passées par une prop
iconacceptant unReactNode, jamais par nom de string.
5. Layout dashboard — spécification
Les primitives ci-dessus s'assemblent dans src/features/dashboard/ et src/pages/dashboard/.
Structure sémantique
<body>
<Sidebar /> (≥ 1024px)
<main>
<PageHeader /> (greeting + plan)
<section>
<MetricCard hero /> (NCLC estimé + progression)
<MetricCard /> (simulations restantes)
<MetricCard /> (dernier score)
</section>
<Button primary /> (Nouvelle simulation)
<Button upgrade /> (Passer au plan Standard)
<section>
<SectionHeader /> (3 dernières simulations)
<Card interactive /> × 3
</section>
<section>
<SectionHeader /> (Prochaine étape recommandée)
<Card raised />
</section>
</main>
<BottomNav /> (< 1024px)
</body>
Breakpoints
| Breakpoint | Comportement |
|---|---|
< 1024px |
Mono-colonne, BottomNav fixe en bas, padding horizontal 20px |
≥ 1024px |
Sidebar 240px + contenu centré 860px max, padding horizontal 32px |
≥ 1440px |
Contenu centré 920px max (pas d'élargissement excessif) |
Densité verticale
- Padding vertical section : 24px mobile, 32px desktop.
- Gap inter-cards : 12px mobile, 16px desktop.
- Marge sous
PageHeader: 20px mobile, 28px desktop.
6. Données mock — Sprint 1
Avant branchement API, fournir les données via src/shared/api/mock/dashboard.ts. Données crédibles, françaises, alignées sur l'audience réelle.
export const mockDashboard = {
user: {
firstName: 'Yacine',
plan: 'decouverte' as const,
planLabel: 'Plan Découverte',
},
metrics: {
nclcEstimated: 7.5,
nclcTarget: 9,
simulationsUsed: 2,
simulationsQuota: 5,
lastScore: { value: 16, max: 20, type: 'ecrit' as const },
},
recentSimulations: [
{ id: 's-001', type: 'ecrit', relativeDate: 'il y a 2 jours', score: 16, max: 20, nclc: 8 },
{ id: 's-002', type: 'oral', relativeDate: 'il y a 5 jours', score: 14, max: 20, nclc: 7 },
{ id: 's-003', type: 'ecrit', relativeDate: 'il y a 1 semaine', score: 15, max: 20, nclc: 7 },
],
nextStep: {
title: 'Cible une simulation orale cette semaine',
body: 'Ton écrit est solide (NCLC 8). L\'oral reste à consolider pour sécuriser ton NCLC 9.',
action: { label: 'Démarrer Expression Orale', to: '/simulation/orale' },
},
} as const;
Règles contenu :
- Aucun "Lorem ipsum", aucune date absolue — relatif uniquement (
il y a X jours). - Les prénoms mocks reflètent l'audience : Yacine, Aminata, Kenza, Bilal, Fatou, Kévin (Canada).
- Les scores suivent une progression crédible (pas de 20/20 ni de 5/20).
7. Accessibilité — plancher Sprint 1
- Contraste minimum WCAG AA sur tous les couples texte/fond (vérifié pour la palette ci-dessus).
- Tous les éléments interactifs ont un
:focus-visibleavec--shadow-focus(halo bleu 3px). - Les icônes purement décoratives portent
aria-hidden="true". - Les icônes fonctionnelles (sans label visible) portent
aria-label. - Les landmarks sémantiques sont utilisés :
<header>,<nav>,<main>,<section>. - Le
BottomNavmobile respecte la hauteur minimale tap target : 44×44 px par item.
8. Dépendances externes — Sprint 1
Aucune nouvelle dépendance UI n'est ajoutée en Sprint 1. On n'installe pas shadcn/ui, pas lucide-react, pas radix-ui. Les primitives sont écrites à la main pour deux raisons :
- On veut maîtriser exactement chaque composant (pas d'override Tailwind contre Radix).
- Volume fonctionnel Sprint 1 minimal (~9 primitives) : pas d'économie à externaliser.
La porte reste ouverte Sprint 2+ pour radix-ui sur les primitives complexes (Dialog, Popover, Dropdown) si besoin justifié dans un ADR.
9. Journal des décisions DA
| Date | Décision | Contexte |
|---|---|---|
| 2026-04-17 | Direction A (Boréal) validée comme canonique | 5 directions explorées (A/B/E/F/G), A et B retenues, A choisie comme base |
| 2026-04-17 | Fond #F4F2EC (V1 calibré) retenu |
Test côte-à-côte contre #F5F3ED et #F2EFE6 |
| 2026-04-17 | Dark mode (Cadence) reporté Sprint 2+ | Tokens écrits dual-theme-ready dès J1 pour éviter réécriture |
| 2026-04-17 | Bleu #1B4FD8 sacro-saint en light, #7C9BFF prévu pour dark |
Pattern Apple system colors |
| 2026-04-17 | Pas de shadcn/ui en Sprint 1 | Volume faible, maîtrise totale souhaitée |
10. Hors périmètre Sprint 1
Éléments explicitement reportés :
- Dark mode applicatif.
- Thème haut-contraste (WCAG AAA).
- Internationalisation (i18n) — Sprint 1 monolingue FR.
- Animations avancées (scroll-linked, shared element transitions).
- Illustrations personnalisées / iconographie signature.
- Self-hosting de la font Plus Jakarta Sans (on reste en fallback système Sprint 1).
Chacun de ces points mérite un ADR dédié quand il sera abordé.