diff --git a/.cursor/skills/frontend-design/SKILL.md b/.cursor/skills/frontend-design/SKILL.md
new file mode 100644
index 0000000..600b6db
--- /dev/null
+++ b/.cursor/skills/frontend-design/SKILL.md
@@ -0,0 +1,42 @@
+---
+name: frontend-design
+description: Create distinctive, production-grade frontend interfaces with high design quality. Use this skill when the user asks to build web components, pages, or applications. Generates creative, polished code that avoids generic AI aesthetics.
+license: Complete terms in LICENSE.txt
+---
+
+This skill guides creation of distinctive, production-grade frontend interfaces that avoid generic "AI slop" aesthetics. Implement real working code with exceptional attention to aesthetic details and creative choices.
+
+The user provides frontend requirements: a component, page, application, or interface to build. They may include context about the purpose, audience, or technical constraints.
+
+## Design Thinking
+
+Before coding, understand the context and commit to a BOLD aesthetic direction:
+- **Purpose**: What problem does this interface solve? Who uses it?
+- **Tone**: Pick an extreme: brutally minimal, maximalist chaos, retro-futuristic, organic/natural, luxury/refined, playful/toy-like, editorial/magazine, brutalist/raw, art deco/geometric, soft/pastel, industrial/utilitarian, etc. There are so many flavors to choose from. Use these for inspiration but design one that is true to the aesthetic direction.
+- **Constraints**: Technical requirements (framework, performance, accessibility).
+- **Differentiation**: What makes this UNFORGETTABLE? What's the one thing someone will remember?
+
+**CRITICAL**: Choose a clear conceptual direction and execute it with precision. Bold maximalism and refined minimalism both work - the key is intentionality, not intensity.
+
+Then implement working code (HTML/CSS/JS, React, Vue, etc.) that is:
+- Production-grade and functional
+- Visually striking and memorable
+- Cohesive with a clear aesthetic point-of-view
+- Meticulously refined in every detail
+
+## Frontend Aesthetics Guidelines
+
+Focus on:
+- **Typography**: Choose fonts that are beautiful, unique, and interesting. Avoid generic fonts like Arial and Inter; opt instead for distinctive choices that elevate the frontend's aesthetics; unexpected, characterful font choices. Pair a distinctive display font with a refined body font.
+- **Color & Theme**: Commit to a cohesive aesthetic. Use CSS variables for consistency. Dominant colors with sharp accents outperform timid, evenly-distributed palettes.
+- **Motion**: Use animations for effects and micro-interactions. Prioritize CSS-only solutions for HTML. Use Motion library for React when available. Focus on high-impact moments: one well-orchestrated page load with staggered reveals (animation-delay) creates more delight than scattered micro-interactions. Use scroll-triggering and hover states that surprise.
+- **Spatial Composition**: Unexpected layouts. Asymmetry. Overlap. Diagonal flow. Grid-breaking elements. Generous negative space OR controlled density.
+- **Backgrounds & Visual Details**: Create atmosphere and depth rather than defaulting to solid colors. Add contextual effects and textures that match the overall aesthetic. Apply creative forms like gradient meshes, noise textures, geometric patterns, layered transparencies, dramatic shadows, decorative borders, custom cursors, and grain overlays.
+
+NEVER use generic AI-generated aesthetics like overused font families (Inter, Roboto, Arial, system fonts), cliched color schemes (particularly purple gradients on white backgrounds), predictable layouts and component patterns, and cookie-cutter design that lacks context-specific character.
+
+Interpret creatively and make unexpected choices that feel genuinely designed for the context. No design should be the same. Vary between light and dark themes, different fonts, different aesthetics. NEVER converge on common choices (Space Grotesk, for example) across generations.
+
+**IMPORTANT**: Match implementation complexity to the aesthetic vision. Maximalist designs need elaborate code with extensive animations and effects. Minimalist or refined designs need restraint, precision, and careful attention to spacing, typography, and subtle details. Elegance comes from executing the vision well.
+
+Remember: Claude is capable of extraordinary creative work. Don't hold back, show what can truly be created when thinking outside the box and committing fully to a distinctive vision.
\ No newline at end of file
diff --git a/.gitignore b/.gitignore
index 4591520..1862d22 100644
--- a/.gitignore
+++ b/.gitignore
@@ -30,3 +30,6 @@ dist-ssr
# Claude Code local config
.claude/
+
+# Exploration DA temporaire — supprimer une fois la direction choisie
+design-exploration/
diff --git a/design-reference/direction-h-dark.html b/design-reference/direction-h-dark.html
new file mode 100644
index 0000000..4a52f4a
--- /dev/null
+++ b/design-reference/direction-h-dark.html
@@ -0,0 +1,927 @@
+
+
+
+
+
+Expria — Tableau de bord · Direction H : Mode sombre
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Simulations restantes
+
3 / 5
+
+
Plan Découverte — renouvellement à chaque upgrade
+
+
+
+
Niveau estimé
+
NCLC 8
+
+
Moyenne des 3 dernières simulations · Objectif NCLC 9
+
+
+
+
Plan actuel
+
Découverte
+
Débloquez des simulations illimitées, le Mode Examen et le T2 Live avec Standard ou Premium.
+
+ Passer à Standard
+ →
+
+
+
+
+
+
+
+
+
+
+
Simulations récentes
+
Vos 3 dernières corrections
+
+
+
+
+
+
+
Expression écrite — Tâche 2
+
Aujourd'hui · 09:42
+
+
NCLC 9
+
16/20
+
+
+
+
+
+
Expression orale — Tâche 1
+
Il y a 2 jours
+
+
NCLC 8
+
14/20
+
+
+
+
+
+
Expression écrite — Tâche 3
+
Il y a 5 jours
+
+
NCLC 9
+
15/20
+
+
+
+
+
+
+
+
Prochaine étape recommandée
+
Travaillez la tâche 2 à l'oral
+
+ Votre dernier score à l'EO T2 (14/20) est en dessous de votre moyenne. Une simulation de 10 minutes suffit pour consolider.
+
+
+
+
+
+ Commencer maintenant
+ →
+
+
+
+
+
+
+
+
Palette de la direction H — mode sombre
+
+
+
+
#0D1220
+
Fond principal
+
+
+
+
#182238
+
Cards surface
+
+
+
+
#5B7FFF
+
Bleu Expria — remonté
+
+
+
+
#27324B
+
Hairlines
+
+
+
+
+
#A8B2C7
+
Corps secondaire
+
+
+
+
+
#F5B849
+
Attention
+
+
+
+
+
+
+
+
+
+
diff --git a/design-reference/direction-h-juste-milieu.html b/design-reference/direction-h-juste-milieu.html
new file mode 100644
index 0000000..87a062d
--- /dev/null
+++ b/design-reference/direction-h-juste-milieu.html
@@ -0,0 +1,921 @@
+
+
+
+
+
+Expria — Tableau de bord · Direction H : Juste milieu
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Simulations restantes
+
3 / 5
+
+
Plan Découverte — renouvellement à chaque upgrade
+
+
+
+
Niveau estimé
+
NCLC 8
+
+
Moyenne des 3 dernières simulations · Objectif NCLC 9
+
+
+
+
Plan actuel
+
Découverte
+
Débloquez des simulations illimitées, le Mode Examen et le T2 Live avec Standard ou Premium.
+
+ Passer à Standard
+ →
+
+
+
+
+
+
+
+
+
+
+
Simulations récentes
+
Vos 3 dernières corrections
+
+
+
+
+
+
+
Expression écrite — Tâche 2
+
Aujourd'hui · 09:42
+
+
NCLC 9
+
16/20
+
+
+
+
+
+
Expression orale — Tâche 1
+
Il y a 2 jours
+
+
NCLC 8
+
14/20
+
+
+
+
+
+
Expression écrite — Tâche 3
+
Il y a 5 jours
+
+
NCLC 9
+
15/20
+
+
+
+
+
+
+
+
Prochaine étape recommandée
+
Travaillez la tâche 2 à l'oral
+
+ Votre dernier score à l'EO T2 (14/20) est en dessous de votre moyenne. Une simulation de 10 minutes suffit pour consolider.
+
+
+
+
+
+ Commencer maintenant
+ →
+
+
+
+
+
+
+
+
Palette de la direction H
+
+
+
+
#EEF2F8
+
Fond principal
+
+
+
+
#FFFFFF
+
Cards (blanc franc)
+
+
+
+
#1B4FD8
+
Bleu Expria — pivot
+
+
+
+
#0B1F5C
+
Bleu nuit — premium
+
+
+
+
+
+
+
#C77A00
+
Attention
+
+
+
+
+
+
+
+
+
+
diff --git a/docs/DESIGN_SYSTEM.md b/docs/DESIGN_SYSTEM.md
new file mode 100644
index 0000000..3a16386
--- /dev/null
+++ b/docs/DESIGN_SYSTEM.md
@@ -0,0 +1,343 @@
+# DESIGN_SYSTEM.md — Expria Frontend
+
+> **Document de référence — Version 1.0 — Sprint 1**
+> Source de vérité unique pour l'identité visuelle, les tokens de design et les primitives UI.
+> Toute décision de DA doit être consignée ici avant d'être implémentée.
+
+---
+
+## 1. Direction artistique — verrouillée
+
+**Nom :** Boréal
+**Positionnement :** institutionnel chaleureux, premium sans flashy, sérieux sans austère.
+**Référence mentale :** Stripe Dashboard, Linear, Notion Desktop — mais réchauffé d'un cran.
+
+### Parti pris fondateurs
+
+| Principe | Décision |
+|---|---|
+| Mode canonique Sprint 1 | **Clair uniquement** (light chaud) |
+| Mode sombre | Prévu Sprint 2+ (tokens écrits dual-theme-ready dès J1) |
+| Fond principal | `#F4F2EC` (off-white calibré, ni froid ni saturé) |
+| Surfaces élevées | Blanc pur `#FFFFFF` pour contraste subtil avec le fond |
+| Bleu de marque | `#1B4FD8` **sacro-saint** en mode clair — aucune variation |
+| Bleu mode sombre | `#7C9BFF` **prévu** pour Sprint 2+ (pattern Apple system colors) |
+| Accent chaleureux | Aucun en Sprint 1 — le bleu porte toute l'intentionnalité |
+| Angles | Rayons généreux mais retenus : 8 / 12 / 16 px |
+| Ombres | Minimales. 1 ombre-card unique, très subtile. Hairlines 1px privilégiées. |
+| Animations | 150–200 ms, `ease-out`, respect de `prefers-reduced-motion` |
+| Icônes | SVG inline dans `shared/ui/icons/` — aucune dépendance externe |
+| Typographie | Plus Jakarta Sans (via `font-family`, fallback système) |
+
+### Ce qu'on refuse explicitement
+
+- Gradients criards (le seul acceptable : aucun).
+- Glassmorphism ou `backdrop-filter` généralisé — réservé à la bottom nav mobile si besoin.
+- Emojis dans les éléments interactifs ou les labels fonctionnels.
+- Ombres lourdes, "drop shadows" style Material Design 2.
+- Plus de 2 niveaux d'élévation visuelle (fond → card → modal).
+- Toute police de display fantaisiste, serif décorative ou condensée.
+- Les motifs SaaS génériques : illustrations 3D, dégradés violet-rose, glass blobs.
+
+---
+
+## 2. Tokens — `src/index.css`
+
+Remplacer intégralement le contenu actuel (`@import 'tailwindcss';`) par le bloc ci-dessous. Tailwind 4 lit automatiquement les tokens déclarés dans `@theme`.
+
+```css
+@import 'tailwindcss';
+
+@theme {
+ /* ----- Brand ------------------------------------------------------- */
+ --color-brand: #1B4FD8;
+ --color-brand-hover: #1744B8;
+ --color-brand-active: #13379C;
+ --color-brand-soft: #E7EDFC;
+ --color-brand-ink: #FFFFFF;
+
+ /* ----- Surfaces (light — Sprint 1) --------------------------------- */
+ --color-bg: #F4F2EC;
+ --color-surface: #FBFAF6;
+ --color-surface-raised: #FFFFFF;
+ --color-surface-sunken: #EEECE4;
+
+ /* ----- Ink (texte) ------------------------------------------------- */
+ --color-ink-primary: #0F1220;
+ --color-ink-secondary: #4A4F5E;
+ --color-ink-tertiary: #8A8F9E;
+ --color-ink-inverse: #FBFAF6;
+
+ /* ----- Borders & dividers ------------------------------------------ */
+ --color-border: #E3E0D6;
+ --color-border-strong: #C9C5B7;
+ --color-border-focus: #1B4FD8;
+
+ /* ----- Feedback ---------------------------------------------------- */
+ --color-success: #1F7A4C;
+ --color-success-soft: #E3F2EA;
+ --color-warning: #B8741A;
+ --color-warning-soft: #F7EEDF;
+ --color-danger: #B8322D;
+ --color-danger-soft: #F7E1DF;
+
+ /* ----- Typographie ------------------------------------------------- */
+ --font-sans: "Plus Jakarta Sans", system-ui, -apple-system, "Segoe UI", sans-serif;
+ --font-mono: ui-monospace, "SF Mono", "JetBrains Mono", Menlo, monospace;
+
+ /* Échelle : mobile-first, les tailles desktop se gèrent via utilities Tailwind */
+ --text-xs: 11px;
+ --text-sm: 13px;
+ --text-base: 14px;
+ --text-md: 15px;
+ --text-lg: 17px;
+ --text-xl: 20px;
+ --text-2xl: 24px;
+ --text-3xl: 32px;
+ --text-display: 40px;
+
+ /* ----- Rayons ------------------------------------------------------ */
+ --radius-xs: 6px;
+ --radius-sm: 8px;
+ --radius-md: 12px;
+ --radius-lg: 16px;
+ --radius-xl: 20px;
+ --radius-pill: 999px;
+
+ /* ----- Ombres ------------------------------------------------------ */
+ --shadow-card: 0 1px 2px rgba(15, 18, 32, 0.04), 0 1px 8px rgba(15, 18, 32, 0.03);
+ --shadow-raised: 0 4px 16px rgba(15, 18, 32, 0.06), 0 1px 2px rgba(15, 18, 32, 0.04);
+ --shadow-focus: 0 0 0 3px rgba(27, 79, 216, 0.18);
+}
+
+/* --------------------------------------------------------------------- */
+/* Globals — reset minimal, fond chaud par défaut */
+/* --------------------------------------------------------------------- */
+
+html, body {
+ background: var(--color-bg);
+ color: var(--color-ink-primary);
+ font-family: var(--font-sans);
+ -webkit-font-smoothing: antialiased;
+ -moz-osx-font-smoothing: grayscale;
+ font-variant-numeric: tabular-nums;
+}
+
+*:focus-visible {
+ outline: none;
+ box-shadow: var(--shadow-focus);
+ border-radius: var(--radius-xs);
+}
+
+@media (prefers-reduced-motion: reduce) {
+ *, *::before, *::after {
+ animation-duration: 0ms !important;
+ transition-duration: 0ms !important;
+ }
+}
+
+/* --------------------------------------------------------------------- */
+/* TODO Sprint 2+ — Dark theme (Cadence recalibré) */
+/* --------------------------------------------------------------------- */
+/*
+@theme {
+ --color-bg-dark: #0F1320;
+ --color-surface-dark: #171B2B;
+ --color-surface-raised-dark: #1E2338;
+ --color-ink-primary-dark: #E6E4DB;
+ --color-ink-secondary-dark: #9CA0AC;
+ --color-brand-dark: #7C9BFF;
+ --color-brand-hover-dark: #9AB3FF;
+ --color-border-dark: #2A3048;
+}
+*/
+```
+
+### Règles d'usage des tokens
+
+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+.
+
+---
+
+## 3. Typographie
+
+| Usage | Taille | Poids | Tracking | Ligne | Token |
+|---|---|---|---|---|---|
+| Display (NCLC hero) | 40px | 700 | -0.02em | 1.0 | `text-display` |
+| H1 page | 32px | 700 | -0.02em | 1.1 | `text-3xl` |
+| H2 section | 24px | 700 | -0.015em | 1.2 | `text-2xl` |
+| H3 card title | 20px | 700 | -0.01em | 1.3 | `text-xl` |
+| Lead / intro | 17px | 500 | -0.005em | 1.5 | `text-lg` |
+| Body | 14px | 400 | 0 | 1.6 | `text-base` |
+| Body renforcé | 15px | 500 | 0 | 1.55 | `text-md` |
+| Small / meta | 13px | 400 | 0 | 1.5 | `text-sm` |
+| Eyebrow / label | 11px | 600 | 0.1em (uppercase) | 1.4 | `text-xs` |
+
+**Règles :**
+- Tout nombre métier (score 16/20, NCLC 7,5, compteur 3/5) est rendu en `font-variant-numeric: tabular-nums`. Hérité par le body, mais à re-spécifier explicitement sur les tables et listes.
+- `Plus Jakarta 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).
+- Les chiffres français utilisent la **virgule** comme séparateur décimal (`7,5`, jamais `7.5`).
+
+---
+
+## 4. Primitives UI — Sprint 1
+
+À créer dans `src/shared/ui/` en FSD, une primitive par dossier (`button/`, `card/`, etc.) avec `index.ts` pour l'export.
+
+### Inventaire minimal
+
+| Composant | Variants | Usage dashboard |
+|---|---|---|
+| `Button` | `primary` / `secondary` / `ghost` / `upgrade` | CTA "Nouvelle simulation", "Passer au plan Standard", actions tertiaires |
+| `Card` | `default` / `raised` / `interactive` | Cadre métriques, item simulation, recommandation |
+| `MetricCard` | `default` / `hero` (pour le NCLC) | Bloc NCLC, compteur simulations, dernier score |
+| `ProgressBar` | `default` | Progression vers NCLC 9 |
+| `Badge` | `plan` / `nclc` / `neutral` | Plan actuel dans header, niveau NCLC par simulation |
+| `Sidebar` | — | Nav desktop (≥ 1024px) |
+| `BottomNav` | — | Nav mobile (< 1024px), 4 items max |
+| `PageHeader` | — | Greeting + plan pill |
+| `SectionHeader` | — | Titre de section + action optionnelle |
+
+### Règles d'implémentation
+
+- Chaque primitive **accepte `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.
+
+---
+
+## 5. Layout dashboard — spécification
+
+Les primitives ci-dessus s'assemblent dans `src/features/dashboard/` et `src/pages/dashboard/`.
+
+### Structure sémantique
+
+```
+
+ (≥ 1024px)
+
+ (greeting + plan)
+
+ (NCLC estimé + progression)
+ (simulations restantes)
+ (dernier score)
+
+ (Nouvelle simulation)
+ (Passer au plan Standard)
+
+ (3 dernières simulations)
+ × 3
+
+
+ (Prochaine étape recommandée)
+
+
+
+ (< 1024px)
+
+```
+
+### Breakpoints
+
+| Breakpoint | Comportement |
+|---|---|
+| `< 1024px` | Mono-colonne, `BottomNav` fixe en bas, padding horizontal 20px |
+| `≥ 1024px` | Sidebar 240px + contenu centré 860px max, padding horizontal 32px |
+| `≥ 1440px` | Contenu centré 920px max (pas d'élargissement excessif) |
+
+### Densité verticale
+
+- Padding vertical section : 24px mobile, 32px desktop.
+- Gap inter-cards : 12px mobile, 16px desktop.
+- Marge sous `PageHeader` : 20px mobile, 28px desktop.
+
+---
+
+## 6. Données mock — Sprint 1
+
+Avant branchement API, fournir les données via `src/shared/api/mock/dashboard.ts`. Données crédibles, françaises, alignées sur l'audience réelle.
+
+```typescript
+export const mockDashboard = {
+ user: {
+ firstName: 'Yacine',
+ plan: 'decouverte' as const,
+ planLabel: 'Plan Découverte',
+ },
+ metrics: {
+ nclcEstimated: 7.5,
+ nclcTarget: 9,
+ simulationsUsed: 2,
+ simulationsQuota: 5,
+ lastScore: { value: 16, max: 20, type: 'ecrit' as const },
+ },
+ recentSimulations: [
+ { id: 's-001', type: 'ecrit', relativeDate: 'il y a 2 jours', score: 16, max: 20, nclc: 8 },
+ { id: 's-002', type: 'oral', relativeDate: 'il y a 5 jours', score: 14, max: 20, nclc: 7 },
+ { id: 's-003', type: 'ecrit', relativeDate: 'il y a 1 semaine', score: 15, max: 20, nclc: 7 },
+ ],
+ nextStep: {
+ title: 'Cible une simulation orale cette semaine',
+ body: 'Ton écrit est solide (NCLC 8). L\'oral reste à consolider pour sécuriser ton NCLC 9.',
+ action: { label: 'Démarrer Expression Orale', to: '/simulation/orale' },
+ },
+} as const;
+```
+
+**Règles contenu :**
+- Aucun "Lorem ipsum", aucune date absolue — relatif uniquement (`il y a X jours`).
+- Les prénoms mocks reflètent l'audience : Yacine, Aminata, Kenza, Bilal, Fatou, Kévin (Canada).
+- Les scores suivent une progression crédible (pas de 20/20 ni de 5/20).
+
+---
+
+## 7. Accessibilité — plancher Sprint 1
+
+- Contraste minimum **WCAG AA** sur tous les couples texte/fond (vérifié pour la palette ci-dessus).
+- Tous les éléments interactifs ont un `:focus-visible` avec `--shadow-focus` (halo bleu 3px).
+- Les icônes purement décoratives portent `aria-hidden="true"`.
+- Les icônes fonctionnelles (sans label visible) portent `aria-label`.
+- Les landmarks sémantiques sont utilisés : ``, ``, ``, ``.
+- Le `BottomNav` mobile respecte la hauteur minimale tap target : **44×44 px** par item.
+
+---
+
+## 8. Dépendances externes — Sprint 1
+
+**Aucune nouvelle dépendance UI n'est ajoutée en Sprint 1.** On n'installe pas `shadcn/ui`, pas `lucide-react`, pas `radix-ui`. Les primitives sont écrites à la main pour deux raisons :
+
+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.
+
+---
+
+## 9. Journal des décisions DA
+
+| Date | Décision | Contexte |
+|---|---|---|
+| 2026-04-17 | Direction A (Boréal) validée comme canonique | 5 directions explorées (A/B/E/F/G), A et B retenues, A choisie comme base |
+| 2026-04-17 | Fond `#F4F2EC` (V1 calibré) retenu | Test côte-à-côte contre `#F5F3ED` et `#F2EFE6` |
+| 2026-04-17 | Dark mode (Cadence) reporté Sprint 2+ | Tokens écrits dual-theme-ready dès J1 pour éviter réécriture |
+| 2026-04-17 | Bleu `#1B4FD8` sacro-saint en light, `#7C9BFF` prévu pour dark | Pattern Apple system colors |
+| 2026-04-17 | Pas de shadcn/ui en Sprint 1 | Volume faible, maîtrise totale souhaitée |
+
+---
+
+## 10. Hors périmètre Sprint 1
+
+Éléments explicitement **reportés** :
+
+- Dark mode applicatif.
+- Thème haut-contraste (WCAG AAA).
+- Internationalisation (i18n) — Sprint 1 monolingue FR.
+- Animations avancées (scroll-linked, shared element transitions).
+- Illustrations personnalisées / iconographie signature.
+- Self-hosting de la font Plus Jakarta Sans (on reste en fallback système Sprint 1).
+
+Chacun de ces points mérite un ADR dédié quand il sera abordé.
diff --git a/docs/adr/006-stack-versions-2026.md b/docs/adr/006-stack-versions-2026.md
index 52a63b8..7fb587a 100644
--- a/docs/adr/006-stack-versions-2026.md
+++ b/docs/adr/006-stack-versions-2026.md
@@ -1,6 +1,6 @@
# ADR 006 — Stack frontend : versions 2026 (React 19, Vite 8, TypeScript 6, Tailwind 4, RR7)
-**Statut :** Accepté
+**Statut :** Accepté — mis à jour Sprint 0.5
**Date :** 2026-04-17
**Décideur :** Hermann
**Contexte :** Révélé par l'état des lieux Claude Code au démarrage du Sprint 0 frontend
@@ -105,18 +105,110 @@ Garder React 19, Vite 8, TypeScript 6 mais downgrader Tailwind 4 → 3 pour "com
### Configuration Tailwind 4
-Pas de `tailwind.config.ts`. La configuration se fait exclusivement dans `src/index.css` via les directives :
+Pas de `tailwind.config.ts`. La configuration se fait exclusivement dans `src/index.css`.
+
+#### Dark mode
+
+Dark mode class-based (`.dark` sur ``) — toggle manuel via ThemeProvider React (Sprint 0.5 étape 2). Configuré via :
```css
-@import "tailwindcss";
+@variant dark (&:where(.dark, .dark *));
+```
+
+Si cette syntaxe est rejetée par une future version de Tailwind 4, le fallback est `@custom-variant dark (...)`.
+
+#### Tokens @theme (palette Direction H — validée Sprint 0.5)
+
+```css
+@import 'tailwindcss';
+
+@variant dark (&:where(.dark, .dark *));
@theme {
- --color-primary: #1B4FD8; /* Couleur brand Expria */
- --font-sans: "Plus Jakarta Sans", system-ui, sans-serif;
- /* Autres variables de thème */
+ /* Typographie */
+ --font-sans: "Plus Jakarta Sans", -apple-system, BlinkMacSystemFont, "Segoe UI",
+ system-ui, sans-serif;
+
+ /* 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 */
+
+ /* 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;
+
+ /* Sémantiques */
+ --color-success: #0E9F6E; --color-success-bg: #E6F6F0;
+ --color-warning: #C77A00; --color-warning-bg: #FEF3E2;
+ --color-danger: #C53030; --color-danger-bg: #FDECEC;
+
+ /* Rayons */
+ --radius-sm: 6px; --radius-md: 10px;
+ --radius-lg: 14px; --radius-xl: 18px; --radius-full: 999px;
+
+ /* 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);
+}
+
+/* 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);
}
```
+#### Classes Tailwind générées
+
+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) |
+
+**Convention critique** : `bg-surface` = cards / modals / panels. `bg-canvas` = fond de page. Ne jamais inverser.
+
+#### Typographie
+
+Plus Jakarta Sans chargée via Google Fonts dans `index.html` (preconnect + stylesheet, weights 400/500/600/700). Migration vers auto-hébergement (`@fontsource/plus-jakarta-sans`) après MVP si les performances réseau deviennent un enjeu.
+
### shadcn/ui avec Tailwind 4
La CLI shadcn/ui supporte Tailwind 4 depuis début 2025 :
diff --git a/index.html b/index.html
index fa5268e..519ef0f 100644
--- a/index.html
+++ b/index.html
@@ -5,6 +5,12 @@
Expria
+
+
+
diff --git a/src/features/auth/components/ProtectedRoute.tsx b/src/features/auth/components/ProtectedRoute.tsx
new file mode 100644
index 0000000..b1081e5
--- /dev/null
+++ b/src/features/auth/components/ProtectedRoute.tsx
@@ -0,0 +1,42 @@
+/**
+ * Wrapper de route qui exige un utilisateur authentifié.
+ *
+ * - Pendant le chargement de la session : affiche un spinner centré.
+ * - Si non authentifié : redirige vers `/login` avec replace (pas d'entrée
+ * parasite dans l'historique navigateur).
+ * - Si authentifié : rend les `children`.
+ *
+ * Le backend reste l'autorité finale : cette garde est de l'UX. Les routes
+ * sensibles sont protégées par les middlewares Hono côté API (ADR 002).
+ */
+
+import { Navigate } from 'react-router-dom'
+import { Loader2 } from 'lucide-react'
+import { useAuth } from '../hooks/useAuth'
+
+interface ProtectedRouteProps {
+ children: React.ReactNode
+}
+
+export function ProtectedRoute({ children }: ProtectedRouteProps) {
+ const { isLoading, isAuthenticated } = useAuth()
+
+ if (isLoading) {
+ return (
+
+
+
+ )
+ }
+
+ if (!isAuthenticated) {
+ return
+ }
+
+ return <>{children}>
+}
diff --git a/src/features/auth/hooks/useAuth.ts b/src/features/auth/hooks/useAuth.ts
new file mode 100644
index 0000000..5de5968
--- /dev/null
+++ b/src/features/auth/hooks/useAuth.ts
@@ -0,0 +1,54 @@
+/**
+ * Hook source de vérité pour l'état d'authentification dans toute l'app.
+ *
+ * Au mount : récupère la session courante depuis Supabase (cookie + localStorage).
+ * S'abonne ensuite à `onAuthStateChange` pour réagir aux login/logout/refresh
+ * token ; se désabonne au unmount.
+ *
+ * Consommé par `ProtectedRoute` (redirect si non authentifié) et par toute page
+ * qui a besoin du profil Supabase (ex. prénom affiché dans le header).
+ */
+
+import { useEffect, useState } from 'react'
+import {
+ getCurrentSession,
+ subscribeToAuthChanges,
+ type User,
+} from '@/shared/lib/auth-client'
+
+interface UseAuthResult {
+ user: User | null
+ isLoading: boolean
+ isAuthenticated: boolean
+}
+
+export function useAuth(): UseAuthResult {
+ const [user, setUser] = useState(null)
+ const [isLoading, setIsLoading] = useState(true)
+
+ useEffect(() => {
+ let cancelled = false
+
+ getCurrentSession().then((session) => {
+ if (cancelled) return
+ setUser(session?.user ?? null)
+ setIsLoading(false)
+ })
+
+ const unsubscribe = subscribeToAuthChanges((session) => {
+ setUser(session?.user ?? null)
+ setIsLoading(false)
+ })
+
+ return () => {
+ cancelled = true
+ unsubscribe()
+ }
+ }, [])
+
+ return {
+ user,
+ isLoading,
+ isAuthenticated: user !== null,
+ }
+}
diff --git a/src/features/design-system/DesignSystemPage.tsx b/src/features/design-system/DesignSystemPage.tsx
index 27edc6f..74ce2a8 100644
--- a/src/features/design-system/DesignSystemPage.tsx
+++ b/src/features/design-system/DesignSystemPage.tsx
@@ -26,29 +26,39 @@ import {
// ─── palette data ────────────────────────────────────────────────────────────
const PALETTE: { token: string; var: string; light: string; dark: string }[] = [
- { token: 'canvas', var: '--color-canvas', light: '#EEF2F8', dark: '#0D1220' },
- { token: 'canvas-2', var: '--color-canvas-2', light: '#E6EBF4', dark: '#121A2D' },
- { token: 'surface', var: '--color-surface', light: '#FFFFFF', dark: '#182238' },
- { token: 'surface-hover',var: '--color-surface-hover',light: '#F8FAFD', dark: '#1E2A42' },
- { token: 'line', var: '--color-line', light: '#DDE3ED', dark: '#27324B' },
- { token: 'line-strong', var: '--color-line-strong', light: '#C7D0E0', dark: '#364363' },
- { token: 'ink-1', var: '--color-ink-1', light: '#0F172A', dark: '#F1F4FA' },
- { token: 'ink-2', var: '--color-ink-2', light: '#1E293B', dark: '#DDE3EF' },
- { token: 'ink-3', var: '--color-ink-3', light: '#475569', dark: '#A8B2C7' },
- { token: 'ink-4', var: '--color-ink-4', light: '#64748B', dark: '#7A8499' },
- { token: 'ink-5', var: '--color-ink-5', light: '#94A3B8', dark: '#525C73' },
- { token: 'expria', var: '--color-expria', light: '#1B4FD8', dark: '#5B7FFF' },
+ { token: 'canvas', var: '--color-canvas', light: '#EEF2F8', dark: '#0D1220' },
+ { token: 'canvas-2', var: '--color-canvas-2', light: '#E6EBF4', dark: '#121A2D' },
+ { token: 'surface', var: '--color-surface', light: '#FFFFFF', dark: '#182238' },
+ { token: 'surface-hover', var: '--color-surface-hover', light: '#F8FAFD', dark: '#1E2A42' },
+ { token: 'line', var: '--color-line', light: '#DDE3ED', dark: '#27324B' },
+ { token: 'line-strong', var: '--color-line-strong', light: '#C7D0E0', dark: '#364363' },
+ { token: 'ink-1', var: '--color-ink-1', light: '#0F172A', dark: '#F1F4FA' },
+ { token: 'ink-2', var: '--color-ink-2', light: '#1E293B', dark: '#DDE3EF' },
+ { token: 'ink-3', var: '--color-ink-3', light: '#475569', dark: '#A8B2C7' },
+ { token: 'ink-4', var: '--color-ink-4', light: '#64748B', dark: '#7A8499' },
+ { token: 'ink-5', var: '--color-ink-5', light: '#94A3B8', dark: '#525C73' },
+ { token: 'expria', var: '--color-expria', light: '#1B4FD8', dark: '#5B7FFF' },
{ token: 'expria-hover', var: '--color-expria-hover', light: '#1741B8', dark: '#6F8EFF' },
- { token: 'expria-50', var: '--color-expria-50', light: '#EEF3FF', dark: 'rgba(91,127,255,.12)' },
- { token: 'expria-100', var: '--color-expria-100', light: '#DCE6FF', dark: '—' },
- { token: 'expria-200', var: '--color-expria-200', light: '#B8CDFF', dark: '—' },
- { token: 'deep', var: '--color-deep', light: '#0B1F5C', dark: '#060B1A' },
- { token: 'success', var: '--color-success', light: '#0E9F6E', dark: '#3DD68C' },
- { token: 'success-bg', var: '--color-success-bg', light: '#E6F6F0', dark: 'rgba(61,214,140,.12)' },
- { token: 'warning', var: '--color-warning', light: '#C77A00', dark: '#F5B849' },
- { token: 'warning-bg', var: '--color-warning-bg', light: '#FEF3E2', dark: 'rgba(245,184,73,.12)' },
- { token: 'danger', var: '--color-danger', light: '#C53030', dark: '#F06B6B' },
- { token: 'danger-bg', var: '--color-danger-bg', light: '#FDECEC', dark: 'rgba(240,107,107,.12)' },
+ { token: 'expria-50', var: '--color-expria-50', light: '#EEF3FF', dark: 'rgba(91,127,255,.12)' },
+ { token: 'expria-100', var: '--color-expria-100', light: '#DCE6FF', dark: '—' },
+ { token: 'expria-200', var: '--color-expria-200', light: '#B8CDFF', dark: '—' },
+ { token: 'deep', var: '--color-deep', light: '#0B1F5C', dark: '#060B1A' },
+ { token: 'success', var: '--color-success', light: '#0E9F6E', dark: '#3DD68C' },
+ {
+ token: 'success-bg',
+ var: '--color-success-bg',
+ light: '#E6F6F0',
+ dark: 'rgba(61,214,140,.12)',
+ },
+ { token: 'warning', var: '--color-warning', light: '#C77A00', dark: '#F5B849' },
+ {
+ token: 'warning-bg',
+ var: '--color-warning-bg',
+ light: '#FEF3E2',
+ dark: 'rgba(245,184,73,.12)',
+ },
+ { token: 'danger', var: '--color-danger', light: '#C53030', dark: '#F06B6B' },
+ { token: 'danger-bg', var: '--color-danger-bg', light: '#FDECEC', dark: 'rgba(240,107,107,.12)' },
]
// ─── section wrapper ─────────────────────────────────────────────────────────
@@ -71,7 +81,6 @@ export default function DesignSystemPage() {
return (
Disabled
- Outline disabled
+
+ Outline disabled
+
@@ -208,7 +215,7 @@ export default function DesignSystemPage() {
- {['AB', 'CD', 'EF'].map(initials => (
+ {['AB', 'CD', 'EF'].map((initials) => (
{initials}
@@ -231,8 +238,8 @@ export default function DesignSystemPage() {
Example dialog
- This dialog uses Direction H tokens — bg-surface, border-line, text-ink-4.
- Toggle the theme to see it adapt.
+ This dialog uses Direction H tokens — bg-surface, border-line, text-ink-4. Toggle
+ the theme to see it adapt.
@@ -242,7 +249,6 @@ export default function DesignSystemPage() {
-
)
}
diff --git a/src/index.css b/src/index.css
index 558ede0..17e6743 100644
--- a/src/index.css
+++ b/src/index.css
@@ -5,50 +5,50 @@
@theme {
/* ─── Typographie ───────────────────────────────────────────── */
- --font-sans: "Plus Jakarta Sans", -apple-system, BlinkMacSystemFont, "Segoe UI",
- system-ui, sans-serif;
+ --font-sans:
+ 'Plus Jakarta Sans', -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif;
/* ─── Fonds ─────────────────────────────────────────────────── */
/* bg-canvas = fond de page (jamais pur blanc) */
/* bg-surface = cards — ressortent sur le canvas */
- --color-canvas: #EEF2F8;
- --color-canvas-2: #E6EBF4;
- --color-surface: #FFFFFF;
- --color-surface-hover: #F8FAFD;
+ --color-canvas: #eef2f8;
+ --color-canvas-2: #e6ebf4;
+ --color-surface: #ffffff;
+ --color-surface-hover: #f8fafd;
/* ─── Hairlines ──────────────────────────────────────────────── */
- --color-line: #DDE3ED;
- --color-line-strong: #C7D0E0;
+ --color-line: #dde3ed;
+ --color-line-strong: #c7d0e0;
/* ─── Encres ─────────────────────────────────────────────────── */
- --color-ink-1: #0F172A;
- --color-ink-2: #1E293B;
+ --color-ink-1: #0f172a;
+ --color-ink-2: #1e293b;
--color-ink-3: #475569;
- --color-ink-4: #64748B;
- --color-ink-5: #94A3B8;
+ --color-ink-4: #64748b;
+ --color-ink-5: #94a3b8;
/* ─── Brand Expria ───────────────────────────────────────────── */
- --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-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;
/* ─── Sémantiques ────────────────────────────────────────────── */
- --color-success: #0E9F6E;
- --color-success-bg: #E6F6F0;
- --color-warning: #C77A00;
- --color-warning-bg: #FEF3E2;
- --color-danger: #C53030;
- --color-danger-bg: #FDECEC;
+ --color-success: #0e9f6e;
+ --color-success-bg: #e6f6f0;
+ --color-warning: #c77a00;
+ --color-warning-bg: #fef3e2;
+ --color-danger: #c53030;
+ --color-danger-bg: #fdecec;
/* ─── Rayons (override des defaults Tailwind) ────────────────── */
- --radius-sm: 6px;
- --radius-md: 10px;
- --radius-lg: 14px;
- --radius-xl: 18px;
+ --radius-sm: 6px;
+ --radius-md: 10px;
+ --radius-lg: 14px;
+ --radius-xl: 18px;
--radius-full: 999px;
/* ─── Ombres (light mode) ────────────────────────────────────── */
@@ -60,35 +60,35 @@
/* ─── Dark mode — override des tokens couleur et ombres ──────────── */
.dark {
/* Fonds */
- --color-canvas: #0D1220;
- --color-canvas-2: #121A2D;
- --color-surface: #182238;
- --color-surface-hover: #1E2A42;
+ --color-canvas: #0d1220;
+ --color-canvas-2: #121a2d;
+ --color-surface: #182238;
+ --color-surface-hover: #1e2a42;
/* Hairlines */
- --color-line: #27324B;
- --color-line-strong: #364363;
+ --color-line: #27324b;
+ --color-line-strong: #364363;
/* Encres */
- --color-ink-1: #F1F4FA;
- --color-ink-2: #DDE3EF;
- --color-ink-3: #A8B2C7;
- --color-ink-4: #7A8499;
- --color-ink-5: #525C73;
+ --color-ink-1: #f1f4fa;
+ --color-ink-2: #dde3ef;
+ --color-ink-3: #a8b2c7;
+ --color-ink-4: #7a8499;
+ --color-ink-5: #525c73;
/* Brand — remonté en luminance pour rester lisible sur fond sombre */
- --color-expria: #5B7FFF;
- --color-expria-hover: #6F8EFF;
- --color-expria-50: rgba(91, 127, 255, 0.12);
- --color-deep: #060B1A;
+ --color-expria: #5b7fff;
+ --color-expria-hover: #6f8eff;
+ --color-expria-50: rgba(91, 127, 255, 0.12);
+ --color-deep: #060b1a;
/* Sémantiques */
- --color-success: #3DD68C;
+ --color-success: #3dd68c;
--color-success-bg: rgba(61, 214, 140, 0.12);
- --color-warning: #F5B849;
+ --color-warning: #f5b849;
--color-warning-bg: rgba(245, 184, 73, 0.12);
- --color-danger: #F06B6B;
- --color-danger-bg: rgba(240, 107, 107, 0.12);
+ --color-danger: #f06b6b;
+ --color-danger-bg: rgba(240, 107, 107, 0.12);
/* Ombres — jouer sur les surfaces, pas les ombres claires */
--shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.3);
diff --git a/src/shared/components/Logo.tsx b/src/shared/components/Logo.tsx
index f1573d5..dd31c3e 100644
--- a/src/shared/components/Logo.tsx
+++ b/src/shared/components/Logo.tsx
@@ -24,7 +24,7 @@ export function Logo({ size = 'md', variant = 'full', className }: LogoProps) {
diff --git a/src/shared/components/ui/avatar.tsx b/src/shared/components/ui/avatar.tsx
index 099636e..0eaaf0a 100644
--- a/src/shared/components/ui/avatar.tsx
+++ b/src/shared/components/ui/avatar.tsx
@@ -1,36 +1,33 @@
-import * as React from "react"
-import { Avatar as AvatarPrimitive } from "radix-ui"
+import * as React from 'react'
+import { Avatar as AvatarPrimitive } from 'radix-ui'
-import { cn } from "@/shared/lib/utils"
+import { cn } from '@/shared/lib/utils'
function Avatar({
className,
- size = "default",
+ size = 'default',
...props
}: React.ComponentProps
& {
- size?: "default" | "sm" | "lg"
+ size?: 'default' | 'sm' | 'lg'
}) {
return (
)
}
-function AvatarImage({
- className,
- ...props
-}: React.ComponentProps) {
+function AvatarImage({ className, ...props }: React.ComponentProps) {
return (
)
@@ -44,64 +41,54 @@ function AvatarFallback({
)
}
-function AvatarBadge({ className, ...props }: React.ComponentProps<"span">) {
+function AvatarBadge({ className, ...props }: React.ComponentProps<'span'>) {
return (
svg]:hidden",
- "group-data-[size=default]/avatar:size-2.5 group-data-[size=default]/avatar:[&>svg]:size-2",
- "group-data-[size=lg]/avatar:size-3 group-data-[size=lg]/avatar:[&>svg]:size-2",
- className
+ 'absolute right-0 bottom-0 z-10 inline-flex items-center justify-center rounded-full bg-expria text-white ring-2 ring-canvas select-none',
+ 'group-data-[size=sm]/avatar:size-2 group-data-[size=sm]/avatar:[&>svg]:hidden',
+ 'group-data-[size=default]/avatar:size-2.5 group-data-[size=default]/avatar:[&>svg]:size-2',
+ 'group-data-[size=lg]/avatar:size-3 group-data-[size=lg]/avatar:[&>svg]:size-2',
+ className,
)}
{...props}
/>
)
}
-function AvatarGroup({ className, ...props }: React.ComponentProps<"div">) {
+function AvatarGroup({ className, ...props }: React.ComponentProps<'div'>) {
return (
)
}
-function AvatarGroupCount({
- className,
- ...props
-}: React.ComponentProps<"div">) {
+function AvatarGroupCount({ className, ...props }: React.ComponentProps<'div'>) {
return (
svg]:size-4 group-has-data-[size=lg]/avatar-group:[&>svg]:size-5 group-has-data-[size=sm]/avatar-group:[&>svg]:size-3",
- className
+ 'relative flex size-8 shrink-0 items-center justify-center rounded-full bg-canvas-2 text-sm text-ink-4 ring-2 ring-canvas group-has-data-[size=lg]/avatar-group:size-10 group-has-data-[size=sm]/avatar-group:size-6 [&>svg]:size-4 group-has-data-[size=lg]/avatar-group:[&>svg]:size-5 group-has-data-[size=sm]/avatar-group:[&>svg]:size-3',
+ className,
)}
{...props}
/>
)
}
-export {
- Avatar,
- AvatarImage,
- AvatarFallback,
- AvatarBadge,
- AvatarGroup,
- AvatarGroupCount,
-}
+export { Avatar, AvatarImage, AvatarFallback, AvatarBadge, AvatarGroup, AvatarGroupCount }
diff --git a/src/shared/components/ui/dialog.tsx b/src/shared/components/ui/dialog.tsx
index bd1efc6..96fc57d 100644
--- a/src/shared/components/ui/dialog.tsx
+++ b/src/shared/components/ui/dialog.tsx
@@ -1,31 +1,23 @@
-import * as React from "react"
-import { XIcon } from "lucide-react"
-import { Dialog as DialogPrimitive } from "radix-ui"
+import * as React from 'react'
+import { XIcon } from 'lucide-react'
+import { Dialog as DialogPrimitive } from 'radix-ui'
-import { cn } from "@/shared/lib/utils"
-import { Button } from "@/shared/components/ui/button"
+import { cn } from '@/shared/lib/utils'
+import { Button } from '@/shared/components/ui/button'
-function Dialog({
- ...props
-}: React.ComponentProps
) {
+function Dialog({ ...props }: React.ComponentProps) {
return
}
-function DialogTrigger({
- ...props
-}: React.ComponentProps) {
+function DialogTrigger({ ...props }: React.ComponentProps) {
return
}
-function DialogPortal({
- ...props
-}: React.ComponentProps) {
+function DialogPortal({ ...props }: React.ComponentProps) {
return
}
-function DialogClose({
- ...props
-}: React.ComponentProps) {
+function DialogClose({ ...props }: React.ComponentProps) {
return
}
@@ -37,8 +29,8 @@ function DialogOverlay({
@@ -59,8 +51,8 @@ function DialogContent({
@@ -79,11 +71,11 @@ function DialogContent({
)
}
-function DialogHeader({ className, ...props }: React.ComponentProps<"div">) {
+function DialogHeader({ className, ...props }: React.ComponentProps<'div'>) {
return (
)
@@ -94,16 +86,13 @@ function DialogFooter({
showCloseButton = false,
children,
...props
-}: React.ComponentProps<"div"> & {
+}: React.ComponentProps<'div'> & {
showCloseButton?: boolean
}) {
return (
{children}
@@ -116,14 +105,11 @@ function DialogFooter({
)
}
-function DialogTitle({
- className,
- ...props
-}: React.ComponentProps
) {
+function DialogTitle({ className, ...props }: React.ComponentProps) {
return (
)
@@ -136,7 +122,7 @@ function DialogDescription({
return (
)
diff --git a/src/shared/components/ui/input.tsx b/src/shared/components/ui/input.tsx
index 536184c..8a0563e 100644
--- a/src/shared/components/ui/input.tsx
+++ b/src/shared/components/ui/input.tsx
@@ -1,17 +1,17 @@
-import * as React from "react"
+import * as React from 'react'
-import { cn } from "@/shared/lib/utils"
+import { cn } from '@/shared/lib/utils'
-function Input({ className, type, ...props }: React.ComponentProps<"input">) {
+function Input({ className, type, ...props }: React.ComponentProps<'input'>) {
return (
diff --git a/src/shared/components/ui/label.tsx b/src/shared/components/ui/label.tsx
index f61345a..2cebcda 100644
--- a/src/shared/components/ui/label.tsx
+++ b/src/shared/components/ui/label.tsx
@@ -1,18 +1,15 @@
-import * as React from "react"
-import { Label as LabelPrimitive } from "radix-ui"
+import * as React from 'react'
+import { Label as LabelPrimitive } from 'radix-ui'
-import { cn } from "@/shared/lib/utils"
+import { cn } from '@/shared/lib/utils'
-function Label({
- className,
- ...props
-}: React.ComponentProps) {
+function Label({ className, ...props }: React.ComponentProps) {
return (
diff --git a/src/shared/components/ui/progress.tsx b/src/shared/components/ui/progress.tsx
index 5e2c5cc..870b81f 100644
--- a/src/shared/components/ui/progress.tsx
+++ b/src/shared/components/ui/progress.tsx
@@ -1,7 +1,7 @@
-import * as React from "react"
-import { Progress as ProgressPrimitive } from "radix-ui"
+import * as React from 'react'
+import { Progress as ProgressPrimitive } from 'radix-ui'
-import { cn } from "@/shared/lib/utils"
+import { cn } from '@/shared/lib/utils'
function Progress({
className,
@@ -11,10 +11,7 @@ function Progress({
return (
) {
@@ -15,8 +15,8 @@ function Separator({
decorative={decorative}
orientation={orientation}
className={cn(
- "shrink-0 bg-line data-[orientation=horizontal]:h-px data-[orientation=horizontal]:w-full data-[orientation=vertical]:h-full data-[orientation=vertical]:w-px",
- className
+ 'shrink-0 bg-line data-[orientation=horizontal]:h-px data-[orientation=horizontal]:w-full data-[orientation=vertical]:h-full data-[orientation=vertical]:w-px',
+ className,
)}
{...props}
/>
diff --git a/src/shared/lib/auth-client.ts b/src/shared/lib/auth-client.ts
index de6cca3..3d7c73b 100644
--- a/src/shared/lib/auth-client.ts
+++ b/src/shared/lib/auth-client.ts
@@ -1,4 +1,4 @@
-import { createClient } from '@supabase/supabase-js'
+import { createClient, type Session, type User } from '@supabase/supabase-js'
import { env } from '@/shared/config/env'
import { logger } from './logger'
@@ -17,6 +17,32 @@ export async function signIn(email: string, password: string) {
return supabase.auth.signInWithPassword({ email, password })
}
+export async function signUp(email: string, password: string) {
+ return supabase.auth.signUp({ email, password })
+}
+
export async function signOut() {
return supabase.auth.signOut()
}
+
+export async function getCurrentSession(): Promise {
+ const { data, error } = await supabase.auth.getSession()
+ if (error) {
+ logger.error('Auth session fetch failed', { name: error.name })
+ return null
+ }
+ return data.session
+}
+
+/**
+ * S'abonne aux changements d'état d'authentification Supabase.
+ * Retourne une fonction de désabonnement à appeler au cleanup.
+ */
+export function subscribeToAuthChanges(callback: (session: Session | null) => void): () => void {
+ const { data } = supabase.auth.onAuthStateChange((_event, session) => {
+ callback(session)
+ })
+ return () => data.subscription.unsubscribe()
+}
+
+export type { Session, User }