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:
Hermann_Kitio 2026-04-24 23:07:38 +03:00
parent 407d1bd134
commit b68f160bce
61 changed files with 1269 additions and 726 deletions

View file

@ -29,13 +29,40 @@ Chaque entrée suit ce format :
---
## [Unreleased] — 2026-04-24 — Sprint DA Charcoal — Reskin complet
### Changed
- Remplacement intégral `src/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 et light) avec tokens `--color-sidebar-*`.
- Layout `AppLayout` : radial-gradient sur `<main>`, sidebar 230px, `max-w-[1100px]`.
- Script anti-FOUC inline dans `index.html` (détection `prefers-color-scheme` + `localStorage`).
- Renommage tokens Boréal→Charcoal sur ~45 composants (ink-1→ink-primary, expria→brand, line→border, deep→sidebar-bg, \*-bg→\*-soft, etc.).
- Inversion `dark:` → baseline + `light:` sur 5 primitives shadcn (button, badge, input, dialog, avatar).
- `DesignSystemPage` réécrite avec palette Charcoal complète.
- `docs/adr/006-stack-versions-2026.md` mis à jour : tokens Charcoal, suppression `@variant dark` et `.dark {}`.
### Fixed
- Logo Expria : wordmark forcé en `text-white` dans la sidebar (invisible en light mode sur fond navy).
### Notes
- 59 fichiers modifiés, +1173/-727 lignes.
- Tests : 122/122 verts. Typecheck : 0 erreur.
- Timeout API intermittent observé (cold start Render) — préexistant, non lié au reskin.
---
## [Unreleased] — 2026-04-23 — Clean FTD-23 + FTD-24
### Fixed
- **FTD-23 résolu** : `useAutosave` ne fire plus après correction — `enabled` propagé avec `step !== 'done' && step !== 'correcting'` depuis `SimulationForm`. 2 tests de régression ajoutés.
- **FTD-24 résolu** : polling automatique 3s dans `useRapport` quand `exercices_status` ou `modele_status === 'pending'`. Arrêt auto dès ready/error. Timeout 2 min avec message + bouton Réessayer dans `JobStatusFallback`. 5 tests ajoutés.
### Notes
- Tests frontend : 122/122 verts (+7 vs baseline 115).
- TECH_DEBT.md → v1.19. 10 FTD actives (cap 15).
@ -44,10 +71,12 @@ Chaque entrée suit ce format :
## [Unreleased] — 2026-04-23 — FTD-28 — Semgrep CI + CI verte
### Added
- Semgrep scan (`--severity=ERROR`) dans les CI frontend et backend (FTD-28).
- Variables d'env factices dans CI frontend pour les tests.
### Fixed
- 4 erreurs ESLint corrigées : split SimulationFlowProvider (react-refresh), hook conditionnel MonProfilPreparation, ref render useTimer, setState effect AppLayout.
- Prettier format sur 7 fichiers.
- CI frontend verte pour la première fois depuis le 18 avril.
@ -57,6 +86,7 @@ Chaque entrée suit ce format :
## [Unreleased] — 2026-04-23 — FTD-27 — CI backend
### Added
- `expria-backend/.github/workflows/ci.yml` — CI GitHub Actions (test + audit, Node 22). CI verte au premier run.
- FTD-27 fermée dans TECH_DEBT.md (v1.17).
@ -65,6 +95,7 @@ Chaque entrée suit ce format :
## [Unreleased] — 2026-04-23 — FTD-29 — Dependabot config
### Added
- `.github/dependabot.yml` créé dans les 2 dépôts (npm, weekly, limit 10 PRs).
- FTD-29 fermée dans TECH_DEBT.md (v1.16).
@ -73,6 +104,7 @@ Chaque entrée suit ce format :
## [Unreleased] — 2026-04-23 — Réorg sécurité TECH_DEBT v1.15
### Changed
- `TECH_DEBT.md` v1.14 → v1.15 — réorganisation sécurité.
- Gelées (backlog post-MVP) : FTD-06 (AudioWorklet), FTD-08 (Tests E2E), FTD-15 (option 'system' thème).
- Ajoutées : FTD-27 🔴 (CI backend), FTD-28 🔴 (Semgrep CI), FTD-29 🟡 (Dependabot config).
@ -83,6 +115,7 @@ Chaque entrée suit ce format :
## [Unreleased] — 2026-04-23 — Triage FTD v1.14
### Changed
- `TECH_DEBT.md` v1.13 → v1.14 — triage dette technique : 17 → 15 FTD actives (cap respecté).
- Fermées : FTD-04 (miroir docs, accepté ADR 004), FTD-05 (scaffold caduc, audit clean), FTD-20 (GET /simulations/:id livré Sprint 3.6a), FTD-22 (code orphelin /sujets, résolution complète).
- Ajoutées : FTD-25 🟢 (ARCHITECTURE.md §3 désaligné), FTD-26 🟡 (cohabitation shared/ui vs shared/components/ui).
@ -92,14 +125,15 @@ Chaque entrée suit ce format :
## [Unreleased]
### Added
- Documentation initiale du projet (ARCHITECTURE, ONBOARDING, SECURITY, etc.)
- 5 ADRs pour les décisions architecturales majeures
- Code source de `src/entities/user/access.ts` et `lib.ts` avec tests
## [Unreleased] — 2026-04-22 — Sprint 3.5 — Clean post-Sprint 3
### Changed
- **FTD-17 résolu** : `PLAN_QUERY_KEY` centralisé dans `src/entities/user/query-keys.ts` (constantes pures, aucun import runtime). `usePlan` le ré-exporte ; `SimulationPage` et `RapportPage` remplacent leur `useQuery` inline par le hook `usePlan()` — déduplication totale de la clé et de la config `staleTime`.
- **FTD-18 résolu** : `SimulationForm` migré de `@/shared/components/ui/button` (shadcn) vers la primitive canonique `@/shared/ui/Button`. Aucun variant à adapter (usage sans prop `variant`).
- **FTD-19 résolu** : token `--shadow-focus` ajouté dans `@theme {}` (`0 0 0 3px rgba(27, 79, 216, 0.18)` — conforme `DESIGN_SYSTEM.md §2`) et dans `.dark {}` (recalculé sur la teinte expria dark). Migration de 5 occurrences `ring-2 ring-expria/20` → utility `shadow-focus` dans `Button`, `Card`, `SimulationForm` (×3), `SpecialCharsKeyboard`.
@ -107,13 +141,14 @@ Chaque entrée suit ce format :
- `TECH_DEBT.md` → v1.11. 15 FTD actives (cap de 15 respecté).
### Notes
- Timeouts DeepSeek intermittents observés pendant les tests manuels Groupe B + C — cause externe (API tierce), hors périmètre refactor Sprint 3.5.
- B8 : comportement actuel diffère du spec `PARCOURS_UTILISATEURS.md §2 "Quota atteint"` — affichage d'une bannière inline au lieu du modal de blocage attendu. À corriger dans un sprint dédié (non inclus dans ce clean, qui n'introduit aucune nouvelle fonctionnalité).
## [Unreleased] — 2026-04-22 — Sprint 3.6c — Analyse patterns (Backend + Frontend)
### Added (backend)
- `GET /users/patterns` — analyse des patterns récurrents pour utilisateur Premium.
- Auth : `authMiddleware` + `planMiddleware('pattern_analysis')` (403 `PLAN_INSUFFICIENT` si Free/Standard).
- < 5 productions corrigées `200 { ready: false, minimum: 5, current: N }`.
@ -126,12 +161,13 @@ Chaque entrée suit ce format :
- 19 nouveaux tests (`patternsController.test.ts`) : 7 sur `aggregatePatterns`, 4 sur `computePreparationIndex`, 8 sur route (401, 403 free/standard, <5 prod, cache hit, cache miss + insert, no patterns, DeepSeek fail gracieux). **205 tests backend verts** (+19 vs baseline 186).
### Added (frontend)
- Page `/progression` — route sous `AppLayout` + `ProtectedRoute`, remplace le placeholder `ComingSoon`.
- `ProgressionPage` — orchestre `usePlan` + `usePatterns`, gate plan via `hasAccess('pattern_analysis')`.
- `ProgressionPremium` — orchestrateur : si not-ready → `NotReadyState` ; sinon Hero indice + patterns + exercices long terme + footer « Analyse basée sur vos N dernières productions — il y a X ».
- `PreparationIndexHero` — score /100 + jauge horizontale colorée (rouge <40 / ambre 40-70 / vert >70) + message.
- `PatternsList` — liste des patterns avec libellé via nouveau `CRITERE_LABELS` + badge fréquence (3/5, 4/5, 5/5).
- **`PatternExerciceCard`** — *nouveau composant lesson-style*, non interactif (contrairement à `ExerciceInteractive` du rapport individuel) : critère + diagnostic + consigne + bloc incorrect (barré rouge) côte à côte avec bloc correct (vert) + **encart astuce proéminent** (icône ampoule + fond warning).
- **`PatternExerciceCard`** — _nouveau composant lesson-style_, non interactif (contrairement à `ExerciceInteractive` du rapport individuel) : critère + diagnostic + consigne + bloc incorrect (barré rouge) côte à côte avec bloc correct (vert) + **encart astuce proéminent** (icône ampoule + fond warning).
- `NotReadyState` — barre de progression N/5 + CTA `Démarrer une simulation`.
- `BlurredProgression` — aperçu flouté pour Free/Standard + bouton upgrade Premium.
- Section Dashboard Premium `MonProfilPreparation` — MetricCard indice (score + jauge compacte + message) + nombre d'erreurs récurrentes + CTA « Voir mon profil de préparation » vers `/progression`. Garde explicite `hasAccess('pattern_analysis')` → composant retourne `null` pour Free/Standard (pas rendu dans le DOM).
@ -141,15 +177,16 @@ Chaque entrée suit ce format :
- 13 nouveaux tests : 6 sur `ProgressionPremium` (not-ready, ready avec indice/patterns/exercices, footer, 0 pattern) + 7 sur `MonProfilPreparation` (gating Free/Standard, Premium ready/not-ready, loading, error, 0 pattern). **115 tests frontend verts** (+13 vs baseline 102).
### Notes
- **Formule indice** arbitraire (60/20/20) — à affiner après observation prod si besoin.
- **Dégradation gracieuse DeepSeek** : si `generatePatternExercices` throw, le backend persiste quand même l'analyse avec `exercises: []` et logue l'erreur. Le frontend affiche alors la liste des patterns sans section exercices (pas de message d'erreur explicite côté UI — l'utilisateur ne sait pas qu'il manque quelque chose).
- **`ExerciceInteractive` NON réutilisé** pour les exercices long terme : les shapes et UX sont différents (lesson vs tentative). Deux composants distincts cohabitent.
- **Migration SQL à exécuter manuellement** : `cd expria-backend && supabase db push` avant les tests end-to-end Premium.
## [Unreleased] — 2026-04-22 — Sprint 3.7 — Historique (Backend + Frontend)
### Added (backend)
- `GET /simulations` — liste paginée des productions de l'utilisateur connecté.
- Query params : `page` (défaut 1, entier ≥ 1), `limit` (défaut 20, entier entre 1 et 50).
- Tri : `created_at DESC` côté Supabase.
@ -161,6 +198,7 @@ Chaque entrée suit ce format :
- 12 nouveaux tests sur la route `GET /simulations` (186 tests backend verts, +12 vs baseline 174).
### Added (frontend)
- Page `/historique` (route sous `AppLayout` + `ProtectedRoute`, remplace le placeholder `ComingSoon`).
- `HistoriquePage` — orchestre `usePlan` + `useSimulationsList`, state local de pagination, gating plan Free via `hasAccess('dashboard')`.
- `SimulationsList` — composant liste avec :
@ -178,15 +216,16 @@ Chaque entrée suit ce format :
- 18 nouveaux tests frontend (7 `date.test.ts` + 11 `SimulationsList.test.tsx`).
### Notes
- Les simulations avec `score === null` (en cours ou correction échouée) sont **affichées** avec un badge « En cours ». Clic → `/rapport/:id``RapportPage` gère le cas `REPORT_NOT_READY` (FTD-21) en redirigeant vers `/simulation/ee`.
- `BlurredPreview` dupliqué localement dans `SimulationsList` (pattern équivalent à `BlurredSection` de `RapportPage`). À extraire en `shared/` si le pattern se répète dans un 3ᵉ endroit — pas fait dans ce sprint.
- Pagination : Précédent/Suivant (MVP) retenu contre scroll infini. Le choix sera revu si l'historique dépasse 100 items en prod.
- Tests frontend : **102/102 verts** (+18 vs baseline 84).
## [Unreleased] — 2026-04-22 — Sprint 3.6b — Qualité correction — Frontend
### Added
- `NclcCibleSelector` (segmented control NCLC 9 / NCLC 10) dans `SimulationForm` — valeur propagée au payload `POST /corrections/ee` via `SimulationFlowProvider.submitText(texte, nclcCible)`.
- Composants `rapport/` dans `features/simulations/components/` :
- `ScoreHero` — score /20, jauge avec marqueur du seuil NCLC cible, écart vs objectif (« X points avant NCLC 9 »), badges NCLC atteint / cible.
@ -202,6 +241,7 @@ Chaque entrée suit ce format :
- FTD-24 🟡 dans `TECH_DEBT.md` — polling automatique pour exercices/modèle `pending` (refresh manuel en MVP).
### Changed
- `entities/report/types.ts` — refonte complète alignée sur le backend Sprint 3.6a : `Report` remplace l'ancien (revelation, diagnostic, criteres enrichis, conseil_nclc, erreurs_codes top-level, exercices dynamiques, modele structuré, statuts pending/ready/error). Suppression de `feedback_court`, `erreurs[]`, `modele:string`, `idees[]` (obsolètes).
- `entities/report/lib.ts``BlurableSection` réduite à `'criteres' | 'exercices' | 'modele'` : `revelation`, `diagnostic`, `conseil_nclc` deviennent visibles pour tous les plans conformément à PLANS_TARIFAIRES.md §2.
- `entities/production/types.ts``SimulationState` étendu avec `nclc_cible`, `exercices`, `exercices_status`, `modele`, `modele_status` ; `SimulationRapport` aligné sur `CorrectionRapport` backend.
@ -210,30 +250,34 @@ Chaque entrée suit ce format :
- `floutage.test.ts` réécrit (17 tests — matrice de visibilité + helpers lib).
### Fixed
- **Race condition `modele_status`** (backend) : l'update principal de correction écrasait `modele_status='ready'` déjà posé par `runModeleJob` (lancé en parallèle option b). `correctionController.correctEE` ne touche plus aux colonnes `*_status` — pilotées exclusivement par les jobs asynchrones.
- **Boucle infinie retour rapport → SimulationPage** : le useEffect sticky `step === 'done' → navigate('/rapport/:id')` renvoyait l'utilisateur sur le rapport à chaque tentative de retour vers `/simulation/ee`. Supprimé ; la navigation initiale vers `/rapport/:id` est déclenchée une seule fois dans `correctMutation.onSuccess` du provider.
- **Boucle retour /sujets → SimulationPage** : même pattern sticky pour `step === 'choosing-subject' → navigate('/sujets')`. Supprimé ; navigation initiale vers `/sujets` déplacée dans `createMutation.onSuccess`.
- **RapportPage hors SimulationFlowProvider** : la route `/rapport/:id` n'était pas sous `SimulationFlowLayout` — l'appel à `useSimulation()` depuis RapportPage throw. Route déplacée sous le layout, l'instance du provider est partagée avec `/simulation/ee` et `/sujets`.
### Added
- Bouton « Nouvelle simulation » en bas de `RapportPage` qui `reset()` + `navigate('/simulation/ee')`.
- `reset()` explicite dans le bouton « ← Retour » de `SujetsPage` avant la navigation, pour empêcher tout re-déclenchement de la garde sticky.
### Changed
- Navigations post-mutation déplacées dans `onSuccess` du provider (pattern cohérent pour `createMutation``/sujets` et `correctMutation``/rapport/:id`). Plus de useEffect réactif aux changements de `step` côté SimulationPage.
- `SujetsPage` : garde étendue de `!production` à `!production \|\| step === 'idle' \|\| step === 'done'` pour couvrir le cas post-rapport (évite le 400 VALIDATION_ERROR sur `PATCH /simulations/:id/sujet` d'une simulation déjà corrigée).
- `RapportPage` breadcrumb : `<Link>` remplacé par `<button>` qui `reset()` avant navigate.
### Notes
- **Option β retenue** : frontend aligné sur la structure backend réelle du Sprint 3.6a. Aucun aller-retour backend.
- `feedback_court` supprimé de l'UI ; `diagnostic` remplace la section « Retour général ».
- Polling automatique non implémenté (FTD-24) : refresh manuel de la page si `exercices_status` / `modele_status` = `'pending'`.
- Tests : **84/84 verts** (+8 vs baseline 76).
## [Unreleased] — 2026-04-22 — Sprint 3.6a — Qualité correction — Backend
### Added (backend)
- `src/lib/taxonomieErreurs.ts` : constantes des 63 codes TCF Canada + 4 codes `autre` par critère, validation runtime `isValidCode` / `isValidCritere`, et injection au prompt via `buildTaxonomyPromptSection`.
- Prompts dynamiques dans `src/lib/deepseek.ts` : `buildCorrectionPrompt` (prompt maître avec `nclc_cible` 9 ou 10, sujet, documents T3), `buildModelPrompt` (production modèle cible NCLC 9 fixe), `buildExercicesPrompt` (3 exercices ciblés sur `erreurs_codes` + extraits `exemple`, format `{difficulte, theme, diagnostic, consigne, extrait, indice, correction, explication}`).
- Post-traitement production modèle : `wordCountTCF`, `stripModelAnnotations`, `truncateToMaxWords`.
@ -243,42 +287,47 @@ Chaque entrée suit ce format :
- `docs/TECH_DEBT.md` TD-15 🟡 : jobs fire-and-forget peuvent rester `pending` si redémarrage process.
### Changed (backend)
- `correctEE` dans `deepseek.ts` — nouvelle signature `correctEE(CorrectionInput)` + nouvelle forme `CorrectionRapport` (revelation, diagnostic, criteres[{exemple,suggestion,astuce}], conseil_nclc, erreurs_codes). `EERapport` devient alias de `CorrectionRapport`.
- `correctionController.correctEE` : lance 3 appels DeepSeek en parallèle ; await uniquement sur la correction pour répondre 200 ; modèle et exercices s'exécutent en fire-and-forget et mettent à jour `{exercices,exercices_status}` et `{modele,modele_status}` en base (pending → ready/error).
- `simulationController.getById` retourne les nouveaux champs : `nclc_cible`, `exercices`, `exercices_status`, `modele`, `modele_status` en plus du `rapport` enrichi.
- `deepseek.test.ts` réécrit — 25 tests (ancien pipeline supprimé, nouveaux tests sur correctEE/generateProductionModele/generateExercices/helpers + EO inchangé).
### Notes
- **Option A retenue** : backend renvoie uniquement la nouvelle forme. Frontend (Sprint 3.6b) casse tant que non livré — livraison groupée sans déploiement intermédiaire.
- Prompt exercices rédigé côté backend (option b), basé sur les codes taxonomie + extraits `exemple` des critères. Format aligné sur captures d'écran demandées.
- Migration SQL à exécuter manuellement via `supabase db push` — Hermann avant le premier test end-to-end.
- Tests backend : 173/173 verts (+18 vs baseline de 155).
## [Unreleased] — 2026-04-22 — Planification Sprint 3.6a/3.6b/3.6c
### Added
- Sprints 3.6a (backend prompts + taxonomie), 3.6b (frontend rapport enrichi), 3.6c (analyse patterns Premium) ajoutés à la ROADMAP entre Sprint 3.5 et Sprint 4.
- `TAXONOMIE_ERREURS.md` — 63 codes d'erreurs TCF Canada sur 4 critères + 4 codes « autre » + procédure d'enrichissement.
## 2026-04-21 — FTD-21 — Persistance session `/simulation/ee`
### Added
- `useAutosave(simulationId, contenu, enabled)` : autosave debounce 30 s + flush sur `beforeunload`, dedup par dernier contenu sauvegardé (6 tests).
- `SimulationFlowProvider` hydrate la session au montage depuis `localStorage` (`expria_simulation_id`) → `GET /simulations/:id` → restaure `step='task-selected'` + `production` + `sujet` si `rapport=null` ; nettoie la clé sinon (3 tests resume).
- Types `SimulationState`, `SimulationRapport` + API `getSimulationState`, `autosaveContenu`, `updateSujet` dans `entities/production`.
- Indicateur "Sauvegardé à HH:MM" sous la textarea `SimulationForm` (text-xs, `aria-live="polite"`).
### Changed
- `getReport` délègue désormais à `getSimulationState` et lève `REPORT_NOT_READY` si `rapport=null`. `RapportPage` catche cette erreur et redirige vers `/simulation/ee` avec message discret "Votre simulation est en cours.".
- `SimulationForm` accepte `simulationId`, `initialContenu`, `step` et persiste `expria_simulation_id` dans `localStorage` tant que la simulation est active ; nettoie la clé quand `step='done'`.
- `changeSubject` persiste le changement côté backend via `PATCH /simulations/:id/sujet` (best-effort, silencieux si échec).
### Security
- localStorage ne stocke que `simulation_id` (UUID non-sensible) — conforme SECURITY.md §2.6.
### Notes
- FTD-21 reste ouvert pour `/simulation/eo` (Sprint 4) et `/examen` (Sprint 7).
---
@ -286,6 +335,7 @@ Chaque entrée suit ce format :
## 2026-04-21 — Tâche G5 — Suggestions d'idées DeepSeek
### Ajouté
- **Backend**`POST /sujets/idees` : génère 5 suggestions
d'idées via DeepSeek pour aider l'étudiant à prolonger sa
rédaction (prompt coach TCF Canada, temperature 0.5,
@ -310,6 +360,7 @@ Chaque entrée suit ce format :
`planData.plan` depuis `SimulationPage`)
### Règles d'accès
- Règle D respectée : `hasAccess(plan, 'tips')` obligatoire
- Plan Free : bouton visible mais désactivé avec tooltip
"Disponible en Standard" (tips=false pour Free)
@ -318,6 +369,7 @@ Chaque entrée suit ce format :
`idees.isLoading`
### Tests
- Backend — Typecheck : 0 erreur, Vitest : 144/144 passés
(+5 tests POST /sujets/idees)
- Frontend — Typecheck : 0 erreur, Vitest : 67/67 passés
@ -325,10 +377,10 @@ Chaque entrée suit ce format :
à 30+ mots, modal affiche 5 idées) et Free (bouton
verrouillé avec tooltip)
## 2026-04-21 — Tâche G4 + Refonte page /sujets + Fix quota simulations
### Ajouté
- **Tâche G4** — choix du sujet avec dropdown intégré et bouton
aléatoire dans SimulationForm (hook `useSujets`, composant
`SujetSelector`, `getSujets()` sur `GET /sujets?mode=&tache=`)
@ -344,6 +396,7 @@ Chaque entrée suit ce format :
jusqu'au Sprint EO)
### Modifié
- `useSimulation` refacto en consommateur de
`SimulationFlowProvider` (source de vérité déplacée hors du hook)
- `SujetDisplay` redevient présentationnel (dropdown retiré)
@ -351,6 +404,7 @@ Chaque entrée suit ce format :
Expression Écrite (affiche uniquement EE T1/T2/T3)
### Corrigé
- **Quota simulations (backend — commit `ecb478e`, expria-backend)** :
incrément `simulations_used` déplacé de
`simulationController.create()` vers `correctionController.correctEE/EO`
@ -358,21 +412,23 @@ Chaque entrée suit ce format :
plus le quota utilisateur.
### Supprimé
- `SujetSelector.tsx` — orphelin après refonte `/sujets`
- Helper `selectSujet` de `useSimulation` — orphelin
- FTD-22 tracée résolue partiellement (step `'choosing-subject'`
+ `goToSubjectPicker` conservés intentionnellement)
- `goToSubjectPicker` conservés intentionnellement)
### Tests
- Typecheck : 0 erreur
- Vitest : 67/67 passés
- Test manuel : flux complet EE T1 avec choix de sujet
(carte + aléatoire + changement de sujet) validé
## 2026-04-21 — Tâches G2+G3 — Clavier + Minuteur
### Ajouté
- Composant SpecialCharsKeyboard — 30 caractères spéciaux
français en flex-wrap, sticky au scroll
- Bloc "Temps restant" sticky avec TimerDisplay MM:SS
@ -382,25 +438,27 @@ Chaque entrée suit ce format :
- Hook useTimer avec 7 tests unitaires
- Config par tâche dans simulationConfig.ts
(EE T1: 10min/60-120 mots, T2: 20min/120-150,
T3: 30min/120-180)
T3: 30min/120-180)
- Auto-submit à l'expiration si ≥ 30 mots
- Bouton "Soumettre ma production" (était "Envoyer")
- Textarea auto-resize sans scroll interne
### Changed
- Compteur de caractères remplacé par WordCountBar
- Bouton soumission bloqué si < 30 mots
### Tests
- Typecheck : 0 erreur
- Vitest : 66/66 passés (+7 tests useTimer)
- Test manuel : minuteur + clavier validés sur mobile
et desktop
## 2026-04-21 — Tâche G1 — Affichage de la consigne
### Ajouté
- Interface SujetData dans entities/production/types.ts
- Production enrichie avec sujet: SujetData | null
- Composant SujetDisplay — affiche consigne, rôle, contexte, doc1, doc2 selon le sujet retourné
@ -409,40 +467,46 @@ Chaque entrée suit ce format :
- FTD-21 tracée (persistance session simulation)
### Tests
- Typecheck : 0 erreur
- Vitest : 59/59 passés
- Test manuel : consigne affichée sur /simulation/ee
## 2026-04-20 — Audit frontend ↔ backend — alignement types Report
### Modifié
- `src/entities/report/types.ts``Critere.note``Critere.score`, `Report.exercices: Exercice[]``Report.exercices: string[]`, JSDoc ajusté
- `src/features/simulations/pages/RapportPage.tsx` — import `Exercice` retiré, `critere.note``critere.score`, `ExerciceCard` refactoré pour consommer une `string` rendue en Markdown, clé d'itération par index
### Supprimé
- Interface `Exercice { titre, contenu }` de `entities/report/types.ts` — remplacée par `string[]` pour coller au contrat backend
### Contexte (backend associé, expria-backend)
Quatre commits côté backend finalisent l'alignement du contrat `Report` :
- `feat(corrections)`: renommages `production_modele``modele`, `suggestions_idees``idees`, ajout `feedback_court` + prompts DeepSeek mis à jour + validations runtime
- `feat(corrections)`: réponse enrichie avec `simulation_id` côté `correctionController`
- `feat(simulations)`: nouvelle route `GET /simulations/:id` (auth owner, gestion `SIMULATION_NOT_FOUND`/`AUTH_REQUIRED`/`REPORT_NOT_READY`) + 4 tests
- `feat(simulations)`: sujet aléatoire (table `sujets`) retourné avec chaque production créée (EO_T2_LIVE exclu, non bloquant si aucun sujet actif)
### Tests
- Typecheck : 0 erreur
- Vitest : 59/59 passés
### À faire (hors scope — session frontend dédiée ultérieurement)
- Ajouter `sujet: SujetData | null` dans `entities/production/types.ts`
- Consommer le sujet retourné dans `SimulationPage` (affichage consigne + docs)
- Consommer `feedback_court` dans `RapportPage` (rendu toujours visible — cf. PLANS_TARIFAIRES §2 — déjà supporté par le type `Report`, reste à brancher dans l'UI si ce n'est pas déjà le cas)
## 2026-04-20 — Sprint 0.5 bis — AppLayout + primitives UI + refonte visuelle
### Ajouté
- `src/app/AppLayout.tsx` — layout applicatif desktop/mobile (sidebar fixe 240px, drawer mobile, BottomNav)
- `src/app/Sidebar.tsx` — navigation latérale avec verrouillage `hasAccess()` (Progression, Examen blanc, Historique)
- `src/app/MobileHeader.tsx` — header mobile sticky (Logo, ThemeToggle, bouton menu hamburger)
@ -452,15 +516,18 @@ Quatre commits côté backend finalisent l'alignement du contrat `Report` :
- `src/shared/ui/Badge.tsx` — primitive Badge (variants: plan/nclc/neutral ; couleur selon `planValue` pour variant plan)
### Modifié
- `src/app/router.tsx` — layout routes via `PrivateLayout` (`ProtectedRoute` + `AppLayout` + `Outlet`) ; `ComingSoon` inline ; redirect `/simulation``/simulation/ee`
- `src/features/simulations/components/TaskSelector.tsx` — refonte avec `Card interactive` / `Card default opacity-60`, `Badge` "EE"/"EO", eyebrow `tracking-widest`, icône verrou
- `src/features/simulations/pages/SimulationPage.tsx` — suppression header interne (Logo + ThemeToggle) ; root `<main>` ; `Button` migré vers `@/shared/ui/Button` `variant="secondary"`
- `src/features/dashboard/pages/DashboardPage.tsx` — suppression header interne ; `Button` `variant="primary"` avec `navigate('/simulation/ee')` ; `Badge` `variant="plan" planValue={data.plan}` ; tout migré vers `@/shared/ui/`
### Documentation
- `docs/TECH_DEBT.md` v1.6 — ajout FTD-18 (SimulationForm migration Button), FTD-19 (token `--shadow-focus` manquant)
### Tests
- Typecheck : 0 erreur
- Vitest : 59/59 passés
- Tests manuels : à valider par Hermann
@ -470,6 +537,7 @@ Quatre commits côté backend finalisent l'alignement du contrat `Report` :
## 2026-04-19 — Sprint 1 / Étape 6 — Maintenance mode + outillage sécurité
### Ajouté
- Page de maintenance statique (`src/app/MaintenancePage.tsx`) — logo + message, tokens Direction H, zéro dépendance
- Guard `VITE_MAINTENANCE_MODE` dans `main.tsx` — si `true`, aucun provider ne se monte, aucun appel réseau
- Variable `VITE_MAINTENANCE_MODE` dans `env.ts` (optionnelle, défaut `false`)
@ -478,10 +546,12 @@ Quatre commits côté backend finalisent l'alignement du contrat `Report` :
- MCP server Semgrep enregistré dans Claude Code
### Documentation
- `ARCHITECTURE.md` §7 — ajout `VITE_MAINTENANCE_MODE` dans la liste des variables
- `TECH_DEBT.md` — FTD-16 résolu (maintenance mode implémenté)
### Tests
- Typecheck : 0 erreur
- Vitest : 37/37 passés
- Test manuel : maintenance mode vérifié (page affichée, aucun appel réseau, routing bloqué)
- Test manuel : maintenance mode vérifié (page affichée, aucun appel réseau, routing bloqué)

View file

@ -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` à 45% opacité) |
| Fond principal (light) | `#F3F4F6` avec deux halos bleus très discrets (23% 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 | 150200 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), 45 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é.

View file

@ -1,43 +1,51 @@
# ROADMAP.md — Expria Frontend
> Source de vérité de l'ordre d'implémentation des sprints.
> Ne pas modifier sans validation de Hermann.
---
## Sprint 0 — Fondations ✅
1. Scaffold Vite + TypeScript + Tailwind + shadcn/ui
2. Structure de dossiers complète
3. docs/ copiés depuis backend + adaptations
4. ONBOARDING.md rédigé
## Sprint 0.5 — Design System ✅
- Direction artistique Boréal validée
- Tokens CSS dans index.css
- DESIGN_SYSTEM.md rédigé
## Sprint 1 — Auth + API layer ✅
5. auth-client.ts
6. api-client.ts
7. query-client.ts
8. entities/user/*
8. entities/user/\*
9. features/auth (Login, Register, ProtectedRoute)
## Sprint 2 — Dashboard conditionnel ✅
10. usePlan hook
11. shared/components/PaywallModal
12. features/dashboard (Free / Standard / Premium)
## Sprint 3 — Simulations EE ✅
13. entities/production/* + entities/report/*
13. entities/production/_ + entities/report/_
14. features/simulations (EE T1/T2/T3)
15. Affichage rapport avec floutage conditionnel
## Sprint 3.5 — Clean
- Factorisation des fichiers modifiés Sprint 3
- Tests manuels Groupe B + C rejoués
- Commit refactor(simulation-ee)
## Sprint 3.6a — Qualité correction — Backend ✅
- Remplacement prompt maître (docs/Prompt_maître.md) + intégration taxonomie erreurs (docs/TAXONOMIE_ERREURS.md)
- Remplacement prompt production modèle (docs/Prompt_production_modèle.md) — cible fixe NCLC 9
- Génération parallèle correction + exercices + modèle (await correction, fire-and-forget sur les deux autres)
@ -47,6 +55,7 @@
- Tests : 173 tests verts (+18 vs baseline)
## Sprint 3.6b — Qualité correction — Frontend ✅
- Sélecteur NCLC cible dans SimulationForm (9 ou 10, défaut 9) — NclcCibleSelector
- RapportPage réécrite : ScoreHero (jauge + seuil NCLC cible + écart), RevelationCards, DiagnosticCallout, CritereCard enrichie (exemple/suggestion/astuce + codes taxonomie), ConseilNclcCallout
- ExerciceInteractive : badge difficulté, zone texte, bouton Indice (une fois), bouton Voir la correction (activé après saisie), explication
@ -56,6 +65,7 @@
- Tests : 84 verts (+8 vs baseline — floutage + helpers lib + ExerciceInteractive)
## Sprint 3.7 — Historique ✅
- Backend : `GET /simulations` — liste paginée des productions de l'utilisateur connecté (page/limit, tri `created_at DESC`, projection légère). 186 tests backend verts.
- Frontend : page `/historique` (route sous AppLayout), liste d'items (date relative, tâche, score /20, NCLC, badge Examen / En cours), pagination Précédent/Suivant, clic → `/rapport/:id`.
- Gating plan : Free → aperçu flouté + CTA « Passer en Standard » (`hasAccess(plan, 'dashboard')`) ; Standard + Premium → liste complète.
@ -65,6 +75,7 @@
- 102 tests frontend verts (+18 vs baseline 84).
## Sprint 3.6c — Analyse patterns (Premium) ✅
- Backend : `GET /users/patterns` — agrégation des `erreurs_codes` sur les 5 dernières productions corrigées, seuil 3/5, tri DESC, cache `pattern_analyses` avec invalidation si nouvelle production plus récente que la dernière analyse.
- Backend : exercices long terme générés par DeepSeek sur patterns confirmés — format `{ consigne, exemple, correction, astuce }` avec prompt dédié (température 0.4, timeout 20 s). Dégradation gracieuse si DeepSeek échoue.
- Backend : indice de préparation 0→100 — formule 60 % score moyen + 20 % régularité + 20 % tendance, messages figés (`<40`, `40-70`, `>70`).
@ -76,39 +87,55 @@
- Frontend : hook `usePatterns` (staleTime 60 s, cache partagé entre page et dashboard, `enabled` conditionné par feature).
- Frontend : 115 tests verts (+13 vs baseline 102).
## Sprint DA Charcoal — Reskin ✅
- Remplacement palette Boréal par Charcoal (dark default, light override)
- Sidebar navy permanent, layout radial-gradient, anti-FOUC
- Renommage tokens sur ~45 composants + inversion dark:/light: shadcn
- ADR 006 mis à jour
## Sprint 4 — Simulations EO (audio)
16. MediaRecorder + upload audio EO T1/T3
## Sprint 4.5 — Clean
- Factorisation des fichiers modifiés Sprint 4
- Tests manuels Groupe B + D rejoués
- Commit refactor(simulation-eo)
## Sprint 5 — Billing
17. features/billing (Stripe Checkout + prorata)
## Sprint 5.5 — Clean
- Factorisation des fichiers modifiés Sprint 5
- Tests manuels Groupe E rejoués
- Commit refactor(billing)
## Sprint 6 — T2 Live
18. features/t2-live (ws-client + audio worklet + state machine)
## Sprint 6.5 — Clean
- Factorisation des fichiers modifiés Sprint 6
- Tests manuels Groupe D rejoués
- Commit refactor(t2-live)
## Sprint 7 — Mode Examen
19. Timer inarrêtable + readOnly à T=0
## Sprint 7.5 — Clean
- Factorisation des fichiers modifiés Sprint 7
- Tests manuels Groupe D rejoués
- Commit refactor(exam-mode)
## Sprint 8 — Pré-lancement
20. MAINTENANCE_MODE implémenté ✅ (2026-04-19)
21. Sentry configuré
22. /ultrareview avant bascule

View file

@ -107,85 +107,116 @@ Garder React 19, Vite 8, TypeScript 6 mais downgrader Tailwind 4 → 3 pour "com
Pas de `tailwind.config.ts`. La configuration se fait exclusivement dans `src/index.css`.
#### Dark mode
#### Mode thème (mis à jour Sprint DA Charcoal — 2026-04-24)
Dark mode class-based (`.dark` sur `<html>`) — toggle manuel via ThemeProvider React (Sprint 0.5 étape 2). Configuré via :
**Dark est le thème par défaut.** Les tokens de contenu (`--color-canvas`, `--color-ink-*`, etc.) sont déclarés en mode dark dans `@theme`. Une classe `.light` sur `<html>` active le mode clair en override. Configuré via :
```css
@variant dark (&:where(.dark, .dark *));
@custom-variant light (&:where(.light, .light *));
```
Si cette syntaxe est rejetée par une future version de Tailwind 4, le fallback est `@custom-variant dark (...)`.
Ce variant permet d'écrire `light:bg-surface` dans les composants quand un comportement spécifique au mode clair est requis (ex. primitives shadcn où l'opacité doit être adaptée).
#### Tokens @theme (palette Direction H — validée Sprint 0.5)
#### Tokens @theme (DA Charcoal — validée Sprint DA Charcoal 2026-04-24)
```css
@import 'tailwindcss';
@import url('https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@400;500;600;700;800&display=swap');
@variant dark (&:where(.dark, .dark *));
@custom-variant light (&:where(.light, .light *));
@theme {
/* Typographie */
--font-sans: "Plus Jakarta Sans", -apple-system, BlinkMacSystemFont, "Segoe UI",
system-ui, sans-serif;
/* ── Invariants (identiques dark + light) ── */
/* Fonds — bg-canvas = page, bg-surface = cards */
--color-canvas: #EEF2F8;
--color-canvas-2: #E6EBF4;
--color-surface: #FFFFFF;
--color-surface-hover: #F8FAFD;
/* Hairlines */
--color-line: #DDE3ED;
--color-line-strong: #C7D0E0;
/* Encres */
--color-ink-1: #0F172A; /* titres */
--color-ink-2: #1E293B; /* corps */
--color-ink-3: #475569;
--color-ink-4: #64748B;
--color-ink-5: #94A3B8; /* désactivé, hints */
/* 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-expria: #1B4FD8;
--color-expria-hover: #1741B8;
--color-expria-50: #EEF3FF;
--color-expria-100: #DCE6FF;
--color-expria-200: #B8CDFF;
--color-deep: #0B1F5C;
--color-deep-2: #142B6E;
--color-brand: #1B4FD8;
--color-brand-hover: #1744B8;
--color-brand-active: #13379C;
--color-brand-dark: #1740B0;
--color-brand-ink: #FFFFFF;
/* Sémantiques */
--color-success: #0E9F6E; --color-success-bg: #E6F6F0;
--color-warning: #C77A00; --color-warning-bg: #FEF3E2;
--color-danger: #C53030; --color-danger-bg: #FDECEC;
/* Semantic (invariants) */
--color-warning: #F59E0B;
--color-warning-soft: rgba(245, 158, 11, 0.12);
--color-danger: #EF4444;
--color-danger-soft: rgba(239, 68, 68, 0.12);
/* Rayons */
--radius-sm: 6px; --radius-md: 10px;
--radius-lg: 14px; --radius-xl: 18px; --radius-full: 999px;
/* Typographie */
--font-sans: "Plus Jakarta Sans", system-ui, -apple-system, "Segoe UI", sans-serif;
--font-mono: ui-monospace, "SF Mono", "JetBrains Mono", Menlo, monospace;
/* Ombres */
--shadow-sm: 0 1px 2px rgba(15, 23, 42, 0.04);
--shadow-md: 0 4px 12px rgba(15, 23, 42, 0.06), 0 1px 3px rgba(15, 23, 42, 0.04);
--shadow-lg: 0 12px 28px rgba(15, 23, 42, 0.08), 0 2px 6px rgba(15, 23, 42, 0.04);
/* Rayons (override Tailwind) */
--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 (défaut) ── */
--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;
}
/* Dark mode overrides */
.dark {
--color-canvas: #0D1220; --color-canvas-2: #121A2D;
--color-surface: #182238; --color-surface-hover: #1E2A42;
--color-line: #27324B; --color-line-strong: #364363;
--color-ink-1: #F1F4FA; --color-ink-2: #DDE3EF;
--color-ink-3: #A8B2C7; --color-ink-4: #7A8499; --color-ink-5: #525C73;
--color-expria: #5B7FFF; --color-expria-hover: #6F8EFF;
--color-expria-50: rgba(91, 127, 255, 0.12);
--color-deep: #060B1A;
--color-success: #3DD68C; --color-success-bg: rgba(61, 214, 140, 0.12);
--color-warning: #F5B849; --color-warning-bg: rgba(245, 184, 73, 0.12);
--color-danger: #F06B6B; --color-danger-bg: rgba(240, 107, 107, 0.12);
--shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.3);
--shadow-md: 0 4px 16px rgba(0, 0, 0, 0.4);
--shadow-lg: 0 12px 32px rgba(0, 0, 0, 0.5);
/* Light mode — override sur <html class="light"> */
.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);
}
```
@ -196,14 +227,23 @@ Les tokens `@theme` créent des classes utilitaires directement utilisables :
| Token | Classes Tailwind |
|---|---|
| `--color-canvas` | `bg-canvas`, `text-canvas`, `border-canvas` |
| `--color-surface` | `bg-surface`, `text-surface`, `border-surface` |
| `--color-ink-1` | `text-ink-1`, `bg-ink-1` |
| `--color-expria` | `bg-expria`, `text-expria`, `border-expria` |
| `--color-success` | `text-success`, `bg-success` |
| `--radius-md` | `rounded-md` (override : 10px au lieu de 6px Tailwind) |
| `--shadow-sm` | `shadow-sm` (override valeurs Tailwind) |
| `--color-surface` | `bg-surface`, `border-surface` |
| `--color-surface-hover` | `bg-surface-hover` |
| `--color-sidebar-bg` | `bg-sidebar-bg` (navy permanent, identique dark+light) |
| `--color-ink-primary` | `text-ink-primary` |
| `--color-ink-secondary` | `text-ink-secondary` |
| `--color-ink-tertiary` | `text-ink-tertiary` |
| `--color-brand` | `bg-brand`, `border-brand`, `ring-brand` |
| `--color-brand-text` | `text-brand-text` (bleu adapté au fond — `#7DA4F0` dark, `#1B4FD8` light) |
| `--color-brand-soft` | `bg-brand-soft` (teinte chip / highlight discret) |
| `--color-success-soft`, `-warning-soft`, `-danger-soft` | `bg-success-soft`, etc. |
| `--shadow-card`, `--shadow-raised` | `shadow-card`, `shadow-raised` (auto dual-theme : `none` en dark, ombre en light) |
| `--shadow-focus` | `shadow-focus` (halo bleu 3px sur `:focus-visible`) |
**Convention critique** : `bg-surface` = cards / modals / panels. `bg-canvas` = fond de page. Ne jamais inverser.
**Conventions critiques :**
- `bg-surface` = cards / modals / panels. `bg-canvas` = fond de page. Ne jamais inverser.
- `bg-sidebar-bg` = navy permanent — ne change jamais entre dark et light (ancre visuelle de marque).
- Utiliser le préfixe `light:` uniquement quand un override spécifique au mode clair est strictement nécessaire (ex. primitives shadcn où l'opacité d'une couleur sémantique diffère).
#### Typographie