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>
This commit is contained in:
parent
407d1bd134
commit
b68f160bce
61 changed files with 1269 additions and 726 deletions
|
|
@ -1,91 +1,94 @@
|
|||
# DESIGN_SYSTEM.md — Expria Frontend
|
||||
|
||||
> **Document de référence — Version 1.0 — Sprint 1**
|
||||
> **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 :** 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.
|
||||
**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 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é |
|
||||
| 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 | Minimales. 1 ombre-card unique, très subtile. Hairlines 1px privilégiées. |
|
||||
| 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 | SVG inline dans `shared/ui/icons/` — aucune dépendance externe |
|
||||
| Typographie | Plus Jakarta Sans (via `font-family`, fallback système) |
|
||||
| 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 : aucun).
|
||||
- Glassmorphism ou `backdrop-filter` généralisé — réservé à la bottom nav mobile si besoin.
|
||||
- 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 2 niveaux d'élévation visuelle (fond → card → modal).
|
||||
- Toute police de display fantaisiste, serif décorative ou condensée.
|
||||
- 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 (`@import 'tailwindcss';`) par le bloc ci-dessous. Tailwind 4 lit automatiquement les tokens déclarés dans `@theme`.
|
||||
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 {
|
||||
/* ----- Brand ------------------------------------------------------- */
|
||||
/* ══════════════════════════════════════════════════════════════════════
|
||||
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-soft: #E7EDFC;
|
||||
--color-brand-dark: #1740b0;
|
||||
--color-brand-ink: #FFFFFF;
|
||||
|
||||
/* ----- Surfaces (light — Sprint 1) --------------------------------- */
|
||||
--color-bg: #F4F2EC;
|
||||
--color-surface: #FBFAF6;
|
||||
--color-surface-raised: #FFFFFF;
|
||||
--color-surface-sunken: #EEECE4;
|
||||
/* ── 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);
|
||||
|
||||
/* ----- 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 ------------------------------------------------- */
|
||||
/* ── Typography ── */
|
||||
--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;
|
||||
|
|
@ -96,7 +99,7 @@ Remplacer intégralement le contenu actuel (`@import 'tailwindcss';`) par le blo
|
|||
--text-3xl: 32px;
|
||||
--text-display: 40px;
|
||||
|
||||
/* ----- Rayons ------------------------------------------------------ */
|
||||
/* ── Rayons ── */
|
||||
--radius-xs: 6px;
|
||||
--radius-sm: 8px;
|
||||
--radius-md: 12px;
|
||||
|
|
@ -104,18 +107,76 @@ Remplacer intégralement le contenu actuel (`@import 'tailwindcss';`) par le blo
|
|||
--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);
|
||||
/* ── 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;
|
||||
}
|
||||
|
||||
/* --------------------------------------------------------------------- */
|
||||
/* Globals — reset minimal, fond chaud par défaut */
|
||||
/* --------------------------------------------------------------------- */
|
||||
/* ══════════════════════════════════════════════════════════════════════
|
||||
LIGHT MODE — override .light sur <body>
|
||||
══════════════════════════════════════════════════════════════════════ */
|
||||
|
||||
.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-bg);
|
||||
background: var(--color-canvas);
|
||||
color: var(--color-ink-primary);
|
||||
font-family: var(--font-sans);
|
||||
-webkit-font-smoothing: antialiased;
|
||||
|
|
@ -135,119 +196,201 @@ html, body {
|
|||
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
|
||||
|
||||
1. **Aucune valeur hexadécimale en dur** dans les composants. Toute couleur passe par un token.
|
||||
2. **Nommage sémantique obligatoire.** On écrit `bg-surface`, pas `bg-gray-50`.
|
||||
3. Si un cas d'usage exige une teinte hors charte, **le documenter ici avant de l'ajouter**. Pas de token orphelin.
|
||||
4. Les tokens marqués `*-dark` ne sont **pas utilisés en Sprint 1**. Leur présence en commentaire est intentionnelle pour faciliter la reprise Sprint 2+.
|
||||
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. Typographie
|
||||
## 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 `<head>` de `index.html` :
|
||||
|
||||
```html
|
||||
<script>
|
||||
(function(){
|
||||
var t = localStorage.getItem('expria-theme');
|
||||
if (!t) t = window.matchMedia('(prefers-color-scheme: light)').matches ? 'light' : 'dark';
|
||||
if (t === 'light') document.documentElement.classList.add('light');
|
||||
})();
|
||||
</script>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. Typographie
|
||||
|
||||
| Usage | Taille | Poids | Tracking | Ligne | Token |
|
||||
|---|---|---|---|---|---|
|
||||
| Display (NCLC hero) | 40px | 700 | -0.02em | 1.0 | `text-display` |
|
||||
| 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 | 400 | 0 | 1.5 | `text-sm` |
|
||||
| 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`. Hérité par le body, mais à re-spécifier explicitement sur les tables et listes.
|
||||
- `Plus Jakarta Sans` est déclarée en `font-family` avec fallback système. **Aucune webfont chargée** tant qu'on n'a pas validé la stratégie self-hosting (décision reportée).
|
||||
- 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`).
|
||||
|
||||
---
|
||||
|
||||
## 4. Primitives UI — Sprint 1
|
||||
## 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 minimal
|
||||
### Inventaire
|
||||
|
||||
| Composant | Variants | Usage dashboard |
|
||||
| Composant | Variants | Usage |
|
||||
|---|---|---|
|
||||
| `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 |
|
||||
| `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` / `neutral` | Plan actuel dans header, niveau NCLC par simulation |
|
||||
| `Sidebar` | — | Nav desktop (≥ 1024px) |
|
||||
| `BottomNav` | — | Nav mobile (< 1024px), 4 items max |
|
||||
| `PageHeader` | — | Greeting + plan pill |
|
||||
| `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
|
||||
<Link
|
||||
className={cn(
|
||||
'relative flex items-center gap-2.5 px-2.5 py-2 rounded-lg',
|
||||
'text-[13px] font-medium transition-colors',
|
||||
isActive
|
||||
? 'bg-[var(--color-sidebar-nav-active)] text-white font-semibold'
|
||||
: 'text-[var(--color-sidebar-text)] hover:bg-[var(--color-sidebar-nav-hover)] hover:text-[var(--color-sidebar-text-hover)]'
|
||||
)}
|
||||
>
|
||||
{isActive && (
|
||||
<span className="absolute left-0 top-[20%] bottom-[20%] w-[3px]
|
||||
rounded-r bg-[var(--color-brand)]" />
|
||||
)}
|
||||
<Icon className={cn('w-4 h-4 shrink-0', isActive ? 'opacity-100' : 'opacity-60')} />
|
||||
{label}
|
||||
</Link>
|
||||
```
|
||||
|
||||
**Card :**
|
||||
```tsx
|
||||
<div className={cn(
|
||||
'rounded-[var(--radius-md)] border border-[var(--color-border)]',
|
||||
'bg-[var(--color-surface)] p-[18px] transition-colors',
|
||||
'shadow-[var(--shadow-card)]',
|
||||
)}>
|
||||
{children}
|
||||
</div>
|
||||
```
|
||||
|
||||
**Bouton CTA primaire :**
|
||||
```tsx
|
||||
<button className="w-full py-3.5 rounded-[var(--radius-md)]
|
||||
bg-gradient-to-br from-[var(--color-brand)] to-[var(--color-brand-dark)]
|
||||
text-white font-bold text-sm
|
||||
shadow-[0_4px_20px_rgba(27,79,216,0.15)]
|
||||
hover:translate-y-[-1px] hover:shadow-[0_6px_28px_rgba(27,79,216,0.25)]
|
||||
transition-all">
|
||||
Nouvelle simulation
|
||||
</button>
|
||||
```
|
||||
|
||||
**Bouton secondaire :**
|
||||
```tsx
|
||||
<button className="w-full py-2.5 rounded-[var(--radius-sm)]
|
||||
border border-[var(--color-border)] bg-transparent
|
||||
text-[var(--color-ink-secondary)] text-[13px] font-semibold
|
||||
hover:border-[var(--color-brand)] hover:text-[var(--color-brand-text)]
|
||||
hover:bg-[var(--color-brand-soft)]
|
||||
transition-all">
|
||||
Voir mon profil →
|
||||
</button>
|
||||
```
|
||||
|
||||
**Badge sémantique :**
|
||||
```tsx
|
||||
<span className={cn(
|
||||
'inline-flex items-center gap-1.5 px-2.5 py-0.5 rounded-full',
|
||||
'text-[11px] font-semibold border',
|
||||
variant === 'brand' && 'bg-[var(--color-brand-soft)] text-[var(--color-brand-text)] border-[rgba(27,79,216,0.22)]',
|
||||
variant === 'success' && 'bg-[var(--color-success-soft)] text-[var(--color-success)] border-[rgba(74,222,128,0.22)]',
|
||||
)}>
|
||||
{children}
|
||||
</span>
|
||||
```
|
||||
|
||||
### Règles d'implémentation
|
||||
|
||||
- Chaque primitive **accepte `className`** en 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 `icon` acceptant un `ReactNode`, jamais par nom de string.
|
||||
- Les icônes sont importées de `lucide-react` et passées comme composant, jamais par nom de string.
|
||||
|
||||
---
|
||||
|
||||
## 5. Layout dashboard — spécification
|
||||
## 6. Layout principal — `AppLayout`
|
||||
|
||||
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>
|
||||
```tsx
|
||||
<div className="flex min-h-screen">
|
||||
<Sidebar /> {/* fixed, w-[230px], bg sidebar navy */}
|
||||
<main
|
||||
className="flex-1 ml-[230px] min-h-screen p-9"
|
||||
style={{
|
||||
background: `
|
||||
radial-gradient(ellipse at 35% 0%, var(--color-gradient-a), transparent 55%),
|
||||
radial-gradient(ellipse at 80% 100%, var(--color-gradient-b), transparent 50%),
|
||||
var(--color-canvas)
|
||||
`,
|
||||
}}
|
||||
>
|
||||
<div className="max-w-[1100px] mx-auto">
|
||||
{children}
|
||||
</div>
|
||||
</main>
|
||||
<BottomNav /> (< 1024px)
|
||||
</body>
|
||||
</div>
|
||||
```
|
||||
|
||||
### 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) |
|
||||
| `< 1024px` | Sidebar masquée, `BottomNav` fixe en bas, padding horizontal 20px |
|
||||
| `≥ 1024px` | Sidebar 230px + contenu centré 1100px max, padding 36px |
|
||||
| `≥ 1440px` | Contenu centré 1100px max (pas d'élargissement) |
|
||||
|
||||
### Densité verticale
|
||||
|
||||
|
|
@ -257,7 +400,7 @@ Les primitives ci-dessus s'assemblent dans `src/features/dashboard/` et `src/pag
|
|||
|
||||
---
|
||||
|
||||
## 6. Données mock — Sprint 1
|
||||
## 7. Données mock
|
||||
|
||||
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.
|
||||
|
||||
|
|
@ -276,9 +419,9 @@ export const mockDashboard = {
|
|||
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 },
|
||||
{ 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',
|
||||
|
|
@ -289,55 +432,76 @@ export const mockDashboard = {
|
|||
```
|
||||
|
||||
**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).
|
||||
- Aucun "Lorem ipsum", aucune date absolue — relatif uniquement.
|
||||
- Les prénoms mocks reflètent l'audience : Yacine, Aminata, Kenza, Bilal, Fatou, Kévin.
|
||||
- Les scores suivent une progression crédible (pas de 20/20 ni de 5/20).
|
||||
|
||||
---
|
||||
|
||||
## 7. Accessibilité — plancher Sprint 1
|
||||
## 8. Accessibilité — plancher
|
||||
|
||||
- Contraste minimum **WCAG AA** sur tous les couples texte/fond (vérifié pour la palette ci-dessus).
|
||||
- Contraste minimum **WCAG AA** sur tous les couples texte/fond (vérifié dark ET light).
|
||||
- Tous les éléments interactifs ont un `:focus-visible` avec `--shadow-focus` (halo bleu 3px).
|
||||
- Les icônes purement décoratives portent `aria-hidden="true"`.
|
||||
- Les icônes 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>`.
|
||||
- Les landmarks sémantiques : `<header>`, `<nav>`, `<main>`, `<section>`.
|
||||
- Le `BottomNav` mobile respecte la hauteur minimale tap target : **44×44 px** par item.
|
||||
- Le `ThemeToggle` a un `aria-label` dynamique : "Passer en mode clair" / "Passer en mode sombre".
|
||||
|
||||
---
|
||||
|
||||
## 8. Dépendances externes — Sprint 1
|
||||
## 9. Dépendances externes
|
||||
|
||||
**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 :
|
||||
|
||||
1. On veut maîtriser **exactement** chaque composant (pas d'override Tailwind contre Radix).
|
||||
2. 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.
|
||||
| Dépendance | Statut | Justification |
|
||||
|---|---|---|
|
||||
| `lucide-react` | ✅ Autorisée | Icônes cohérentes, tree-shakeable, aucun CSS importé |
|
||||
| `clsx` + `tailwind-merge` | ✅ Autorisées | Utilitaire `cn()` pour merge de classes |
|
||||
| `shadcn/ui` | ⛔ Interdit | Overrides Tailwind trop complexes pour le volume actuel |
|
||||
| `radix-ui` | 🔒 Reporté | Utilisable si besoin justifié par ADR (Dialog, Popover) |
|
||||
|
||||
---
|
||||
|
||||
## 9. Journal des décisions DA
|
||||
## 10. Règles impératives pour Claude Code
|
||||
|
||||
1. **Ne jamais utiliser de couleurs en dur** — toujours `var(--color-*)`.
|
||||
2. **Ne jamais utiliser `bg-white`, `bg-gray-*`, `text-gray-*`** — utiliser les tokens sémantiques.
|
||||
3. **La sidebar est toujours navy** — ses tokens ne changent jamais entre dark et light.
|
||||
4. **Le fond principal utilise deux `radial-gradient` subtils** — jamais un aplat uni.
|
||||
5. **Typographie : Plus Jakarta Sans uniquement** — jamais Inter, Roboto, ou system seul.
|
||||
6. **Les cartes** utilisent `var(--color-surface)` + `var(--color-border)` — en dark c'est semi-transparent, en light c'est blanc avec shadow.
|
||||
7. **Les hover states** utilisent `var(--color-surface-hover)` — jamais de `rgba` en dur.
|
||||
8. **Copier les patterns de la section 5** — ne pas réinterpréter, ne pas "améliorer".
|
||||
9. **Tester visuellement en dark ET en light** avant de valider un composant.
|
||||
|
||||
---
|
||||
|
||||
## 11. 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 |
|
||||
| 2026-04-17 | Direction A (Boréal) validée comme base | 5 directions explorées, A choisie |
|
||||
| 2026-04-17 | Fond `#F4F2EC`, light-only, dark reporté Sprint 2+ | Première itération |
|
||||
| 2026-04-24 | **Direction Charcoal adoptée — remplace Boréal** | Analyse concurrentielle Primo TCF, 4 directions testées (Deep Navy, Royal Blue, Gradient Mesh, Charcoal), Charcoal retenu avec touch de Gradient Mesh |
|
||||
| 2026-04-24 | Sidebar navy `#0C1528` permanent dark+light | Cohérence Slack/Discord/Linear, ancre visuelle de marque |
|
||||
| 2026-04-24 | Dark mode activé par défaut (`#111111`) | Usage quotidien desktop, cible intérieur, cohérent avec le positionnement premium |
|
||||
| 2026-04-24 | Light mode activé avec fond `#F3F4F6` | Sidebar navy maintenue, topbar claire, cartes blanches avec shadow |
|
||||
| 2026-04-24 | `prefers-color-scheme` respecté au chargement | Fallback dark si pas de préférence système |
|
||||
| 2026-04-24 | Desktop-first pour l'app | Analytics V1 : 60% desktop après 1 semaine d'usage. Mobile = acquisition (Facebook/WhatsApp), desktop = usage quotidien |
|
||||
| 2026-04-24 | Plus Jakarta Sans via Google Fonts CDN | Chargement explicite, pas de fallback-only |
|
||||
| 2026-04-24 | `lucide-react` autorisée | Remplace les SVG inline manuels |
|
||||
| 2026-04-24 | Tokens dual-theme actifs dès maintenant | Plus de dark reporté — les deux modes sont livrés ensemble |
|
||||
|
||||
---
|
||||
|
||||
## 10. Hors périmètre Sprint 1
|
||||
## 12. Hors périmètre actuel
|
||||
|
||||
Éléments explicitement **reportés** :
|
||||
|
||||
- Dark mode applicatif.
|
||||
- Thème haut-contraste (WCAG AAA).
|
||||
- Internationalisation (i18n) — Sprint 1 monolingue FR.
|
||||
- Internationalisation (i18n) — 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).
|
||||
- Self-hosting de la font Plus Jakarta Sans.
|
||||
- Troisième thème (ex: "mode examen" épuré).
|
||||
|
||||
Chacun de ces points mérite un ADR dédié quand il sera abordé.
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue