feat(auth): useAuth + ProtectedRoute + signUp dans auth-client (Sprint 1 étape 2)
This commit is contained in:
parent
107a37d197
commit
38777796aa
19 changed files with 2620 additions and 191 deletions
42
.cursor/skills/frontend-design/SKILL.md
Normal file
42
.cursor/skills/frontend-design/SKILL.md
Normal file
|
|
@ -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.
|
||||||
3
.gitignore
vendored
3
.gitignore
vendored
|
|
@ -30,3 +30,6 @@ dist-ssr
|
||||||
|
|
||||||
# Claude Code local config
|
# Claude Code local config
|
||||||
.claude/
|
.claude/
|
||||||
|
|
||||||
|
# Exploration DA temporaire — supprimer une fois la direction choisie
|
||||||
|
design-exploration/
|
||||||
|
|
|
||||||
927
design-reference/direction-h-dark.html
Normal file
927
design-reference/direction-h-dark.html
Normal file
|
|
@ -0,0 +1,927 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="fr">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Expria — Tableau de bord · Direction H : Mode sombre</title>
|
||||||
|
<style>
|
||||||
|
/* =========================================================
|
||||||
|
DIRECTION H — MODE SOMBRE
|
||||||
|
Inversion réfléchie de la palette claire :
|
||||||
|
- Fond bleu-nuit désaturé (pas noir pur = fatigue visuelle)
|
||||||
|
- Cards en bleu-slate légèrement plus clair pour ressortir
|
||||||
|
- Bleu Expria remonté en luminance (lisibilité sur fond sombre)
|
||||||
|
- Hairlines plus visibles qu'en clair pour structurer
|
||||||
|
- Titres en blanc cassé (jamais pur blanc = trop agressif)
|
||||||
|
========================================================= */
|
||||||
|
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||||||
|
|
||||||
|
:root {
|
||||||
|
/* Fonds — hiérarchie 3 niveaux pour créer la profondeur */
|
||||||
|
--canvas: #0D1220; /* fond global, bleu-nuit désaturé */
|
||||||
|
--canvas-2: #121A2D; /* séparations, fond hover sidebar */
|
||||||
|
--surface: #182238; /* cards — ressortent nettement */
|
||||||
|
--surface-hover: #1E2A42; /* cards au survol */
|
||||||
|
--surface-raised: #1F2B45; /* cards surélevées (plan premium) */
|
||||||
|
|
||||||
|
/* Hairlines — plus visibles qu'en clair, structure l'œil */
|
||||||
|
--line: #27324B;
|
||||||
|
--line-strong: #364363;
|
||||||
|
|
||||||
|
/* Encres — blanc cassé pour confort visuel, jamais pur blanc */
|
||||||
|
--ink-1: #F1F4FA; /* titres forts — très blanc mais chaud */
|
||||||
|
--ink-2: #DDE3EF; /* corps principal */
|
||||||
|
--ink-3: #A8B2C7; /* secondaire */
|
||||||
|
--ink-4: #7A8499; /* tertiaire, labels */
|
||||||
|
--ink-5: #525C73; /* désactivé, hints */
|
||||||
|
|
||||||
|
/* Brand Expria — remonté en luminance pour rester lisible sur sombre */
|
||||||
|
--expria: #5B7FFF; /* #1B4FD8 brightened pour dark mode */
|
||||||
|
--expria-hover: #6F8EFF;
|
||||||
|
--expria-dim: #3A5DD9; /* variante pour fonds/backgrounds */
|
||||||
|
--expria-bg: rgba(91, 127, 255, 0.12); /* wash très léger */
|
||||||
|
--expria-bg-2: rgba(91, 127, 255, 0.18);
|
||||||
|
|
||||||
|
/* Bleu nuit — utilisé inversement en clair : ici en accent très foncé */
|
||||||
|
--deep: #060B1A; /* plus profond que canvas, pour contraster */
|
||||||
|
|
||||||
|
/* Sémantiques — versions dark mode (fonds sombres transparents) */
|
||||||
|
--success: #3DD68C;
|
||||||
|
--success-bg: rgba(61, 214, 140, 0.12);
|
||||||
|
--warning: #F5B849;
|
||||||
|
--warning-bg: rgba(245, 184, 73, 0.12);
|
||||||
|
--danger: #F06B6B;
|
||||||
|
--danger-bg: rgba(240, 107, 107, 0.12);
|
||||||
|
|
||||||
|
/* Élévations — jouer sur les surfaces plus que les ombres */
|
||||||
|
--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);
|
||||||
|
|
||||||
|
/* Rayons — identiques à light */
|
||||||
|
--r-sm: 6px;
|
||||||
|
--r-md: 10px;
|
||||||
|
--r-lg: 14px;
|
||||||
|
--r-xl: 18px;
|
||||||
|
--r-full: 999px;
|
||||||
|
}
|
||||||
|
|
||||||
|
html, body {
|
||||||
|
font-family: "Plus Jakarta Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif;
|
||||||
|
background: var(--canvas);
|
||||||
|
color: var(--ink-2);
|
||||||
|
line-height: 1.5;
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
-moz-osx-font-smoothing: grayscale;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* =========================================================
|
||||||
|
HEADER (bandeau de présentation de la direction)
|
||||||
|
========================================================= */
|
||||||
|
.da-header {
|
||||||
|
background: var(--surface);
|
||||||
|
border-bottom: 0.5px solid var(--line);
|
||||||
|
padding: 20px 28px;
|
||||||
|
}
|
||||||
|
.da-header-inner {
|
||||||
|
max-width: 1400px;
|
||||||
|
margin: 0 auto;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 24px;
|
||||||
|
}
|
||||||
|
.da-header-title {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--ink-1);
|
||||||
|
letter-spacing: -0.01em;
|
||||||
|
}
|
||||||
|
.da-header-subtitle {
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--ink-4);
|
||||||
|
margin-top: 2px;
|
||||||
|
}
|
||||||
|
.da-header-tag {
|
||||||
|
font-size: 11px;
|
||||||
|
color: var(--expria);
|
||||||
|
background: var(--expria-bg);
|
||||||
|
padding: 5px 12px;
|
||||||
|
border-radius: var(--r-full);
|
||||||
|
font-weight: 600;
|
||||||
|
letter-spacing: 0.02em;
|
||||||
|
border: 0.5px solid rgba(91, 127, 255, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* =========================================================
|
||||||
|
LAYOUT APP
|
||||||
|
========================================================= */
|
||||||
|
.app {
|
||||||
|
max-width: 1400px;
|
||||||
|
margin: 0 auto;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 240px 1fr;
|
||||||
|
gap: 0;
|
||||||
|
min-height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* =========================================================
|
||||||
|
SIDEBAR
|
||||||
|
========================================================= */
|
||||||
|
.sidebar {
|
||||||
|
background: transparent;
|
||||||
|
border-right: 0.5px solid var(--line);
|
||||||
|
padding: 24px 20px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--ink-1);
|
||||||
|
letter-spacing: -0.02em;
|
||||||
|
padding: 0 8px;
|
||||||
|
}
|
||||||
|
.logo-mark {
|
||||||
|
width: 28px;
|
||||||
|
height: 28px;
|
||||||
|
background: var(--expria);
|
||||||
|
border-radius: var(--r-sm);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
color: var(--canvas);
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: -0.02em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-group {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 2px;
|
||||||
|
}
|
||||||
|
.nav-group-label {
|
||||||
|
font-size: 10px;
|
||||||
|
color: var(--ink-5);
|
||||||
|
font-weight: 600;
|
||||||
|
letter-spacing: 0.08em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
padding: 0 10px 6px;
|
||||||
|
}
|
||||||
|
.nav-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
padding: 9px 12px;
|
||||||
|
border-radius: var(--r-md);
|
||||||
|
font-size: 13.5px;
|
||||||
|
color: var(--ink-3);
|
||||||
|
font-weight: 500;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.15s;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
.nav-item:hover {
|
||||||
|
background: var(--canvas-2);
|
||||||
|
color: var(--ink-1);
|
||||||
|
}
|
||||||
|
.nav-item.active {
|
||||||
|
background: var(--surface);
|
||||||
|
color: var(--ink-1);
|
||||||
|
font-weight: 600;
|
||||||
|
box-shadow: var(--shadow-sm);
|
||||||
|
border: 0.5px solid var(--line);
|
||||||
|
}
|
||||||
|
.nav-item svg {
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
stroke: currentColor;
|
||||||
|
fill: none;
|
||||||
|
stroke-width: 1.75;
|
||||||
|
stroke-linecap: round;
|
||||||
|
stroke-linejoin: round;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-card {
|
||||||
|
margin-top: auto;
|
||||||
|
padding: 12px;
|
||||||
|
background: var(--surface);
|
||||||
|
border: 0.5px solid var(--line);
|
||||||
|
border-radius: var(--r-md);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
box-shadow: var(--shadow-sm);
|
||||||
|
}
|
||||||
|
.user-avatar {
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
border-radius: var(--r-full);
|
||||||
|
background: var(--expria);
|
||||||
|
color: var(--canvas);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
.user-meta { flex: 1; min-width: 0; }
|
||||||
|
.user-name {
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--ink-1);
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
.user-plan {
|
||||||
|
font-size: 11px;
|
||||||
|
color: var(--ink-4);
|
||||||
|
margin-top: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* =========================================================
|
||||||
|
MAIN
|
||||||
|
========================================================= */
|
||||||
|
.main {
|
||||||
|
padding: 32px 36px 56px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 24px;
|
||||||
|
margin-bottom: 28px;
|
||||||
|
}
|
||||||
|
.page-title-block { flex: 1; }
|
||||||
|
.page-eyebrow {
|
||||||
|
font-size: 11px;
|
||||||
|
color: var(--ink-4);
|
||||||
|
font-weight: 600;
|
||||||
|
letter-spacing: 0.08em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
margin-bottom: 6px;
|
||||||
|
}
|
||||||
|
.page-title {
|
||||||
|
font-size: 26px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--ink-1);
|
||||||
|
letter-spacing: -0.02em;
|
||||||
|
line-height: 1.2;
|
||||||
|
}
|
||||||
|
.page-sub {
|
||||||
|
font-size: 14px;
|
||||||
|
color: var(--ink-3);
|
||||||
|
margin-top: 6px;
|
||||||
|
line-height: 1.6;
|
||||||
|
max-width: 640px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* =========================================================
|
||||||
|
BOUTONS
|
||||||
|
========================================================= */
|
||||||
|
.btn {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 10px 16px;
|
||||||
|
border: none;
|
||||||
|
border-radius: var(--r-md);
|
||||||
|
font-size: 13.5px;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.15s;
|
||||||
|
font-family: inherit;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
.btn-primary {
|
||||||
|
background: var(--expria);
|
||||||
|
color: var(--canvas);
|
||||||
|
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.3);
|
||||||
|
}
|
||||||
|
.btn-primary:hover {
|
||||||
|
background: var(--expria-hover);
|
||||||
|
transform: translateY(-1px);
|
||||||
|
box-shadow: 0 4px 16px rgba(91, 127, 255, 0.35);
|
||||||
|
}
|
||||||
|
.btn-ghost {
|
||||||
|
background: transparent;
|
||||||
|
color: var(--ink-2);
|
||||||
|
border: 0.5px solid var(--line-strong);
|
||||||
|
}
|
||||||
|
.btn-ghost:hover {
|
||||||
|
background: var(--surface);
|
||||||
|
border-color: var(--ink-5);
|
||||||
|
color: var(--ink-1);
|
||||||
|
}
|
||||||
|
.btn-arrow {
|
||||||
|
font-size: 16px;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* =========================================================
|
||||||
|
GRID & CARDS
|
||||||
|
========================================================= */
|
||||||
|
.metrics {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(3, 1fr);
|
||||||
|
gap: 16px;
|
||||||
|
margin-bottom: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card {
|
||||||
|
background: var(--surface);
|
||||||
|
border: 0.5px solid var(--line);
|
||||||
|
border-radius: var(--r-lg);
|
||||||
|
padding: 22px;
|
||||||
|
box-shadow: var(--shadow-sm);
|
||||||
|
transition: box-shadow 0.2s, transform 0.2s, background 0.2s;
|
||||||
|
}
|
||||||
|
.card-raised {
|
||||||
|
box-shadow: var(--shadow-md);
|
||||||
|
}
|
||||||
|
.card:hover {
|
||||||
|
box-shadow: var(--shadow-md);
|
||||||
|
background: var(--surface-hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-label {
|
||||||
|
font-size: 11px;
|
||||||
|
color: var(--ink-4);
|
||||||
|
font-weight: 600;
|
||||||
|
letter-spacing: 0.08em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Metric cards */
|
||||||
|
.metric-card .metric-value {
|
||||||
|
font-size: 32px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--ink-1);
|
||||||
|
letter-spacing: -0.03em;
|
||||||
|
line-height: 1;
|
||||||
|
font-variant-numeric: tabular-nums;
|
||||||
|
}
|
||||||
|
.metric-value-unit {
|
||||||
|
font-size: 14px;
|
||||||
|
color: var(--ink-4);
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
.metric-sub {
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--ink-4);
|
||||||
|
margin-top: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress {
|
||||||
|
height: 5px;
|
||||||
|
background: var(--canvas-2);
|
||||||
|
border-radius: var(--r-full);
|
||||||
|
margin-top: 12px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
.progress-fill {
|
||||||
|
height: 100%;
|
||||||
|
background: var(--expria);
|
||||||
|
border-radius: var(--r-full);
|
||||||
|
transition: width 0.4s;
|
||||||
|
}
|
||||||
|
.progress-fill.success { background: var(--success); }
|
||||||
|
|
||||||
|
/* Metric card highlight — plan
|
||||||
|
Inversion du light mode : ici on montre un fond bleu saturé
|
||||||
|
avec le brand, au lieu du bleu nuit/deep */
|
||||||
|
.metric-card-plan {
|
||||||
|
background:
|
||||||
|
radial-gradient(circle at 80% -30%, rgba(91, 127, 255, 0.4), transparent 60%),
|
||||||
|
linear-gradient(135deg, #1E2A52 0%, #2C3E7A 100%);
|
||||||
|
border: 0.5px solid rgba(91, 127, 255, 0.4);
|
||||||
|
color: var(--ink-1);
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
.metric-card-plan .card-label {
|
||||||
|
color: rgba(255, 255, 255, 0.55);
|
||||||
|
}
|
||||||
|
.metric-card-plan .plan-name {
|
||||||
|
font-size: 24px;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: -0.02em;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
color: #FFFFFF;
|
||||||
|
}
|
||||||
|
.metric-card-plan .plan-desc {
|
||||||
|
font-size: 12px;
|
||||||
|
color: rgba(255, 255, 255, 0.75);
|
||||||
|
line-height: 1.5;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
.metric-card-plan .btn-upgrade {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
padding: 8px 14px;
|
||||||
|
background: var(--expria);
|
||||||
|
color: var(--canvas);
|
||||||
|
border-radius: var(--r-md);
|
||||||
|
font-size: 12.5px;
|
||||||
|
font-weight: 600;
|
||||||
|
text-decoration: none;
|
||||||
|
transition: background 0.15s;
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
.metric-card-plan .btn-upgrade:hover {
|
||||||
|
background: var(--expria-hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* =========================================================
|
||||||
|
ROW — 2 colonnes pour historique + prochaine étape
|
||||||
|
========================================================= */
|
||||||
|
.row-2 {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1.4fr 1fr;
|
||||||
|
gap: 16px;
|
||||||
|
margin-bottom: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* =========================================================
|
||||||
|
HISTORIQUE
|
||||||
|
========================================================= */
|
||||||
|
.history-title {
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--ink-1);
|
||||||
|
margin-bottom: 4px;
|
||||||
|
letter-spacing: -0.01em;
|
||||||
|
}
|
||||||
|
.history-sub {
|
||||||
|
font-size: 12.5px;
|
||||||
|
color: var(--ink-4);
|
||||||
|
margin-bottom: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.history-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
.history-item {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 40px 1fr auto auto;
|
||||||
|
align-items: center;
|
||||||
|
gap: 14px;
|
||||||
|
padding: 14px 0;
|
||||||
|
border-bottom: 0.5px solid var(--line);
|
||||||
|
}
|
||||||
|
.history-item:last-child { border-bottom: none; }
|
||||||
|
|
||||||
|
.history-icon {
|
||||||
|
width: 36px;
|
||||||
|
height: 36px;
|
||||||
|
border-radius: var(--r-md);
|
||||||
|
background: var(--expria-bg);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
color: var(--expria);
|
||||||
|
flex-shrink: 0;
|
||||||
|
border: 0.5px solid rgba(91, 127, 255, 0.25);
|
||||||
|
}
|
||||||
|
.history-icon svg {
|
||||||
|
width: 18px;
|
||||||
|
height: 18px;
|
||||||
|
stroke: currentColor;
|
||||||
|
fill: none;
|
||||||
|
stroke-width: 1.75;
|
||||||
|
stroke-linecap: round;
|
||||||
|
stroke-linejoin: round;
|
||||||
|
}
|
||||||
|
|
||||||
|
.history-meta { min-width: 0; }
|
||||||
|
.history-type {
|
||||||
|
font-size: 13.5px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--ink-1);
|
||||||
|
margin-bottom: 2px;
|
||||||
|
}
|
||||||
|
.history-date {
|
||||||
|
font-size: 11.5px;
|
||||||
|
color: var(--ink-4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.history-nclc-badge {
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 600;
|
||||||
|
padding: 4px 10px;
|
||||||
|
border-radius: var(--r-full);
|
||||||
|
background: var(--success-bg);
|
||||||
|
color: var(--success);
|
||||||
|
letter-spacing: 0.02em;
|
||||||
|
border: 0.5px solid rgba(61, 214, 140, 0.25);
|
||||||
|
}
|
||||||
|
.history-nclc-badge.warn {
|
||||||
|
background: var(--warning-bg);
|
||||||
|
color: var(--warning);
|
||||||
|
border-color: rgba(245, 184, 73, 0.25);
|
||||||
|
}
|
||||||
|
|
||||||
|
.history-score {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--ink-1);
|
||||||
|
font-variant-numeric: tabular-nums;
|
||||||
|
letter-spacing: -0.01em;
|
||||||
|
min-width: 50px;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
.history-score-max {
|
||||||
|
font-size: 11px;
|
||||||
|
color: var(--ink-5);
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* =========================================================
|
||||||
|
PROCHAINE ÉTAPE
|
||||||
|
========================================================= */
|
||||||
|
.next-step {
|
||||||
|
background: linear-gradient(135deg, rgba(91, 127, 255, 0.08) 0%, rgba(24, 34, 56, 1) 100%);
|
||||||
|
border: 0.5px solid rgba(91, 127, 255, 0.25);
|
||||||
|
}
|
||||||
|
.next-step:hover {
|
||||||
|
background: linear-gradient(135deg, rgba(91, 127, 255, 0.12) 0%, rgba(30, 42, 66, 1) 100%);
|
||||||
|
}
|
||||||
|
.next-step .card-label {
|
||||||
|
color: var(--expria);
|
||||||
|
}
|
||||||
|
.next-step-title {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--ink-1);
|
||||||
|
margin-bottom: 8px;
|
||||||
|
line-height: 1.35;
|
||||||
|
letter-spacing: -0.01em;
|
||||||
|
}
|
||||||
|
.next-step-desc {
|
||||||
|
font-size: 13px;
|
||||||
|
color: var(--ink-3);
|
||||||
|
line-height: 1.6;
|
||||||
|
margin-bottom: 18px;
|
||||||
|
}
|
||||||
|
.next-step-stats {
|
||||||
|
display: flex;
|
||||||
|
gap: 20px;
|
||||||
|
margin-bottom: 18px;
|
||||||
|
padding: 12px 14px;
|
||||||
|
background: rgba(13, 18, 32, 0.5);
|
||||||
|
border-radius: var(--r-md);
|
||||||
|
border: 0.5px solid var(--line);
|
||||||
|
}
|
||||||
|
.next-step-stat {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
.next-step-stat-label {
|
||||||
|
font-size: 10px;
|
||||||
|
color: var(--ink-4);
|
||||||
|
font-weight: 600;
|
||||||
|
letter-spacing: 0.06em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
margin-bottom: 3px;
|
||||||
|
}
|
||||||
|
.next-step-stat-value {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--ink-1);
|
||||||
|
letter-spacing: -0.01em;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* =========================================================
|
||||||
|
CTA BOTTOM
|
||||||
|
========================================================= */
|
||||||
|
.cta-block {
|
||||||
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* =========================================================
|
||||||
|
PALETTE DISPLAY (bas de page)
|
||||||
|
========================================================= */
|
||||||
|
.palette-row {
|
||||||
|
margin-top: 40px;
|
||||||
|
padding: 20px 24px;
|
||||||
|
background: var(--surface);
|
||||||
|
border: 0.5px solid var(--line);
|
||||||
|
border-radius: var(--r-lg);
|
||||||
|
box-shadow: var(--shadow-sm);
|
||||||
|
}
|
||||||
|
.palette-title {
|
||||||
|
font-size: 11px;
|
||||||
|
color: var(--ink-4);
|
||||||
|
font-weight: 600;
|
||||||
|
letter-spacing: 0.08em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
margin-bottom: 14px;
|
||||||
|
}
|
||||||
|
.palette-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(100px, 1fr));
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
.palette-swatch {
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
.palette-color {
|
||||||
|
height: 40px;
|
||||||
|
border-radius: var(--r-sm);
|
||||||
|
border: 0.5px solid var(--line);
|
||||||
|
margin-bottom: 6px;
|
||||||
|
}
|
||||||
|
.palette-name {
|
||||||
|
font-size: 11px;
|
||||||
|
color: var(--ink-2);
|
||||||
|
font-weight: 600;
|
||||||
|
font-family: "SF Mono", ui-monospace, Menlo, Consolas, monospace;
|
||||||
|
}
|
||||||
|
.palette-role {
|
||||||
|
font-size: 10px;
|
||||||
|
color: var(--ink-4);
|
||||||
|
margin-top: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* =========================================================
|
||||||
|
RESPONSIVE
|
||||||
|
========================================================= */
|
||||||
|
@media (max-width: 960px) {
|
||||||
|
.app { grid-template-columns: 1fr; }
|
||||||
|
.sidebar { display: none; }
|
||||||
|
.main { padding: 24px; }
|
||||||
|
.metrics { grid-template-columns: 1fr; }
|
||||||
|
.row-2 { grid-template-columns: 1fr; }
|
||||||
|
.page-header { flex-direction: column; }
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 480px) {
|
||||||
|
.da-header-inner { flex-direction: column; align-items: flex-start; gap: 10px; }
|
||||||
|
.page-title { font-size: 22px; }
|
||||||
|
.metric-card .metric-value { font-size: 28px; }
|
||||||
|
.cta-block { flex-direction: column; }
|
||||||
|
.cta-block .btn { width: 100%; }
|
||||||
|
.history-item { grid-template-columns: 36px 1fr auto; }
|
||||||
|
.history-nclc-badge { display: none; }
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<!-- Bandeau de présentation de la direction -->
|
||||||
|
<div class="da-header">
|
||||||
|
<div class="da-header-inner">
|
||||||
|
<div>
|
||||||
|
<div class="da-header-title">Direction H — Mode sombre</div>
|
||||||
|
<div class="da-header-subtitle">Inversion réfléchie de la version claire. Fond bleu-nuit désaturé, cards qui ressortent, bleu Expria remonté en luminance pour rester lisible.</div>
|
||||||
|
</div>
|
||||||
|
<div class="da-header-tag">Mode sombre</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="app">
|
||||||
|
|
||||||
|
<!-- =====================================================
|
||||||
|
SIDEBAR
|
||||||
|
====================================================== -->
|
||||||
|
<aside class="sidebar">
|
||||||
|
<div class="logo">
|
||||||
|
<div class="logo-mark">EX</div>
|
||||||
|
<span>Expria</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<nav class="nav-group">
|
||||||
|
<div class="nav-group-label">Espace de travail</div>
|
||||||
|
<a class="nav-item active" href="#">
|
||||||
|
<svg viewBox="0 0 24 24"><path d="M3 12l9-9 9 9"/><path d="M5 10v10a1 1 0 001 1h4v-6h4v6h4a1 1 0 001-1V10"/></svg>
|
||||||
|
Tableau de bord
|
||||||
|
</a>
|
||||||
|
<a class="nav-item" href="#">
|
||||||
|
<svg viewBox="0 0 24 24"><path d="M14 2H6a2 2 0 00-2 2v16a2 2 0 002 2h12a2 2 0 002-2V8z"/><path d="M14 2v6h6"/><path d="M8 13h8M8 17h8M8 9h2"/></svg>
|
||||||
|
Simulations
|
||||||
|
</a>
|
||||||
|
<a class="nav-item" href="#">
|
||||||
|
<svg viewBox="0 0 24 24"><path d="M12 2a10 10 0 100 20 10 10 0 000-20z"/><path d="M12 6v6l4 2"/></svg>
|
||||||
|
Historique
|
||||||
|
</a>
|
||||||
|
<a class="nav-item" href="#">
|
||||||
|
<svg viewBox="0 0 24 24"><path d="M12 15l-3-3 3-3M3 12h12a6 6 0 010 12"/></svg>
|
||||||
|
Rapports
|
||||||
|
</a>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<nav class="nav-group">
|
||||||
|
<div class="nav-group-label">Préparation</div>
|
||||||
|
<a class="nav-item" href="#">
|
||||||
|
<svg viewBox="0 0 24 24"><path d="M21 5l-9-3-9 3v6a12 12 0 009 11 12 12 0 009-11V5z"/></svg>
|
||||||
|
Mode examen
|
||||||
|
</a>
|
||||||
|
<a class="nav-item" href="#">
|
||||||
|
<svg viewBox="0 0 24 24"><circle cx="12" cy="12" r="10"/><path d="M12 8v4M12 16h.01"/></svg>
|
||||||
|
Guide TCF
|
||||||
|
</a>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<div class="user-card">
|
||||||
|
<div class="user-avatar">HM</div>
|
||||||
|
<div class="user-meta">
|
||||||
|
<div class="user-name">Hermann Mbanga</div>
|
||||||
|
<div class="user-plan">Plan Découverte</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</aside>
|
||||||
|
|
||||||
|
<!-- =====================================================
|
||||||
|
MAIN
|
||||||
|
====================================================== -->
|
||||||
|
<main class="main">
|
||||||
|
|
||||||
|
<div class="page-header">
|
||||||
|
<div class="page-title-block">
|
||||||
|
<div class="page-eyebrow">Mercredi 17 avril</div>
|
||||||
|
<h1 class="page-title">Bonjour Hermann</h1>
|
||||||
|
<p class="page-sub">Vous progressez bien. Plus que deux simulations pour atteindre votre objectif NCLC 9.</p>
|
||||||
|
</div>
|
||||||
|
<div class="cta-block">
|
||||||
|
<button class="btn btn-ghost">Voir mon profil</button>
|
||||||
|
<button class="btn btn-primary">
|
||||||
|
Nouvelle simulation
|
||||||
|
<span class="btn-arrow">→</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Métriques -->
|
||||||
|
<div class="metrics">
|
||||||
|
|
||||||
|
<div class="card metric-card card-raised">
|
||||||
|
<div class="card-label">Simulations restantes</div>
|
||||||
|
<div class="metric-value">3<span class="metric-value-unit"> / 5</span></div>
|
||||||
|
<div class="progress"><div class="progress-fill" style="width: 60%"></div></div>
|
||||||
|
<div class="metric-sub">Plan Découverte — renouvellement à chaque upgrade</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card metric-card card-raised">
|
||||||
|
<div class="card-label">Niveau estimé</div>
|
||||||
|
<div class="metric-value">NCLC 8</div>
|
||||||
|
<div class="progress"><div class="progress-fill" style="width: 75%"></div></div>
|
||||||
|
<div class="metric-sub">Moyenne des 3 dernières simulations · Objectif NCLC 9</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card metric-card-plan">
|
||||||
|
<div class="card-label">Plan actuel</div>
|
||||||
|
<div class="plan-name">Découverte</div>
|
||||||
|
<div class="plan-desc">Débloquez des simulations illimitées, le Mode Examen et le T2 Live avec Standard ou Premium.</div>
|
||||||
|
<a href="#" class="btn-upgrade">
|
||||||
|
Passer à Standard
|
||||||
|
<span style="font-size: 14px; line-height: 1;">→</span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Row 2 colonnes -->
|
||||||
|
<div class="row-2">
|
||||||
|
|
||||||
|
<!-- Historique -->
|
||||||
|
<div class="card">
|
||||||
|
<div class="history-title">Simulations récentes</div>
|
||||||
|
<div class="history-sub">Vos 3 dernières corrections</div>
|
||||||
|
|
||||||
|
<div class="history-list">
|
||||||
|
|
||||||
|
<div class="history-item">
|
||||||
|
<div class="history-icon">
|
||||||
|
<svg viewBox="0 0 24 24"><path d="M17 3a2.85 2.83 0 114 4L7.5 20.5 2 22l1.5-5.5z"/></svg>
|
||||||
|
</div>
|
||||||
|
<div class="history-meta">
|
||||||
|
<div class="history-type">Expression écrite — Tâche 2</div>
|
||||||
|
<div class="history-date">Aujourd'hui · 09:42</div>
|
||||||
|
</div>
|
||||||
|
<div class="history-nclc-badge">NCLC 9</div>
|
||||||
|
<div class="history-score">16<span class="history-score-max">/20</span></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="history-item">
|
||||||
|
<div class="history-icon">
|
||||||
|
<svg viewBox="0 0 24 24"><path d="M12 1a3 3 0 00-3 3v8a3 3 0 006 0V4a3 3 0 00-3-3z"/><path d="M19 10v2a7 7 0 11-14 0v-2M12 19v4M8 23h8"/></svg>
|
||||||
|
</div>
|
||||||
|
<div class="history-meta">
|
||||||
|
<div class="history-type">Expression orale — Tâche 1</div>
|
||||||
|
<div class="history-date">Il y a 2 jours</div>
|
||||||
|
</div>
|
||||||
|
<div class="history-nclc-badge warn">NCLC 8</div>
|
||||||
|
<div class="history-score">14<span class="history-score-max">/20</span></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="history-item">
|
||||||
|
<div class="history-icon">
|
||||||
|
<svg viewBox="0 0 24 24"><path d="M17 3a2.85 2.83 0 114 4L7.5 20.5 2 22l1.5-5.5z"/></svg>
|
||||||
|
</div>
|
||||||
|
<div class="history-meta">
|
||||||
|
<div class="history-type">Expression écrite — Tâche 3</div>
|
||||||
|
<div class="history-date">Il y a 5 jours</div>
|
||||||
|
</div>
|
||||||
|
<div class="history-nclc-badge">NCLC 9</div>
|
||||||
|
<div class="history-score">15<span class="history-score-max">/20</span></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Prochaine étape -->
|
||||||
|
<div class="card next-step">
|
||||||
|
<div class="card-label">Prochaine étape recommandée</div>
|
||||||
|
<div class="next-step-title">Travaillez la tâche 2 à l'oral</div>
|
||||||
|
<div class="next-step-desc">
|
||||||
|
Votre dernier score à l'EO T2 (14/20) est en dessous de votre moyenne. Une simulation de 10 minutes suffit pour consolider.
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="next-step-stats">
|
||||||
|
<div class="next-step-stat">
|
||||||
|
<div class="next-step-stat-label">Durée</div>
|
||||||
|
<div class="next-step-stat-value">10 min</div>
|
||||||
|
</div>
|
||||||
|
<div class="next-step-stat">
|
||||||
|
<div class="next-step-stat-label">Difficulté</div>
|
||||||
|
<div class="next-step-stat-value">Modérée</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button class="btn btn-primary" style="width: 100%;">
|
||||||
|
Commencer maintenant
|
||||||
|
<span class="btn-arrow">→</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Palette -->
|
||||||
|
<div class="palette-row">
|
||||||
|
<div class="palette-title">Palette de la direction H — mode sombre</div>
|
||||||
|
<div class="palette-grid">
|
||||||
|
<div class="palette-swatch">
|
||||||
|
<div class="palette-color" style="background: #0D1220;"></div>
|
||||||
|
<div class="palette-name">#0D1220</div>
|
||||||
|
<div class="palette-role">Fond principal</div>
|
||||||
|
</div>
|
||||||
|
<div class="palette-swatch">
|
||||||
|
<div class="palette-color" style="background: #182238;"></div>
|
||||||
|
<div class="palette-name">#182238</div>
|
||||||
|
<div class="palette-role">Cards surface</div>
|
||||||
|
</div>
|
||||||
|
<div class="palette-swatch">
|
||||||
|
<div class="palette-color" style="background: #5B7FFF;"></div>
|
||||||
|
<div class="palette-name">#5B7FFF</div>
|
||||||
|
<div class="palette-role">Bleu Expria — remonté</div>
|
||||||
|
</div>
|
||||||
|
<div class="palette-swatch">
|
||||||
|
<div class="palette-color" style="background: #27324B; border-color: #5B7FFF;"></div>
|
||||||
|
<div class="palette-name">#27324B</div>
|
||||||
|
<div class="palette-role">Hairlines</div>
|
||||||
|
</div>
|
||||||
|
<div class="palette-swatch">
|
||||||
|
<div class="palette-color" style="background: #F1F4FA;"></div>
|
||||||
|
<div class="palette-name">#F1F4FA</div>
|
||||||
|
<div class="palette-role">Titres</div>
|
||||||
|
</div>
|
||||||
|
<div class="palette-swatch">
|
||||||
|
<div class="palette-color" style="background: #A8B2C7;"></div>
|
||||||
|
<div class="palette-name">#A8B2C7</div>
|
||||||
|
<div class="palette-role">Corps secondaire</div>
|
||||||
|
</div>
|
||||||
|
<div class="palette-swatch">
|
||||||
|
<div class="palette-color" style="background: #3DD68C;"></div>
|
||||||
|
<div class="palette-name">#3DD68C</div>
|
||||||
|
<div class="palette-role">Succès</div>
|
||||||
|
</div>
|
||||||
|
<div class="palette-swatch">
|
||||||
|
<div class="palette-color" style="background: #F5B849;"></div>
|
||||||
|
<div class="palette-name">#F5B849</div>
|
||||||
|
<div class="palette-role">Attention</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</main>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
921
design-reference/direction-h-juste-milieu.html
Normal file
921
design-reference/direction-h-juste-milieu.html
Normal file
|
|
@ -0,0 +1,921 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="fr">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Expria — Tableau de bord · Direction H : Juste milieu</title>
|
||||||
|
<style>
|
||||||
|
/* =========================================================
|
||||||
|
DIRECTION H — "JUSTE MILIEU"
|
||||||
|
Entre Boréal (A) trop blanc et Cadence (B) trop sombre.
|
||||||
|
Inspirations : Revolut (mode clair), Notion, Linear,
|
||||||
|
N26, Anthropic Console.
|
||||||
|
Parti pris : fond gris-bleuté calme, cards blanches qui
|
||||||
|
ressortent avec profondeur, bleu Expria pivot, accents
|
||||||
|
bleu-nuit pour les éléments premium/structurants.
|
||||||
|
========================================================= */
|
||||||
|
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||||||
|
|
||||||
|
:root {
|
||||||
|
/* Fonds — gris bleutés, jamais pur blanc pour éviter l'effet clinique */
|
||||||
|
--canvas: #EEF2F8; /* fond global, bleuté très léger */
|
||||||
|
--canvas-2: #E6EBF4; /* fond plus marqué, séparations */
|
||||||
|
--surface: #FFFFFF; /* cards — blanc franc qui ressort */
|
||||||
|
--surface-hover: #F8FAFD; /* cards au survol */
|
||||||
|
|
||||||
|
/* Hairlines — fines, bleutées */
|
||||||
|
--line: #DDE3ED;
|
||||||
|
--line-strong: #C7D0E0;
|
||||||
|
|
||||||
|
/* Encres — ardoise, jamais noir pur (plus doux, plus premium) */
|
||||||
|
--ink-1: #0F172A; /* titres forts */
|
||||||
|
--ink-2: #1E293B; /* corps principal */
|
||||||
|
--ink-3: #475569; /* secondaire */
|
||||||
|
--ink-4: #64748B; /* tertiaire, labels */
|
||||||
|
--ink-5: #94A3B8; /* désactivé, hints */
|
||||||
|
|
||||||
|
/* Brand Expria */
|
||||||
|
--expria: #1B4FD8;
|
||||||
|
--expria-hover: #1741B8;
|
||||||
|
--expria-50: #EEF3FF;
|
||||||
|
--expria-100: #DCE6FF;
|
||||||
|
--expria-200: #B8CDFF;
|
||||||
|
|
||||||
|
/* Bleu nuit — accent secondaire pour éléments structurants/premium */
|
||||||
|
--deep: #0B1F5C;
|
||||||
|
--deep-2: #142B6E;
|
||||||
|
|
||||||
|
/* Sémantiques — pastel sur blanc */
|
||||||
|
--success: #0E9F6E;
|
||||||
|
--success-bg: #E6F6F0;
|
||||||
|
--warning: #C77A00;
|
||||||
|
--warning-bg: #FEF3E2;
|
||||||
|
--danger: #C53030;
|
||||||
|
--danger-bg: #FDECEC;
|
||||||
|
|
||||||
|
/* Élévations — subtiles, crée la profondeur sans alourdir */
|
||||||
|
--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 */
|
||||||
|
--r-sm: 6px;
|
||||||
|
--r-md: 10px;
|
||||||
|
--r-lg: 14px;
|
||||||
|
--r-xl: 18px;
|
||||||
|
--r-full: 999px;
|
||||||
|
}
|
||||||
|
|
||||||
|
html, body {
|
||||||
|
font-family: "Plus Jakarta Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif;
|
||||||
|
background: var(--canvas);
|
||||||
|
color: var(--ink-2);
|
||||||
|
line-height: 1.5;
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
-moz-osx-font-smoothing: grayscale;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* =========================================================
|
||||||
|
HEADER (bandeau de présentation de la direction)
|
||||||
|
========================================================= */
|
||||||
|
.da-header {
|
||||||
|
background: var(--surface);
|
||||||
|
border-bottom: 0.5px solid var(--line);
|
||||||
|
padding: 20px 28px;
|
||||||
|
}
|
||||||
|
.da-header-inner {
|
||||||
|
max-width: 1400px;
|
||||||
|
margin: 0 auto;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 24px;
|
||||||
|
}
|
||||||
|
.da-header-title {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--ink-1);
|
||||||
|
letter-spacing: -0.01em;
|
||||||
|
}
|
||||||
|
.da-header-subtitle {
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--ink-4);
|
||||||
|
margin-top: 2px;
|
||||||
|
}
|
||||||
|
.da-header-tag {
|
||||||
|
font-size: 11px;
|
||||||
|
color: var(--expria);
|
||||||
|
background: var(--expria-50);
|
||||||
|
padding: 5px 12px;
|
||||||
|
border-radius: var(--r-full);
|
||||||
|
font-weight: 600;
|
||||||
|
letter-spacing: 0.02em;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* =========================================================
|
||||||
|
LAYOUT APP
|
||||||
|
========================================================= */
|
||||||
|
.app {
|
||||||
|
max-width: 1400px;
|
||||||
|
margin: 0 auto;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 240px 1fr;
|
||||||
|
gap: 0;
|
||||||
|
min-height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* =========================================================
|
||||||
|
SIDEBAR
|
||||||
|
========================================================= */
|
||||||
|
.sidebar {
|
||||||
|
background: transparent;
|
||||||
|
border-right: 0.5px solid var(--line);
|
||||||
|
padding: 24px 20px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--ink-1);
|
||||||
|
letter-spacing: -0.02em;
|
||||||
|
padding: 0 8px;
|
||||||
|
}
|
||||||
|
.logo-mark {
|
||||||
|
width: 28px;
|
||||||
|
height: 28px;
|
||||||
|
background: var(--expria);
|
||||||
|
border-radius: var(--r-sm);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
color: white;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: -0.02em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-group {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 2px;
|
||||||
|
}
|
||||||
|
.nav-group-label {
|
||||||
|
font-size: 10px;
|
||||||
|
color: var(--ink-5);
|
||||||
|
font-weight: 600;
|
||||||
|
letter-spacing: 0.08em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
padding: 0 10px 6px;
|
||||||
|
}
|
||||||
|
.nav-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
padding: 9px 12px;
|
||||||
|
border-radius: var(--r-md);
|
||||||
|
font-size: 13.5px;
|
||||||
|
color: var(--ink-3);
|
||||||
|
font-weight: 500;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.15s;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
.nav-item:hover {
|
||||||
|
background: var(--canvas-2);
|
||||||
|
color: var(--ink-1);
|
||||||
|
}
|
||||||
|
.nav-item.active {
|
||||||
|
background: var(--surface);
|
||||||
|
color: var(--ink-1);
|
||||||
|
font-weight: 600;
|
||||||
|
box-shadow: var(--shadow-sm);
|
||||||
|
border: 0.5px solid var(--line);
|
||||||
|
}
|
||||||
|
.nav-item svg {
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
stroke: currentColor;
|
||||||
|
fill: none;
|
||||||
|
stroke-width: 1.75;
|
||||||
|
stroke-linecap: round;
|
||||||
|
stroke-linejoin: round;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-card {
|
||||||
|
margin-top: auto;
|
||||||
|
padding: 12px;
|
||||||
|
background: var(--surface);
|
||||||
|
border: 0.5px solid var(--line);
|
||||||
|
border-radius: var(--r-md);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
box-shadow: var(--shadow-sm);
|
||||||
|
}
|
||||||
|
.user-avatar {
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
border-radius: var(--r-full);
|
||||||
|
background: var(--deep);
|
||||||
|
color: white;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
.user-meta { flex: 1; min-width: 0; }
|
||||||
|
.user-name {
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--ink-1);
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
.user-plan {
|
||||||
|
font-size: 11px;
|
||||||
|
color: var(--ink-4);
|
||||||
|
margin-top: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* =========================================================
|
||||||
|
MAIN
|
||||||
|
========================================================= */
|
||||||
|
.main {
|
||||||
|
padding: 32px 36px 56px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 24px;
|
||||||
|
margin-bottom: 28px;
|
||||||
|
}
|
||||||
|
.page-title-block { flex: 1; }
|
||||||
|
.page-eyebrow {
|
||||||
|
font-size: 11px;
|
||||||
|
color: var(--ink-4);
|
||||||
|
font-weight: 600;
|
||||||
|
letter-spacing: 0.08em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
margin-bottom: 6px;
|
||||||
|
}
|
||||||
|
.page-title {
|
||||||
|
font-size: 26px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--ink-1);
|
||||||
|
letter-spacing: -0.02em;
|
||||||
|
line-height: 1.2;
|
||||||
|
}
|
||||||
|
.page-sub {
|
||||||
|
font-size: 14px;
|
||||||
|
color: var(--ink-3);
|
||||||
|
margin-top: 6px;
|
||||||
|
line-height: 1.6;
|
||||||
|
max-width: 640px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* =========================================================
|
||||||
|
BOUTONS
|
||||||
|
========================================================= */
|
||||||
|
.btn {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 10px 16px;
|
||||||
|
border: none;
|
||||||
|
border-radius: var(--r-md);
|
||||||
|
font-size: 13.5px;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.15s;
|
||||||
|
font-family: inherit;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
.btn-primary {
|
||||||
|
background: var(--expria);
|
||||||
|
color: white;
|
||||||
|
box-shadow: 0 1px 2px rgba(27, 79, 216, 0.24);
|
||||||
|
}
|
||||||
|
.btn-primary:hover {
|
||||||
|
background: var(--expria-hover);
|
||||||
|
transform: translateY(-1px);
|
||||||
|
box-shadow: 0 4px 12px rgba(27, 79, 216, 0.2);
|
||||||
|
}
|
||||||
|
.btn-ghost {
|
||||||
|
background: transparent;
|
||||||
|
color: var(--ink-2);
|
||||||
|
border: 0.5px solid var(--line-strong);
|
||||||
|
}
|
||||||
|
.btn-ghost:hover {
|
||||||
|
background: var(--surface);
|
||||||
|
border-color: var(--ink-5);
|
||||||
|
}
|
||||||
|
.btn-arrow {
|
||||||
|
font-size: 16px;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* =========================================================
|
||||||
|
GRID & CARDS
|
||||||
|
========================================================= */
|
||||||
|
.metrics {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(3, 1fr);
|
||||||
|
gap: 16px;
|
||||||
|
margin-bottom: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card {
|
||||||
|
background: var(--surface);
|
||||||
|
border: 0.5px solid var(--line);
|
||||||
|
border-radius: var(--r-lg);
|
||||||
|
padding: 22px;
|
||||||
|
box-shadow: var(--shadow-sm);
|
||||||
|
transition: box-shadow 0.2s, transform 0.2s;
|
||||||
|
}
|
||||||
|
.card-raised {
|
||||||
|
box-shadow: var(--shadow-md);
|
||||||
|
}
|
||||||
|
.card:hover {
|
||||||
|
box-shadow: var(--shadow-md);
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-label {
|
||||||
|
font-size: 11px;
|
||||||
|
color: var(--ink-4);
|
||||||
|
font-weight: 600;
|
||||||
|
letter-spacing: 0.08em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Metric cards */
|
||||||
|
.metric-card .metric-value {
|
||||||
|
font-size: 32px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--ink-1);
|
||||||
|
letter-spacing: -0.03em;
|
||||||
|
line-height: 1;
|
||||||
|
font-variant-numeric: tabular-nums;
|
||||||
|
}
|
||||||
|
.metric-value-unit {
|
||||||
|
font-size: 14px;
|
||||||
|
color: var(--ink-4);
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
.metric-sub {
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--ink-4);
|
||||||
|
margin-top: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress {
|
||||||
|
height: 5px;
|
||||||
|
background: var(--canvas-2);
|
||||||
|
border-radius: var(--r-full);
|
||||||
|
margin-top: 12px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
.progress-fill {
|
||||||
|
height: 100%;
|
||||||
|
background: var(--expria);
|
||||||
|
border-radius: var(--r-full);
|
||||||
|
transition: width 0.4s;
|
||||||
|
}
|
||||||
|
.progress-fill.success { background: var(--success); }
|
||||||
|
|
||||||
|
/* Metric card highlight — plan */
|
||||||
|
.metric-card-plan {
|
||||||
|
background: linear-gradient(135deg, var(--deep) 0%, var(--deep-2) 100%);
|
||||||
|
color: white;
|
||||||
|
border-color: transparent;
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
.metric-card-plan::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: -50%;
|
||||||
|
right: -30%;
|
||||||
|
width: 180px;
|
||||||
|
height: 180px;
|
||||||
|
background: radial-gradient(circle, rgba(27, 79, 216, 0.4), transparent 60%);
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
.metric-card-plan .card-label { color: rgba(255, 255, 255, 0.6); }
|
||||||
|
.metric-card-plan .plan-name {
|
||||||
|
font-size: 24px;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: -0.02em;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
.metric-card-plan .plan-desc {
|
||||||
|
font-size: 12px;
|
||||||
|
color: rgba(255, 255, 255, 0.7);
|
||||||
|
line-height: 1.5;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
.metric-card-plan .btn-upgrade {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
padding: 8px 14px;
|
||||||
|
background: var(--expria);
|
||||||
|
color: white;
|
||||||
|
border-radius: var(--r-md);
|
||||||
|
font-size: 12.5px;
|
||||||
|
font-weight: 600;
|
||||||
|
text-decoration: none;
|
||||||
|
transition: background 0.15s;
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
.metric-card-plan .btn-upgrade:hover {
|
||||||
|
background: #2A60E8;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* =========================================================
|
||||||
|
ROW — 2 colonnes pour historique + prochaine étape
|
||||||
|
========================================================= */
|
||||||
|
.row-2 {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1.4fr 1fr;
|
||||||
|
gap: 16px;
|
||||||
|
margin-bottom: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* =========================================================
|
||||||
|
HISTORIQUE
|
||||||
|
========================================================= */
|
||||||
|
.history-title {
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--ink-1);
|
||||||
|
margin-bottom: 4px;
|
||||||
|
letter-spacing: -0.01em;
|
||||||
|
}
|
||||||
|
.history-sub {
|
||||||
|
font-size: 12.5px;
|
||||||
|
color: var(--ink-4);
|
||||||
|
margin-bottom: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.history-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
.history-item {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 40px 1fr auto auto;
|
||||||
|
align-items: center;
|
||||||
|
gap: 14px;
|
||||||
|
padding: 14px 0;
|
||||||
|
border-bottom: 0.5px solid var(--line);
|
||||||
|
}
|
||||||
|
.history-item:last-child { border-bottom: none; }
|
||||||
|
|
||||||
|
.history-icon {
|
||||||
|
width: 36px;
|
||||||
|
height: 36px;
|
||||||
|
border-radius: var(--r-md);
|
||||||
|
background: var(--expria-50);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
color: var(--expria);
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
.history-icon svg {
|
||||||
|
width: 18px;
|
||||||
|
height: 18px;
|
||||||
|
stroke: currentColor;
|
||||||
|
fill: none;
|
||||||
|
stroke-width: 1.75;
|
||||||
|
stroke-linecap: round;
|
||||||
|
stroke-linejoin: round;
|
||||||
|
}
|
||||||
|
|
||||||
|
.history-meta { min-width: 0; }
|
||||||
|
.history-type {
|
||||||
|
font-size: 13.5px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--ink-1);
|
||||||
|
margin-bottom: 2px;
|
||||||
|
}
|
||||||
|
.history-date {
|
||||||
|
font-size: 11.5px;
|
||||||
|
color: var(--ink-4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.history-nclc-badge {
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 600;
|
||||||
|
padding: 4px 10px;
|
||||||
|
border-radius: var(--r-full);
|
||||||
|
background: var(--success-bg);
|
||||||
|
color: var(--success);
|
||||||
|
letter-spacing: 0.02em;
|
||||||
|
}
|
||||||
|
.history-nclc-badge.warn {
|
||||||
|
background: var(--warning-bg);
|
||||||
|
color: var(--warning);
|
||||||
|
}
|
||||||
|
|
||||||
|
.history-score {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--ink-1);
|
||||||
|
font-variant-numeric: tabular-nums;
|
||||||
|
letter-spacing: -0.01em;
|
||||||
|
min-width: 50px;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
.history-score-max {
|
||||||
|
font-size: 11px;
|
||||||
|
color: var(--ink-5);
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* =========================================================
|
||||||
|
PROCHAINE ÉTAPE
|
||||||
|
========================================================= */
|
||||||
|
.next-step {
|
||||||
|
background: linear-gradient(135deg, var(--expria-50) 0%, var(--canvas-2) 100%);
|
||||||
|
border: 0.5px solid var(--expria-100);
|
||||||
|
}
|
||||||
|
.next-step .card-label {
|
||||||
|
color: var(--expria);
|
||||||
|
}
|
||||||
|
.next-step-title {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--ink-1);
|
||||||
|
margin-bottom: 8px;
|
||||||
|
line-height: 1.35;
|
||||||
|
letter-spacing: -0.01em;
|
||||||
|
}
|
||||||
|
.next-step-desc {
|
||||||
|
font-size: 13px;
|
||||||
|
color: var(--ink-3);
|
||||||
|
line-height: 1.6;
|
||||||
|
margin-bottom: 18px;
|
||||||
|
}
|
||||||
|
.next-step-stats {
|
||||||
|
display: flex;
|
||||||
|
gap: 20px;
|
||||||
|
margin-bottom: 18px;
|
||||||
|
padding: 12px 14px;
|
||||||
|
background: rgba(255, 255, 255, 0.6);
|
||||||
|
border-radius: var(--r-md);
|
||||||
|
border: 0.5px solid var(--line);
|
||||||
|
}
|
||||||
|
.next-step-stat {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
.next-step-stat-label {
|
||||||
|
font-size: 10px;
|
||||||
|
color: var(--ink-4);
|
||||||
|
font-weight: 600;
|
||||||
|
letter-spacing: 0.06em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
margin-bottom: 3px;
|
||||||
|
}
|
||||||
|
.next-step-stat-value {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--ink-1);
|
||||||
|
letter-spacing: -0.01em;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* =========================================================
|
||||||
|
CTA BOTTOM
|
||||||
|
========================================================= */
|
||||||
|
.cta-block {
|
||||||
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* =========================================================
|
||||||
|
PALETTE DISPLAY (bas de page)
|
||||||
|
========================================================= */
|
||||||
|
.palette-row {
|
||||||
|
margin-top: 40px;
|
||||||
|
padding: 20px 24px;
|
||||||
|
background: var(--surface);
|
||||||
|
border: 0.5px solid var(--line);
|
||||||
|
border-radius: var(--r-lg);
|
||||||
|
box-shadow: var(--shadow-sm);
|
||||||
|
}
|
||||||
|
.palette-title {
|
||||||
|
font-size: 11px;
|
||||||
|
color: var(--ink-4);
|
||||||
|
font-weight: 600;
|
||||||
|
letter-spacing: 0.08em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
margin-bottom: 14px;
|
||||||
|
}
|
||||||
|
.palette-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(100px, 1fr));
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
.palette-swatch {
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
.palette-color {
|
||||||
|
height: 40px;
|
||||||
|
border-radius: var(--r-sm);
|
||||||
|
border: 0.5px solid var(--line);
|
||||||
|
margin-bottom: 6px;
|
||||||
|
}
|
||||||
|
.palette-name {
|
||||||
|
font-size: 11px;
|
||||||
|
color: var(--ink-2);
|
||||||
|
font-weight: 600;
|
||||||
|
font-family: "SF Mono", monospace;
|
||||||
|
}
|
||||||
|
.palette-role {
|
||||||
|
font-size: 10px;
|
||||||
|
color: var(--ink-4);
|
||||||
|
margin-top: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* =========================================================
|
||||||
|
RESPONSIVE
|
||||||
|
========================================================= */
|
||||||
|
@media (max-width: 960px) {
|
||||||
|
.app { grid-template-columns: 1fr; }
|
||||||
|
.sidebar { display: none; }
|
||||||
|
.main { padding: 24px; }
|
||||||
|
.metrics { grid-template-columns: 1fr; }
|
||||||
|
.row-2 { grid-template-columns: 1fr; }
|
||||||
|
.page-header { flex-direction: column; }
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 480px) {
|
||||||
|
.da-header-inner { flex-direction: column; align-items: flex-start; gap: 10px; }
|
||||||
|
.page-title { font-size: 22px; }
|
||||||
|
.metric-card .metric-value { font-size: 28px; }
|
||||||
|
.cta-block { flex-direction: column; }
|
||||||
|
.cta-block .btn { width: 100%; }
|
||||||
|
.history-item { grid-template-columns: 36px 1fr auto; }
|
||||||
|
.history-nclc-badge { display: none; }
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<!-- Bandeau de présentation de la direction -->
|
||||||
|
<div class="da-header">
|
||||||
|
<div class="da-header-inner">
|
||||||
|
<div>
|
||||||
|
<div class="da-header-title">Direction H — Juste milieu</div>
|
||||||
|
<div class="da-header-subtitle">Entre Boréal (trop blanc) et Cadence (trop sombre). Fond gris-bleuté, cards blanches en relief, bleu Expria pivot, accents bleu-nuit.</div>
|
||||||
|
</div>
|
||||||
|
<div class="da-header-tag">Version recommandée</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="app">
|
||||||
|
|
||||||
|
<!-- =====================================================
|
||||||
|
SIDEBAR
|
||||||
|
====================================================== -->
|
||||||
|
<aside class="sidebar">
|
||||||
|
<div class="logo">
|
||||||
|
<div class="logo-mark">EX</div>
|
||||||
|
<span>Expria</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<nav class="nav-group">
|
||||||
|
<div class="nav-group-label">Espace de travail</div>
|
||||||
|
<a class="nav-item active" href="#">
|
||||||
|
<svg viewBox="0 0 24 24"><path d="M3 12l9-9 9 9"/><path d="M5 10v10a1 1 0 001 1h4v-6h4v6h4a1 1 0 001-1V10"/></svg>
|
||||||
|
Tableau de bord
|
||||||
|
</a>
|
||||||
|
<a class="nav-item" href="#">
|
||||||
|
<svg viewBox="0 0 24 24"><path d="M14 2H6a2 2 0 00-2 2v16a2 2 0 002 2h12a2 2 0 002-2V8z"/><path d="M14 2v6h6"/><path d="M8 13h8M8 17h8M8 9h2"/></svg>
|
||||||
|
Simulations
|
||||||
|
</a>
|
||||||
|
<a class="nav-item" href="#">
|
||||||
|
<svg viewBox="0 0 24 24"><path d="M12 2a10 10 0 100 20 10 10 0 000-20z"/><path d="M12 6v6l4 2"/></svg>
|
||||||
|
Historique
|
||||||
|
</a>
|
||||||
|
<a class="nav-item" href="#">
|
||||||
|
<svg viewBox="0 0 24 24"><path d="M12 15l-3-3 3-3M3 12h12a6 6 0 010 12"/></svg>
|
||||||
|
Rapports
|
||||||
|
</a>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<nav class="nav-group">
|
||||||
|
<div class="nav-group-label">Préparation</div>
|
||||||
|
<a class="nav-item" href="#">
|
||||||
|
<svg viewBox="0 0 24 24"><path d="M21 5l-9-3-9 3v6a12 12 0 009 11 12 12 0 009-11V5z"/></svg>
|
||||||
|
Mode examen
|
||||||
|
</a>
|
||||||
|
<a class="nav-item" href="#">
|
||||||
|
<svg viewBox="0 0 24 24"><circle cx="12" cy="12" r="10"/><path d="M12 8v4M12 16h.01"/></svg>
|
||||||
|
Guide TCF
|
||||||
|
</a>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<div class="user-card">
|
||||||
|
<div class="user-avatar">HM</div>
|
||||||
|
<div class="user-meta">
|
||||||
|
<div class="user-name">Hermann Mbanga</div>
|
||||||
|
<div class="user-plan">Plan Découverte</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</aside>
|
||||||
|
|
||||||
|
<!-- =====================================================
|
||||||
|
MAIN
|
||||||
|
====================================================== -->
|
||||||
|
<main class="main">
|
||||||
|
|
||||||
|
<div class="page-header">
|
||||||
|
<div class="page-title-block">
|
||||||
|
<div class="page-eyebrow">Mercredi 17 avril</div>
|
||||||
|
<h1 class="page-title">Bonjour Hermann</h1>
|
||||||
|
<p class="page-sub">Vous progressez bien. Plus que deux simulations pour atteindre votre objectif NCLC 9.</p>
|
||||||
|
</div>
|
||||||
|
<div class="cta-block">
|
||||||
|
<button class="btn btn-ghost">Voir mon profil</button>
|
||||||
|
<button class="btn btn-primary">
|
||||||
|
Nouvelle simulation
|
||||||
|
<span class="btn-arrow">→</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Métriques -->
|
||||||
|
<div class="metrics">
|
||||||
|
|
||||||
|
<div class="card metric-card card-raised">
|
||||||
|
<div class="card-label">Simulations restantes</div>
|
||||||
|
<div class="metric-value">3<span class="metric-value-unit"> / 5</span></div>
|
||||||
|
<div class="progress"><div class="progress-fill" style="width: 60%"></div></div>
|
||||||
|
<div class="metric-sub">Plan Découverte — renouvellement à chaque upgrade</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card metric-card card-raised">
|
||||||
|
<div class="card-label">Niveau estimé</div>
|
||||||
|
<div class="metric-value">NCLC 8</div>
|
||||||
|
<div class="progress"><div class="progress-fill" style="width: 75%"></div></div>
|
||||||
|
<div class="metric-sub">Moyenne des 3 dernières simulations · Objectif NCLC 9</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card metric-card-plan">
|
||||||
|
<div class="card-label">Plan actuel</div>
|
||||||
|
<div class="plan-name">Découverte</div>
|
||||||
|
<div class="plan-desc">Débloquez des simulations illimitées, le Mode Examen et le T2 Live avec Standard ou Premium.</div>
|
||||||
|
<a href="#" class="btn-upgrade">
|
||||||
|
Passer à Standard
|
||||||
|
<span style="font-size: 14px; line-height: 1;">→</span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Row 2 colonnes -->
|
||||||
|
<div class="row-2">
|
||||||
|
|
||||||
|
<!-- Historique -->
|
||||||
|
<div class="card">
|
||||||
|
<div class="history-title">Simulations récentes</div>
|
||||||
|
<div class="history-sub">Vos 3 dernières corrections</div>
|
||||||
|
|
||||||
|
<div class="history-list">
|
||||||
|
|
||||||
|
<div class="history-item">
|
||||||
|
<div class="history-icon">
|
||||||
|
<svg viewBox="0 0 24 24"><path d="M17 3a2.85 2.83 0 114 4L7.5 20.5 2 22l1.5-5.5z"/></svg>
|
||||||
|
</div>
|
||||||
|
<div class="history-meta">
|
||||||
|
<div class="history-type">Expression écrite — Tâche 2</div>
|
||||||
|
<div class="history-date">Aujourd'hui · 09:42</div>
|
||||||
|
</div>
|
||||||
|
<div class="history-nclc-badge">NCLC 9</div>
|
||||||
|
<div class="history-score">16<span class="history-score-max">/20</span></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="history-item">
|
||||||
|
<div class="history-icon">
|
||||||
|
<svg viewBox="0 0 24 24"><path d="M12 1a3 3 0 00-3 3v8a3 3 0 006 0V4a3 3 0 00-3-3z"/><path d="M19 10v2a7 7 0 11-14 0v-2M12 19v4M8 23h8"/></svg>
|
||||||
|
</div>
|
||||||
|
<div class="history-meta">
|
||||||
|
<div class="history-type">Expression orale — Tâche 1</div>
|
||||||
|
<div class="history-date">Il y a 2 jours</div>
|
||||||
|
</div>
|
||||||
|
<div class="history-nclc-badge warn">NCLC 8</div>
|
||||||
|
<div class="history-score">14<span class="history-score-max">/20</span></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="history-item">
|
||||||
|
<div class="history-icon">
|
||||||
|
<svg viewBox="0 0 24 24"><path d="M17 3a2.85 2.83 0 114 4L7.5 20.5 2 22l1.5-5.5z"/></svg>
|
||||||
|
</div>
|
||||||
|
<div class="history-meta">
|
||||||
|
<div class="history-type">Expression écrite — Tâche 3</div>
|
||||||
|
<div class="history-date">Il y a 5 jours</div>
|
||||||
|
</div>
|
||||||
|
<div class="history-nclc-badge">NCLC 9</div>
|
||||||
|
<div class="history-score">15<span class="history-score-max">/20</span></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Prochaine étape -->
|
||||||
|
<div class="card next-step">
|
||||||
|
<div class="card-label">Prochaine étape recommandée</div>
|
||||||
|
<div class="next-step-title">Travaillez la tâche 2 à l'oral</div>
|
||||||
|
<div class="next-step-desc">
|
||||||
|
Votre dernier score à l'EO T2 (14/20) est en dessous de votre moyenne. Une simulation de 10 minutes suffit pour consolider.
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="next-step-stats">
|
||||||
|
<div class="next-step-stat">
|
||||||
|
<div class="next-step-stat-label">Durée</div>
|
||||||
|
<div class="next-step-stat-value">10 min</div>
|
||||||
|
</div>
|
||||||
|
<div class="next-step-stat">
|
||||||
|
<div class="next-step-stat-label">Difficulté</div>
|
||||||
|
<div class="next-step-stat-value">Modérée</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button class="btn btn-primary" style="width: 100%;">
|
||||||
|
Commencer maintenant
|
||||||
|
<span class="btn-arrow">→</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Palette -->
|
||||||
|
<div class="palette-row">
|
||||||
|
<div class="palette-title">Palette de la direction H</div>
|
||||||
|
<div class="palette-grid">
|
||||||
|
<div class="palette-swatch">
|
||||||
|
<div class="palette-color" style="background: #EEF2F8;"></div>
|
||||||
|
<div class="palette-name">#EEF2F8</div>
|
||||||
|
<div class="palette-role">Fond principal</div>
|
||||||
|
</div>
|
||||||
|
<div class="palette-swatch">
|
||||||
|
<div class="palette-color" style="background: #FFFFFF; border-color: #C7D0E0;"></div>
|
||||||
|
<div class="palette-name">#FFFFFF</div>
|
||||||
|
<div class="palette-role">Cards (blanc franc)</div>
|
||||||
|
</div>
|
||||||
|
<div class="palette-swatch">
|
||||||
|
<div class="palette-color" style="background: #1B4FD8;"></div>
|
||||||
|
<div class="palette-name">#1B4FD8</div>
|
||||||
|
<div class="palette-role">Bleu Expria — pivot</div>
|
||||||
|
</div>
|
||||||
|
<div class="palette-swatch">
|
||||||
|
<div class="palette-color" style="background: #0B1F5C;"></div>
|
||||||
|
<div class="palette-name">#0B1F5C</div>
|
||||||
|
<div class="palette-role">Bleu nuit — premium</div>
|
||||||
|
</div>
|
||||||
|
<div class="palette-swatch">
|
||||||
|
<div class="palette-color" style="background: #0F172A;"></div>
|
||||||
|
<div class="palette-name">#0F172A</div>
|
||||||
|
<div class="palette-role">Titres</div>
|
||||||
|
</div>
|
||||||
|
<div class="palette-swatch">
|
||||||
|
<div class="palette-color" style="background: #475569;"></div>
|
||||||
|
<div class="palette-name">#475569</div>
|
||||||
|
<div class="palette-role">Corps</div>
|
||||||
|
</div>
|
||||||
|
<div class="palette-swatch">
|
||||||
|
<div class="palette-color" style="background: #0E9F6E;"></div>
|
||||||
|
<div class="palette-name">#0E9F6E</div>
|
||||||
|
<div class="palette-role">Succès</div>
|
||||||
|
</div>
|
||||||
|
<div class="palette-swatch">
|
||||||
|
<div class="palette-color" style="background: #C77A00;"></div>
|
||||||
|
<div class="palette-name">#C77A00</div>
|
||||||
|
<div class="palette-role">Attention</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</main>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
343
docs/DESIGN_SYSTEM.md
Normal file
343
docs/DESIGN_SYSTEM.md
Normal file
|
|
@ -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
|
||||||
|
|
||||||
|
```
|
||||||
|
<body>
|
||||||
|
<Sidebar /> (≥ 1024px)
|
||||||
|
<main>
|
||||||
|
<PageHeader /> (greeting + plan)
|
||||||
|
<section>
|
||||||
|
<MetricCard hero /> (NCLC estimé + progression)
|
||||||
|
<MetricCard /> (simulations restantes)
|
||||||
|
<MetricCard /> (dernier score)
|
||||||
|
</section>
|
||||||
|
<Button primary /> (Nouvelle simulation)
|
||||||
|
<Button upgrade /> (Passer au plan Standard)
|
||||||
|
<section>
|
||||||
|
<SectionHeader /> (3 dernières simulations)
|
||||||
|
<Card interactive /> × 3
|
||||||
|
</section>
|
||||||
|
<section>
|
||||||
|
<SectionHeader /> (Prochaine étape recommandée)
|
||||||
|
<Card raised />
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
<BottomNav /> (< 1024px)
|
||||||
|
</body>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Breakpoints
|
||||||
|
|
||||||
|
| Breakpoint | Comportement |
|
||||||
|
|---|---|
|
||||||
|
| `< 1024px` | Mono-colonne, `BottomNav` fixe en bas, padding horizontal 20px |
|
||||||
|
| `≥ 1024px` | Sidebar 240px + contenu centré 860px max, padding horizontal 32px |
|
||||||
|
| `≥ 1440px` | Contenu centré 920px max (pas d'élargissement excessif) |
|
||||||
|
|
||||||
|
### Densité verticale
|
||||||
|
|
||||||
|
- Padding vertical section : 24px mobile, 32px desktop.
|
||||||
|
- Gap inter-cards : 12px mobile, 16px desktop.
|
||||||
|
- Marge sous `PageHeader` : 20px mobile, 28px desktop.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. Données mock — Sprint 1
|
||||||
|
|
||||||
|
Avant branchement API, fournir les données via `src/shared/api/mock/dashboard.ts`. Données crédibles, françaises, alignées sur l'audience réelle.
|
||||||
|
|
||||||
|
```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 : `<header>`, `<nav>`, `<main>`, `<section>`.
|
||||||
|
- 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é.
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
# ADR 006 — Stack frontend : versions 2026 (React 19, Vite 8, TypeScript 6, Tailwind 4, RR7)
|
# 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
|
**Date :** 2026-04-17
|
||||||
**Décideur :** Hermann
|
**Décideur :** Hermann
|
||||||
**Contexte :** Révélé par l'état des lieux Claude Code au démarrage du Sprint 0 frontend
|
**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
|
### 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 `<html>`) — toggle manuel via ThemeProvider React (Sprint 0.5 étape 2). Configuré via :
|
||||||
|
|
||||||
```css
|
```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 {
|
@theme {
|
||||||
--color-primary: #1B4FD8; /* Couleur brand Expria */
|
/* Typographie */
|
||||||
--font-sans: "Plus Jakarta Sans", system-ui, sans-serif;
|
--font-sans: "Plus Jakarta Sans", -apple-system, BlinkMacSystemFont, "Segoe UI",
|
||||||
/* Autres variables de thème */
|
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
|
### shadcn/ui avec Tailwind 4
|
||||||
|
|
||||||
La CLI shadcn/ui supporte Tailwind 4 depuis début 2025 :
|
La CLI shadcn/ui supporte Tailwind 4 depuis début 2025 :
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,12 @@
|
||||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>Expria</title>
|
<title>Expria</title>
|
||||||
|
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||||
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||||
|
<link
|
||||||
|
href="https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@400;500;600;700&display=swap"
|
||||||
|
rel="stylesheet"
|
||||||
|
/>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="root"></div>
|
<div id="root"></div>
|
||||||
|
|
|
||||||
42
src/features/auth/components/ProtectedRoute.tsx
Normal file
42
src/features/auth/components/ProtectedRoute.tsx
Normal file
|
|
@ -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 (
|
||||||
|
<div
|
||||||
|
className="flex min-h-screen items-center justify-center bg-canvas text-ink-4"
|
||||||
|
role="status"
|
||||||
|
aria-live="polite"
|
||||||
|
aria-label="Chargement de la session"
|
||||||
|
>
|
||||||
|
<Loader2 className="size-6 animate-spin" aria-hidden="true" />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isAuthenticated) {
|
||||||
|
return <Navigate to="/login" replace />
|
||||||
|
}
|
||||||
|
|
||||||
|
return <>{children}</>
|
||||||
|
}
|
||||||
54
src/features/auth/hooks/useAuth.ts
Normal file
54
src/features/auth/hooks/useAuth.ts
Normal file
|
|
@ -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<User | null>(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,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -26,29 +26,39 @@ import {
|
||||||
// ─── palette data ────────────────────────────────────────────────────────────
|
// ─── palette data ────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
const PALETTE: { token: string; var: string; light: string; dark: string }[] = [
|
const PALETTE: { token: string; var: string; light: string; dark: string }[] = [
|
||||||
{ token: 'canvas', var: '--color-canvas', light: '#EEF2F8', dark: '#0D1220' },
|
{ token: 'canvas', var: '--color-canvas', light: '#EEF2F8', dark: '#0D1220' },
|
||||||
{ token: 'canvas-2', var: '--color-canvas-2', light: '#E6EBF4', dark: '#121A2D' },
|
{ token: 'canvas-2', var: '--color-canvas-2', light: '#E6EBF4', dark: '#121A2D' },
|
||||||
{ token: 'surface', var: '--color-surface', light: '#FFFFFF', dark: '#182238' },
|
{ token: 'surface', var: '--color-surface', light: '#FFFFFF', dark: '#182238' },
|
||||||
{ token: 'surface-hover',var: '--color-surface-hover',light: '#F8FAFD', dark: '#1E2A42' },
|
{ token: 'surface-hover', var: '--color-surface-hover', light: '#F8FAFD', dark: '#1E2A42' },
|
||||||
{ token: 'line', var: '--color-line', light: '#DDE3ED', dark: '#27324B' },
|
{ token: 'line', var: '--color-line', light: '#DDE3ED', dark: '#27324B' },
|
||||||
{ token: 'line-strong', var: '--color-line-strong', light: '#C7D0E0', dark: '#364363' },
|
{ token: 'line-strong', var: '--color-line-strong', light: '#C7D0E0', dark: '#364363' },
|
||||||
{ token: 'ink-1', var: '--color-ink-1', light: '#0F172A', dark: '#F1F4FA' },
|
{ token: 'ink-1', var: '--color-ink-1', light: '#0F172A', dark: '#F1F4FA' },
|
||||||
{ token: 'ink-2', var: '--color-ink-2', light: '#1E293B', dark: '#DDE3EF' },
|
{ token: 'ink-2', var: '--color-ink-2', light: '#1E293B', dark: '#DDE3EF' },
|
||||||
{ token: 'ink-3', var: '--color-ink-3', light: '#475569', dark: '#A8B2C7' },
|
{ token: 'ink-3', var: '--color-ink-3', light: '#475569', dark: '#A8B2C7' },
|
||||||
{ token: 'ink-4', var: '--color-ink-4', light: '#64748B', dark: '#7A8499' },
|
{ token: 'ink-4', var: '--color-ink-4', light: '#64748B', dark: '#7A8499' },
|
||||||
{ token: 'ink-5', var: '--color-ink-5', light: '#94A3B8', dark: '#525C73' },
|
{ token: 'ink-5', var: '--color-ink-5', light: '#94A3B8', dark: '#525C73' },
|
||||||
{ token: 'expria', var: '--color-expria', light: '#1B4FD8', dark: '#5B7FFF' },
|
{ token: 'expria', var: '--color-expria', light: '#1B4FD8', dark: '#5B7FFF' },
|
||||||
{ token: 'expria-hover', var: '--color-expria-hover', light: '#1741B8', dark: '#6F8EFF' },
|
{ 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-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-100', var: '--color-expria-100', light: '#DCE6FF', dark: '—' },
|
||||||
{ token: 'expria-200', var: '--color-expria-200', light: '#B8CDFF', dark: '—' },
|
{ token: 'expria-200', var: '--color-expria-200', light: '#B8CDFF', dark: '—' },
|
||||||
{ token: 'deep', var: '--color-deep', light: '#0B1F5C', dark: '#060B1A' },
|
{ token: 'deep', var: '--color-deep', light: '#0B1F5C', dark: '#060B1A' },
|
||||||
{ token: 'success', var: '--color-success', light: '#0E9F6E', dark: '#3DD68C' },
|
{ 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: 'success-bg',
|
||||||
{ token: 'warning-bg', var: '--color-warning-bg', light: '#FEF3E2', dark: 'rgba(245,184,73,.12)' },
|
var: '--color-success-bg',
|
||||||
{ token: 'danger', var: '--color-danger', light: '#C53030', dark: '#F06B6B' },
|
light: '#E6F6F0',
|
||||||
{ token: 'danger-bg', var: '--color-danger-bg', light: '#FDECEC', dark: 'rgba(240,107,107,.12)' },
|
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 ─────────────────────────────────────────────────────────
|
// ─── section wrapper ─────────────────────────────────────────────────────────
|
||||||
|
|
@ -71,7 +81,6 @@ export default function DesignSystemPage() {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-canvas px-6 py-10 space-y-14">
|
<div className="min-h-screen bg-canvas px-6 py-10 space-y-14">
|
||||||
|
|
||||||
{/* ── header ── */}
|
{/* ── header ── */}
|
||||||
<header className="flex items-center justify-between">
|
<header className="flex items-center justify-between">
|
||||||
<div>
|
<div>
|
||||||
|
|
@ -98,12 +107,8 @@ export default function DesignSystemPage() {
|
||||||
/>
|
/>
|
||||||
<div className="space-y-0.5">
|
<div className="space-y-0.5">
|
||||||
<p className="text-xs font-mono font-medium text-ink-2">{token}</p>
|
<p className="text-xs font-mono font-medium text-ink-2">{token}</p>
|
||||||
<p className="text-xs font-mono text-ink-4 leading-tight">
|
<p className="text-xs font-mono text-ink-4 leading-tight">☀ {light}</p>
|
||||||
☀ {light}
|
<p className="text-xs font-mono text-ink-4 leading-tight">☾ {dark}</p>
|
||||||
</p>
|
|
||||||
<p className="text-xs font-mono text-ink-4 leading-tight">
|
|
||||||
☾ {dark}
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
|
|
@ -143,7 +148,9 @@ export default function DesignSystemPage() {
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-wrap gap-2 items-center">
|
<div className="flex flex-wrap gap-2 items-center">
|
||||||
<Button disabled>Disabled</Button>
|
<Button disabled>Disabled</Button>
|
||||||
<Button variant="outline" disabled>Outline disabled</Button>
|
<Button variant="outline" disabled>
|
||||||
|
Outline disabled
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Section>
|
</Section>
|
||||||
|
|
@ -208,7 +215,7 @@ export default function DesignSystemPage() {
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col items-center gap-2">
|
<div className="flex flex-col items-center gap-2">
|
||||||
<AvatarGroup>
|
<AvatarGroup>
|
||||||
{['AB', 'CD', 'EF'].map(initials => (
|
{['AB', 'CD', 'EF'].map((initials) => (
|
||||||
<Avatar key={initials}>
|
<Avatar key={initials}>
|
||||||
<AvatarFallback>{initials}</AvatarFallback>
|
<AvatarFallback>{initials}</AvatarFallback>
|
||||||
</Avatar>
|
</Avatar>
|
||||||
|
|
@ -231,8 +238,8 @@ export default function DesignSystemPage() {
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>Example dialog</DialogTitle>
|
<DialogTitle>Example dialog</DialogTitle>
|
||||||
<DialogDescription>
|
<DialogDescription>
|
||||||
This dialog uses Direction H tokens — bg-surface, border-line, text-ink-4.
|
This dialog uses Direction H tokens — bg-surface, border-line, text-ink-4. Toggle
|
||||||
Toggle the theme to see it adapt.
|
the theme to see it adapt.
|
||||||
</DialogDescription>
|
</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
<DialogFooter showCloseButton>
|
<DialogFooter showCloseButton>
|
||||||
|
|
@ -242,7 +249,6 @@ export default function DesignSystemPage() {
|
||||||
</Dialog>
|
</Dialog>
|
||||||
</div>
|
</div>
|
||||||
</Section>
|
</Section>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,50 +5,50 @@
|
||||||
|
|
||||||
@theme {
|
@theme {
|
||||||
/* ─── Typographie ───────────────────────────────────────────── */
|
/* ─── Typographie ───────────────────────────────────────────── */
|
||||||
--font-sans: "Plus Jakarta Sans", -apple-system, BlinkMacSystemFont, "Segoe UI",
|
--font-sans:
|
||||||
system-ui, sans-serif;
|
'Plus Jakarta Sans', -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif;
|
||||||
|
|
||||||
/* ─── Fonds ─────────────────────────────────────────────────── */
|
/* ─── Fonds ─────────────────────────────────────────────────── */
|
||||||
/* bg-canvas = fond de page (jamais pur blanc) */
|
/* bg-canvas = fond de page (jamais pur blanc) */
|
||||||
/* bg-surface = cards — ressortent sur le canvas */
|
/* bg-surface = cards — ressortent sur le canvas */
|
||||||
--color-canvas: #EEF2F8;
|
--color-canvas: #eef2f8;
|
||||||
--color-canvas-2: #E6EBF4;
|
--color-canvas-2: #e6ebf4;
|
||||||
--color-surface: #FFFFFF;
|
--color-surface: #ffffff;
|
||||||
--color-surface-hover: #F8FAFD;
|
--color-surface-hover: #f8fafd;
|
||||||
|
|
||||||
/* ─── Hairlines ──────────────────────────────────────────────── */
|
/* ─── Hairlines ──────────────────────────────────────────────── */
|
||||||
--color-line: #DDE3ED;
|
--color-line: #dde3ed;
|
||||||
--color-line-strong: #C7D0E0;
|
--color-line-strong: #c7d0e0;
|
||||||
|
|
||||||
/* ─── Encres ─────────────────────────────────────────────────── */
|
/* ─── Encres ─────────────────────────────────────────────────── */
|
||||||
--color-ink-1: #0F172A;
|
--color-ink-1: #0f172a;
|
||||||
--color-ink-2: #1E293B;
|
--color-ink-2: #1e293b;
|
||||||
--color-ink-3: #475569;
|
--color-ink-3: #475569;
|
||||||
--color-ink-4: #64748B;
|
--color-ink-4: #64748b;
|
||||||
--color-ink-5: #94A3B8;
|
--color-ink-5: #94a3b8;
|
||||||
|
|
||||||
/* ─── Brand Expria ───────────────────────────────────────────── */
|
/* ─── Brand Expria ───────────────────────────────────────────── */
|
||||||
--color-expria: #1B4FD8;
|
--color-expria: #1b4fd8;
|
||||||
--color-expria-hover: #1741B8;
|
--color-expria-hover: #1741b8;
|
||||||
--color-expria-50: #EEF3FF;
|
--color-expria-50: #eef3ff;
|
||||||
--color-expria-100: #DCE6FF;
|
--color-expria-100: #dce6ff;
|
||||||
--color-expria-200: #B8CDFF;
|
--color-expria-200: #b8cdff;
|
||||||
--color-deep: #0B1F5C;
|
--color-deep: #0b1f5c;
|
||||||
--color-deep-2: #142B6E;
|
--color-deep-2: #142b6e;
|
||||||
|
|
||||||
/* ─── Sémantiques ────────────────────────────────────────────── */
|
/* ─── Sémantiques ────────────────────────────────────────────── */
|
||||||
--color-success: #0E9F6E;
|
--color-success: #0e9f6e;
|
||||||
--color-success-bg: #E6F6F0;
|
--color-success-bg: #e6f6f0;
|
||||||
--color-warning: #C77A00;
|
--color-warning: #c77a00;
|
||||||
--color-warning-bg: #FEF3E2;
|
--color-warning-bg: #fef3e2;
|
||||||
--color-danger: #C53030;
|
--color-danger: #c53030;
|
||||||
--color-danger-bg: #FDECEC;
|
--color-danger-bg: #fdecec;
|
||||||
|
|
||||||
/* ─── Rayons (override des defaults Tailwind) ────────────────── */
|
/* ─── Rayons (override des defaults Tailwind) ────────────────── */
|
||||||
--radius-sm: 6px;
|
--radius-sm: 6px;
|
||||||
--radius-md: 10px;
|
--radius-md: 10px;
|
||||||
--radius-lg: 14px;
|
--radius-lg: 14px;
|
||||||
--radius-xl: 18px;
|
--radius-xl: 18px;
|
||||||
--radius-full: 999px;
|
--radius-full: 999px;
|
||||||
|
|
||||||
/* ─── Ombres (light mode) ────────────────────────────────────── */
|
/* ─── Ombres (light mode) ────────────────────────────────────── */
|
||||||
|
|
@ -60,35 +60,35 @@
|
||||||
/* ─── Dark mode — override des tokens couleur et ombres ──────────── */
|
/* ─── Dark mode — override des tokens couleur et ombres ──────────── */
|
||||||
.dark {
|
.dark {
|
||||||
/* Fonds */
|
/* Fonds */
|
||||||
--color-canvas: #0D1220;
|
--color-canvas: #0d1220;
|
||||||
--color-canvas-2: #121A2D;
|
--color-canvas-2: #121a2d;
|
||||||
--color-surface: #182238;
|
--color-surface: #182238;
|
||||||
--color-surface-hover: #1E2A42;
|
--color-surface-hover: #1e2a42;
|
||||||
|
|
||||||
/* Hairlines */
|
/* Hairlines */
|
||||||
--color-line: #27324B;
|
--color-line: #27324b;
|
||||||
--color-line-strong: #364363;
|
--color-line-strong: #364363;
|
||||||
|
|
||||||
/* Encres */
|
/* Encres */
|
||||||
--color-ink-1: #F1F4FA;
|
--color-ink-1: #f1f4fa;
|
||||||
--color-ink-2: #DDE3EF;
|
--color-ink-2: #dde3ef;
|
||||||
--color-ink-3: #A8B2C7;
|
--color-ink-3: #a8b2c7;
|
||||||
--color-ink-4: #7A8499;
|
--color-ink-4: #7a8499;
|
||||||
--color-ink-5: #525C73;
|
--color-ink-5: #525c73;
|
||||||
|
|
||||||
/* Brand — remonté en luminance pour rester lisible sur fond sombre */
|
/* Brand — remonté en luminance pour rester lisible sur fond sombre */
|
||||||
--color-expria: #5B7FFF;
|
--color-expria: #5b7fff;
|
||||||
--color-expria-hover: #6F8EFF;
|
--color-expria-hover: #6f8eff;
|
||||||
--color-expria-50: rgba(91, 127, 255, 0.12);
|
--color-expria-50: rgba(91, 127, 255, 0.12);
|
||||||
--color-deep: #060B1A;
|
--color-deep: #060b1a;
|
||||||
|
|
||||||
/* Sémantiques */
|
/* Sémantiques */
|
||||||
--color-success: #3DD68C;
|
--color-success: #3dd68c;
|
||||||
--color-success-bg: rgba(61, 214, 140, 0.12);
|
--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-warning-bg: rgba(245, 184, 73, 0.12);
|
||||||
--color-danger: #F06B6B;
|
--color-danger: #f06b6b;
|
||||||
--color-danger-bg: rgba(240, 107, 107, 0.12);
|
--color-danger-bg: rgba(240, 107, 107, 0.12);
|
||||||
|
|
||||||
/* Ombres — jouer sur les surfaces, pas les ombres claires */
|
/* Ombres — jouer sur les surfaces, pas les ombres claires */
|
||||||
--shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.3);
|
--shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.3);
|
||||||
|
|
|
||||||
|
|
@ -24,7 +24,7 @@ export function Logo({ size = 'md', variant = 'full', className }: LogoProps) {
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
'inline-flex items-center gap-2.5 font-bold tracking-tight text-ink-1',
|
'inline-flex items-center gap-2.5 font-bold tracking-tight text-ink-1',
|
||||||
className
|
className,
|
||||||
)}
|
)}
|
||||||
role={variant === 'icon' ? 'img' : undefined}
|
role={variant === 'icon' ? 'img' : undefined}
|
||||||
aria-label={variant === 'icon' ? 'Expria' : undefined}
|
aria-label={variant === 'icon' ? 'Expria' : undefined}
|
||||||
|
|
@ -32,7 +32,7 @@ export function Logo({ size = 'md', variant = 'full', className }: LogoProps) {
|
||||||
<span
|
<span
|
||||||
className={cn(
|
className={cn(
|
||||||
'flex shrink-0 items-center justify-center rounded-sm bg-expria font-bold tracking-tight text-white',
|
'flex shrink-0 items-center justify-center rounded-sm bg-expria font-bold tracking-tight text-white',
|
||||||
markStyles[size]
|
markStyles[size],
|
||||||
)}
|
)}
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
>
|
>
|
||||||
|
|
|
||||||
|
|
@ -1,36 +1,33 @@
|
||||||
import * as React from "react"
|
import * as React from 'react'
|
||||||
import { Avatar as AvatarPrimitive } from "radix-ui"
|
import { Avatar as AvatarPrimitive } from 'radix-ui'
|
||||||
|
|
||||||
import { cn } from "@/shared/lib/utils"
|
import { cn } from '@/shared/lib/utils'
|
||||||
|
|
||||||
function Avatar({
|
function Avatar({
|
||||||
className,
|
className,
|
||||||
size = "default",
|
size = 'default',
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof AvatarPrimitive.Root> & {
|
}: React.ComponentProps<typeof AvatarPrimitive.Root> & {
|
||||||
size?: "default" | "sm" | "lg"
|
size?: 'default' | 'sm' | 'lg'
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<AvatarPrimitive.Root
|
<AvatarPrimitive.Root
|
||||||
data-slot="avatar"
|
data-slot="avatar"
|
||||||
data-size={size}
|
data-size={size}
|
||||||
className={cn(
|
className={cn(
|
||||||
"group/avatar relative flex size-8 shrink-0 overflow-hidden rounded-full select-none data-[size=lg]:size-10 data-[size=sm]:size-6",
|
'group/avatar relative flex size-8 shrink-0 overflow-hidden rounded-full select-none data-[size=lg]:size-10 data-[size=sm]:size-6',
|
||||||
className
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function AvatarImage({
|
function AvatarImage({ className, ...props }: React.ComponentProps<typeof AvatarPrimitive.Image>) {
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof AvatarPrimitive.Image>) {
|
|
||||||
return (
|
return (
|
||||||
<AvatarPrimitive.Image
|
<AvatarPrimitive.Image
|
||||||
data-slot="avatar-image"
|
data-slot="avatar-image"
|
||||||
className={cn("aspect-square size-full", className)}
|
className={cn('aspect-square size-full', className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
|
|
@ -44,64 +41,54 @@ function AvatarFallback({
|
||||||
<AvatarPrimitive.Fallback
|
<AvatarPrimitive.Fallback
|
||||||
data-slot="avatar-fallback"
|
data-slot="avatar-fallback"
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex size-full items-center justify-center rounded-full bg-canvas-2 text-sm text-ink-4 group-data-[size=sm]/avatar:text-xs",
|
'flex size-full items-center justify-center rounded-full bg-canvas-2 text-sm text-ink-4 group-data-[size=sm]/avatar:text-xs',
|
||||||
className
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function AvatarBadge({ className, ...props }: React.ComponentProps<"span">) {
|
function AvatarBadge({ className, ...props }: React.ComponentProps<'span'>) {
|
||||||
return (
|
return (
|
||||||
<span
|
<span
|
||||||
data-slot="avatar-badge"
|
data-slot="avatar-badge"
|
||||||
className={cn(
|
className={cn(
|
||||||
"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",
|
'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=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=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",
|
'group-data-[size=lg]/avatar:size-3 group-data-[size=lg]/avatar:[&>svg]:size-2',
|
||||||
className
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function AvatarGroup({ className, ...props }: React.ComponentProps<"div">) {
|
function AvatarGroup({ className, ...props }: React.ComponentProps<'div'>) {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
data-slot="avatar-group"
|
data-slot="avatar-group"
|
||||||
className={cn(
|
className={cn(
|
||||||
"group/avatar-group flex -space-x-2 *:data-[slot=avatar]:ring-2 *:data-[slot=avatar]:ring-canvas",
|
'group/avatar-group flex -space-x-2 *:data-[slot=avatar]:ring-2 *:data-[slot=avatar]:ring-canvas',
|
||||||
className
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function AvatarGroupCount({
|
function AvatarGroupCount({ className, ...props }: React.ComponentProps<'div'>) {
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<"div">) {
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
data-slot="avatar-group-count"
|
data-slot="avatar-group-count"
|
||||||
className={cn(
|
className={cn(
|
||||||
"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",
|
'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
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export {
|
export { Avatar, AvatarImage, AvatarFallback, AvatarBadge, AvatarGroup, AvatarGroupCount }
|
||||||
Avatar,
|
|
||||||
AvatarImage,
|
|
||||||
AvatarFallback,
|
|
||||||
AvatarBadge,
|
|
||||||
AvatarGroup,
|
|
||||||
AvatarGroupCount,
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -1,31 +1,23 @@
|
||||||
import * as React from "react"
|
import * as React from 'react'
|
||||||
import { XIcon } from "lucide-react"
|
import { XIcon } from 'lucide-react'
|
||||||
import { Dialog as DialogPrimitive } from "radix-ui"
|
import { Dialog as DialogPrimitive } from 'radix-ui'
|
||||||
|
|
||||||
import { cn } from "@/shared/lib/utils"
|
import { cn } from '@/shared/lib/utils'
|
||||||
import { Button } from "@/shared/components/ui/button"
|
import { Button } from '@/shared/components/ui/button'
|
||||||
|
|
||||||
function Dialog({
|
function Dialog({ ...props }: React.ComponentProps<typeof DialogPrimitive.Root>) {
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof DialogPrimitive.Root>) {
|
|
||||||
return <DialogPrimitive.Root data-slot="dialog" {...props} />
|
return <DialogPrimitive.Root data-slot="dialog" {...props} />
|
||||||
}
|
}
|
||||||
|
|
||||||
function DialogTrigger({
|
function DialogTrigger({ ...props }: React.ComponentProps<typeof DialogPrimitive.Trigger>) {
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof DialogPrimitive.Trigger>) {
|
|
||||||
return <DialogPrimitive.Trigger data-slot="dialog-trigger" {...props} />
|
return <DialogPrimitive.Trigger data-slot="dialog-trigger" {...props} />
|
||||||
}
|
}
|
||||||
|
|
||||||
function DialogPortal({
|
function DialogPortal({ ...props }: React.ComponentProps<typeof DialogPrimitive.Portal>) {
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof DialogPrimitive.Portal>) {
|
|
||||||
return <DialogPrimitive.Portal data-slot="dialog-portal" {...props} />
|
return <DialogPrimitive.Portal data-slot="dialog-portal" {...props} />
|
||||||
}
|
}
|
||||||
|
|
||||||
function DialogClose({
|
function DialogClose({ ...props }: React.ComponentProps<typeof DialogPrimitive.Close>) {
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof DialogPrimitive.Close>) {
|
|
||||||
return <DialogPrimitive.Close data-slot="dialog-close" {...props} />
|
return <DialogPrimitive.Close data-slot="dialog-close" {...props} />
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -37,8 +29,8 @@ function DialogOverlay({
|
||||||
<DialogPrimitive.Overlay
|
<DialogPrimitive.Overlay
|
||||||
data-slot="dialog-overlay"
|
data-slot="dialog-overlay"
|
||||||
className={cn(
|
className={cn(
|
||||||
"fixed inset-0 z-50 bg-black/50 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:animate-in data-[state=open]:fade-in-0",
|
'fixed inset-0 z-50 bg-black/50 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:animate-in data-[state=open]:fade-in-0',
|
||||||
className
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
|
|
@ -59,8 +51,8 @@ function DialogContent({
|
||||||
<DialogPrimitive.Content
|
<DialogPrimitive.Content
|
||||||
data-slot="dialog-content"
|
data-slot="dialog-content"
|
||||||
className={cn(
|
className={cn(
|
||||||
"fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border border-line bg-surface p-6 shadow-lg duration-200 outline-none data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95 sm:max-w-lg",
|
'fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border border-line bg-surface p-6 shadow-lg duration-200 outline-none data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95 sm:max-w-lg',
|
||||||
className
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
|
|
@ -79,11 +71,11 @@ function DialogContent({
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function DialogHeader({ className, ...props }: React.ComponentProps<"div">) {
|
function DialogHeader({ className, ...props }: React.ComponentProps<'div'>) {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
data-slot="dialog-header"
|
data-slot="dialog-header"
|
||||||
className={cn("flex flex-col gap-2 text-center sm:text-left", className)}
|
className={cn('flex flex-col gap-2 text-center sm:text-left', className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
|
|
@ -94,16 +86,13 @@ function DialogFooter({
|
||||||
showCloseButton = false,
|
showCloseButton = false,
|
||||||
children,
|
children,
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<"div"> & {
|
}: React.ComponentProps<'div'> & {
|
||||||
showCloseButton?: boolean
|
showCloseButton?: boolean
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
data-slot="dialog-footer"
|
data-slot="dialog-footer"
|
||||||
className={cn(
|
className={cn('flex flex-col-reverse gap-2 sm:flex-row sm:justify-end', className)}
|
||||||
"flex flex-col-reverse gap-2 sm:flex-row sm:justify-end",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
|
|
@ -116,14 +105,11 @@ function DialogFooter({
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function DialogTitle({
|
function DialogTitle({ className, ...props }: React.ComponentProps<typeof DialogPrimitive.Title>) {
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof DialogPrimitive.Title>) {
|
|
||||||
return (
|
return (
|
||||||
<DialogPrimitive.Title
|
<DialogPrimitive.Title
|
||||||
data-slot="dialog-title"
|
data-slot="dialog-title"
|
||||||
className={cn("text-lg leading-none font-semibold", className)}
|
className={cn('text-lg leading-none font-semibold', className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
|
|
@ -136,7 +122,7 @@ function DialogDescription({
|
||||||
return (
|
return (
|
||||||
<DialogPrimitive.Description
|
<DialogPrimitive.Description
|
||||||
data-slot="dialog-description"
|
data-slot="dialog-description"
|
||||||
className={cn("text-sm text-ink-4", className)}
|
className={cn('text-sm text-ink-4', className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -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 (
|
return (
|
||||||
<input
|
<input
|
||||||
type={type}
|
type={type}
|
||||||
data-slot="input"
|
data-slot="input"
|
||||||
className={cn(
|
className={cn(
|
||||||
"h-9 w-full min-w-0 rounded-md border border-line bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none selection:bg-expria selection:text-white file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-ink-2 placeholder:text-ink-4 disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm dark:bg-surface/30",
|
'h-9 w-full min-w-0 rounded-md border border-line bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none selection:bg-expria selection:text-white file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-ink-2 placeholder:text-ink-4 disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm dark:bg-surface/30',
|
||||||
"focus-visible:border-expria focus-visible:ring-[3px] focus-visible:ring-expria/30",
|
'focus-visible:border-expria focus-visible:ring-[3px] focus-visible:ring-expria/30',
|
||||||
"aria-invalid:border-danger aria-invalid:ring-danger/20 dark:aria-invalid:ring-danger/40",
|
'aria-invalid:border-danger aria-invalid:ring-danger/20 dark:aria-invalid:ring-danger/40',
|
||||||
className
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
|
|
|
||||||
|
|
@ -1,18 +1,15 @@
|
||||||
import * as React from "react"
|
import * as React from 'react'
|
||||||
import { Label as LabelPrimitive } from "radix-ui"
|
import { Label as LabelPrimitive } from 'radix-ui'
|
||||||
|
|
||||||
import { cn } from "@/shared/lib/utils"
|
import { cn } from '@/shared/lib/utils'
|
||||||
|
|
||||||
function Label({
|
function Label({ className, ...props }: React.ComponentProps<typeof LabelPrimitive.Root>) {
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof LabelPrimitive.Root>) {
|
|
||||||
return (
|
return (
|
||||||
<LabelPrimitive.Root
|
<LabelPrimitive.Root
|
||||||
data-slot="label"
|
data-slot="label"
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex items-center gap-2 text-sm leading-none font-medium select-none group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50 peer-disabled:cursor-not-allowed peer-disabled:opacity-50",
|
'flex items-center gap-2 text-sm leading-none font-medium select-none group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50 peer-disabled:cursor-not-allowed peer-disabled:opacity-50',
|
||||||
className
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import * as React from "react"
|
import * as React from 'react'
|
||||||
import { Progress as ProgressPrimitive } from "radix-ui"
|
import { Progress as ProgressPrimitive } from 'radix-ui'
|
||||||
|
|
||||||
import { cn } from "@/shared/lib/utils"
|
import { cn } from '@/shared/lib/utils'
|
||||||
|
|
||||||
function Progress({
|
function Progress({
|
||||||
className,
|
className,
|
||||||
|
|
@ -11,10 +11,7 @@ function Progress({
|
||||||
return (
|
return (
|
||||||
<ProgressPrimitive.Root
|
<ProgressPrimitive.Root
|
||||||
data-slot="progress"
|
data-slot="progress"
|
||||||
className={cn(
|
className={cn('relative h-2 w-full overflow-hidden rounded-full bg-expria/20', className)}
|
||||||
"relative h-2 w-full overflow-hidden rounded-full bg-expria/20",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
<ProgressPrimitive.Indicator
|
<ProgressPrimitive.Indicator
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,11 @@
|
||||||
import * as React from "react"
|
import * as React from 'react'
|
||||||
import { Separator as SeparatorPrimitive } from "radix-ui"
|
import { Separator as SeparatorPrimitive } from 'radix-ui'
|
||||||
|
|
||||||
import { cn } from "@/shared/lib/utils"
|
import { cn } from '@/shared/lib/utils'
|
||||||
|
|
||||||
function Separator({
|
function Separator({
|
||||||
className,
|
className,
|
||||||
orientation = "horizontal",
|
orientation = 'horizontal',
|
||||||
decorative = true,
|
decorative = true,
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof SeparatorPrimitive.Root>) {
|
}: React.ComponentProps<typeof SeparatorPrimitive.Root>) {
|
||||||
|
|
@ -15,8 +15,8 @@ function Separator({
|
||||||
decorative={decorative}
|
decorative={decorative}
|
||||||
orientation={orientation}
|
orientation={orientation}
|
||||||
className={cn(
|
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",
|
'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
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
|
|
|
||||||
|
|
@ -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 { env } from '@/shared/config/env'
|
||||||
import { logger } from './logger'
|
import { logger } from './logger'
|
||||||
|
|
||||||
|
|
@ -17,6 +17,32 @@ export async function signIn(email: string, password: string) {
|
||||||
return supabase.auth.signInWithPassword({ email, password })
|
return supabase.auth.signInWithPassword({ email, password })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function signUp(email: string, password: string) {
|
||||||
|
return supabase.auth.signUp({ email, password })
|
||||||
|
}
|
||||||
|
|
||||||
export async function signOut() {
|
export async function signOut() {
|
||||||
return supabase.auth.signOut()
|
return supabase.auth.signOut()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function getCurrentSession(): Promise<Session | null> {
|
||||||
|
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 }
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue