xpeditis2.0/docs/design-system-audit.md
2026-08-13 12:26:46 +02:00

712 lines
35 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Design System Audit — Xpeditis
> **Statut : CHARTE VERROUILLÉE (IMMUTABLE).**
> Ce document est la source de vérité de l'identité visuelle Xpeditis.
> Toute refonte UI doit produire un rendu dont **chaque couleur, police, rayon et token** est présent dans ce document.
> Aucune valeur de ce document ne peut être modifiée dans le cadre d'une refonte UI.
**Date de l'audit :** 2026-08-12
**Périmètre :** `apps/frontend` (126 fichiers `.tsx`, ~38 000 lignes, 70 routes)
**Branche :** `ui_update`
---
## 0. Source de vérité actuelle
Il existe **quatre sources concurrentes**, ce qui est le problème structurel n°1 du projet :
| # | Source | Fichier | Autorité réelle | Statut |
|---|--------|---------|-----------------|--------|
| 1 | **Tailwind theme** | `apps/frontend/tailwind.config.ts` | ✅ **Source de vérité effective** | À conserver tel quel |
| 2 | Variables CSS shadcn | `apps/frontend/app/globals.css` (`:root`, `.dark`) | ⚠️ Partiellement contredite par (1) | Conflit documenté §1.3 |
| 3 | Classes `@layer components` | `apps/frontend/app/globals.css` | ⚠️ Quasi inutilisées dans l'app | Legacy |
| 4 | Doc marketing | `apps/frontend/DESIGN_SYSTEM.md` | ❌ Descriptive, non appliquée | Documentaire |
**Décision d'audit :** la source de vérité retenue est **`tailwind.config.ts`**, complétée par les variables CSS de `globals.css` pour les tokens `--border`, `--input`, `--muted`, `--radius`, `--destructive`.
> ⚠️ **Constat majeur :** l'application n'applique pas sa propre charte. On compte **816 occurrences de `blue-*` Tailwind (bleu générique, hors charte)** contre ~700 occurrences de tokens de marque, et **2 485 occurrences de `gray-*`** au lieu de l'échelle `neutral` navy définie dans le thème. Voir §23.
---
## 1. Color palette
### 1.1 Couleurs de marque — IMMUTABLES
Définies dans `tailwind.config.ts` → `theme.extend.colors`.
| Token Tailwind | Valeur HEX | Usage documenté | Occurrences |
|----------------|-----------|-----------------|-------------|
| `brand-navy` | `#10183A` | Couleur primaire, titres, surfaces sombres, sidebar admin | 123 `text-`, 46 `bg-` |
| `brand-turquoise` | `#34CCCD` | Accent, CTA, focus, liens | 137 `bg-`, 134 `text-` |
| `brand-green` | `#067224` | Succès, validation | 19 `bg-`, 11 `text-` |
| `brand-gray` | `#F2F2F2` | Fond de section clair | 8 `bg-` |
### 1.2 Alias sémantiques de marque — IMMUTABLES
| Token | Valeur | Note |
|-------|--------|------|
| `primary.DEFAULT` | `#10183A` | Override explicite du bleu shadcn |
| `primary.foreground` | `#FFFFFF` | |
| `primary.navy` | `#10183A` | Alias redondant |
| `accent.DEFAULT` | `#34CCCD` | Override explicite du gris shadcn |
| `accent.foreground` | `#FFFFFF` | |
| `accent.turquoise` | `#34CCCD` | Alias redondant |
| `success.DEFAULT` | `#067224` | |
| `success.light` | `#08A131` | |
| `success.dark` | `#044F19` | |
### 1.3 ⚠️ Conflit tokens Tailwind ↔ variables CSS
`globals.css` définit encore la palette shadcn par défaut, **contredite** par `tailwind.config.ts` :
| Token | Valeur CSS `:root` (globals.css) | Valeur Tailwind (config) | Qui gagne |
|-------|----------------------------------|--------------------------|-----------|
| `--primary` | `221.2 83.2% 53.3%` (bleu #3B82F6) | `#10183A` | **Tailwind** (`bg-primary` = navy) |
| `--accent` | `210 40% 96.1%` (gris quasi-blanc) | `#34CCCD` | **Tailwind** (`bg-accent` = turquoise) |
| `--ring` | `221.2 83.2% 53.3%` (bleu) | *(non overridé)* | **CSS** → ❗ **anneaux de focus bleus, hors charte** |
**Conséquence :** `focus-visible:ring-ring` de `Button` et `Input` produit un halo **bleu #3B82F6** sur toute l'application.
**Statut :** défaut d'implémentation, pas un choix de charte. `--ring` doit pointer vers le turquoise de marque — c'est une **restauration** de la charte, pas une modification.
### 1.4 Échelle neutre (dérivée du navy) — IMMUTABLE
`tailwind.config.ts` → `colors.neutral` :
| Token | HEX |
|-------|-----|
| `neutral-50` | `#F8F9FC` |
| `neutral-100` | `#EDEEF5` |
| `neutral-200` | `#DADBEB` |
| `neutral-300` | `#B0B6DA` |
| `neutral-400` | `#8590C9` |
| `neutral-500` | `#5A6BB8` |
| `neutral-600` | `#3A4A97` |
| `neutral-700` | `#2C3978` |
| `neutral-800` | `#1E2859` |
| `neutral-900` | `#10183A` |
> Échelle **froide, bleutée**, dérivée du navy. Actuellement quasi inutilisée (92 occurrences) au profit de `gray-*` Tailwind (2 485 occurrences), qui est une échelle **neutre-froide différente** (`#F9FAFB → #111827`).
### 1.5 Variables CSS shadcn conservées (mode clair)
`globals.css` `:root` :
| Variable | HSL | Équivalent |
|----------|-----|-----------|
| `--background` | `0 0% 100%` | `#FFFFFF` |
| `--foreground` | `222.2 84% 4.9%` | `#020817` |
| `--card` / `--popover` | `0 0% 100%` | `#FFFFFF` |
| `--card-foreground` / `--popover-foreground` | `222.2 84% 4.9%` | `#020817` |
| `--secondary` | `210 40% 96.1%` | `#F1F5F9` |
| `--secondary-foreground` | `222.2 47.4% 11.2%` | `#0F172A` |
| `--muted` | `210 40% 96.1%` | `#F1F5F9` |
| `--muted-foreground` | `215.4 16.3% 46.9%` | `#64748B` |
| `--destructive` | `0 84.2% 60.2%` | `#EF4444` |
| `--destructive-foreground` | `210 40% 98%` | `#F8FAFC` |
| `--border` | `214.3 31.8% 91.4%` | `#E2E8F0` |
| `--input` | `214.3 31.8% 91.4%` | `#E2E8F0` |
### 1.6 Couleurs hardcodées relevées
**Alignées charte (à conserver, idéalement tokenisées) :**
| HEX | Occurrences | Emplacements |
|-----|-------------|--------------|
| `#34CCCD` | 50 | `globals.css` (prose, tiptap), maps, charts, SVG |
| `#10183A` | 23 | `globals.css` (prose), charts, SVG |
| `#067224` | 3 | badges, charts |
| `#F2F2F2` | 1 | section landing |
**Hors charte (dérives) :**
| HEX | Occ. | Emplacement | Nature |
|-----|------|-------------|--------|
| `#1A2550`, `#1A2A5E` | 7 | dégradés navy | Nuances navy inventées |
| `#0E9999`, `#2BB8B9`, `#2AB8B9` | 4 | hovers turquoise | Nuances turquoise inventées |
| `#3B82F6`, `#2563EB`, `#1E3A8A` | 4 | charts Recharts, `Bar fill` | **Bleu générique** |
| `#10B981`, `#EF4444`, `#F59E0B` | 3 | `dashboard/page.tsx` — Pie chart status | Palette Tailwind brute |
| `#8884D8` | 1 | `dashboard/page.tsx` — `Pie fill` par défaut | **Violet par défaut Recharts (oubli)** |
| `#0D1117`, `#161B22` | 2 | `CodeBlock.tsx` | Thème GitHub Dark |
| `#FFD700`, `#FF6600`, `#FF00FF`, `#E30613`, `#006633`, `#003A70`, `#002B5C`, `#00243D`, `#E65100` | 9 | couleurs de compagnies maritimes / partenaires | **Légitime** (identités tierces) |
| `#F3F4F6`, `#E5E7EB`, `#ADB5BD`, `#F0F0F0` | 4 | prose, tiptap, grilles charts | Gris utilitaires |
### 1.7 Familles Tailwind utilisées hors charte
| Famille | Occurrences | Verdict |
|---------|-------------|---------|
| `gray-*` | 2 485 | Neutre de fait de l'app — **doublon** de `neutral-*` |
| `blue-*` | 816 | ❗ **Accent parallèle hors charte** (voir §23) |
| `red-*` | 368 | Destructif — acceptable, à tokeniser |
| `green-*` | 246 | Succès — doublon de `brand-green`/`success` |
| `amber-*` / `yellow-*` | 189 | Avertissement — acceptable, à tokeniser |
| `orange-*` | 66 | Doublon d'`amber` |
| `purple-*` / `violet-*` | 43 | ❗ Sans justification (KPI volume, badge Platinium) |
| `emerald-*` | 15 | Doublon de `green` |
| `slate-*` / `neutral-*` / `zinc-*` | 104 | Doublon de `gray` |
| `indigo-*`, `cyan-*`, `sky-*`, `teal-*`, `rose-*` | 24 | Bruit |
---
## 2. Typography
**Chargement :** `next/font/google` dans `apps/frontend/src/lib/fonts.ts`, `display: 'swap'`, `preload: true`, subset `latin`. Variables CSS injectées sur `<html>` dans `app/[locale]/layout.tsx`.
| Rôle | Police | Variable CSS | Alias Tailwind | Poids chargés |
|------|--------|--------------|----------------|---------------|
| Titres | **Manrope** | `--font-manrope` | `font-manrope`, `font-heading` | 200–800 |
| Corps / UI | **Montserrat** | `--font-montserrat` | `font-montserrat`, `font-body`, `font-sans` (défaut) | 100–900 |
- `body` porte `font-body` (Montserrat) — `app/[locale]/layout.tsx` + `globals.css`.
- `h1`–`h6` portent `font-heading` (Manrope) via `@layer base` dans `globals.css`.
- `.prose h1..h4` forcent Manrope (`globals.css`).
**IMMUTABLE :** aucune autre famille ne doit être introduite. Aucune police système, aucune substitution.
---
## 3. Font weights
| Contexte | Poids | Défini dans |
|----------|-------|-------------|
| `display-lg` | 800 | `tailwind.config.ts` |
| `display-md`, `display-sm`, `h1` | 700 | `tailwind.config.ts` |
| `h2`, `h3`, `h4` | 600 | `tailwind.config.ts` |
| `h5`, `h6` | 500 | `tailwind.config.ts` |
| `body-*` | 400 | `tailwind.config.ts` |
| `label-*` | 600 | `tailwind.config.ts` |
| Boutons (`Button`) | 500 (`font-medium`) | `src/components/ui/button.tsx` |
| Boutons (`.btn-*` CSS) | 600 (`font-semibold`) | `globals.css` — ⚠️ incohérent avec ci-dessus |
| Badge | 600 (`font-semibold`) | `src/components/ui/badge.tsx` |
| CardTitle | 600 | `src/components/ui/card.tsx` |
| TableHead | 500 | `src/components/ui/table.tsx` |
---
## 4. Font sizes
Échelle complète définie dans `tailwind.config.ts` → `theme.extend.fontSize`. **IMMUTABLE.**
### Display
| Token | Taille | Line-height | Letter-spacing | Weight |
|-------|--------|-------------|----------------|--------|
| `text-display-lg` | `4.5rem` / 72px | 1.1 | −0.02em | 800 |
| `text-display-md` | `3.75rem` / 60px | 1.15 | −0.02em | 700 |
| `text-display-sm` | `3rem` / 48px | 1.2 | −0.01em | 700 |
### Headings
| Token | Taille | Line-height | Weight |
|-------|--------|-------------|--------|
| `text-h1` | `2.5rem` / 40px | 1.25 | 700 |
| `text-h2` | `2rem` / 32px | 1.3 | 600 |
| `text-h3` | `1.5rem` / 24px | 1.35 | 600 |
| `text-h4` | `1.25rem` / 20px | 1.4 | 600 |
| `text-h5` | `1.125rem` / 18px | 1.45 | 500 |
| `text-h6` | `1rem` / 16px | 1.5 | 500 |
### Body
| Token | Taille | Line-height | Weight |
|-------|--------|-------------|--------|
| `text-body-lg` | `1.125rem` / 18px | 1.6 | 400 |
| `text-body` | `1rem` / 16px | 1.6 | 400 |
| `text-body-sm` | `0.875rem` / 14px | 1.55 | 400 |
| `text-body-xs` | `0.75rem` / 12px | 1.5 | 400 |
### Labels
| Token | Taille | Line-height | Weight | Letter-spacing |
|-------|--------|-------------|--------|----------------|
| `text-label-lg` | `0.875rem` / 14px | 1.4 | 600 | 0.05em |
| `text-label` | `0.75rem` / 12px | 1.4 | 600 | 0.05em |
| `text-label-sm` | `0.6875rem` / 11px | 1.4 | 600 | 0.05em |
> ⚠️ **Constat :** cette échelle est **quasi inutilisée dans l'app produit**. Les pages utilisent l'échelle Tailwind par défaut (`text-xs`, `text-sm`, `text-base`, `text-lg`, `text-2xl`, `text-3xl`). Les deux échelles divergent (ex. `text-h1` = 40px vs `text-3xl` = 30px).
### Échelle Tailwind par défaut réellement utilisée
`text-[10px]`, `text-xs` (12), `text-sm` (14), `text-base` (16), `text-lg` (18), `text-xl` (20), `text-2xl` (24), `text-3xl` (30), `text-4xl` (36), `text-5xl` (48), `text-6xl` (60).
---
## 5. Line heights
| Source | Valeurs |
|--------|---------|
| Tokens `fontSize` (config) | 1.1 / 1.15 / 1.2 / 1.25 / 1.3 / 1.35 / 1.4 / 1.45 / 1.5 / 1.55 / 1.6 |
| Utilitaires employés dans l'app | `leading-none`, `leading-tight`, `leading-relaxed`, `leading-tight` sur `[10px]` |
Aucune échelle `lineHeight` custom n'est déclarée dans `tailwind.config.ts` — seules les valeurs couplées aux `fontSize` existent.
---
## 6. Spacing system
**Échelle Tailwind par défaut (base 4px)** — aucune extension `spacing` dans la config.
Valeurs réellement employées, par fréquence :
| Rang | Classe | Occ. |
|------|--------|------|
| 1 | `py-2` | 315 |
| 2 | `px-6` | 308 |
| 3 | `px-4` | 295 |
| 4 | `mb-4` | 213 |
| 5 | `py-3` | 202 |
| 6 | `mt-1` / `mb-2` | 174 / 168 |
| 7 | `px-3` | 168 |
| 8 | `p-4` | 149 |
| 9 | `mb-6` | 147 |
| 10 | `py-4` | 125 |
| 11 | `gap-4` / `gap-2` / `gap-3` | 118 / 115 / 68 |
| 12 | `p-6` / `p-3` / `p-8` | 90 / 77 / 59 |
| 13 | `space-y-2` / `space-y-6` / `space-y-4` | 71 / 68 / 52 |
**Container Tailwind** (`theme.container`) : `center: true`, `padding: 2rem`, `screens.2xl: 1400px`.
**Rythme de padding constaté :** `p-4` (mobile/compact) et `p-6` (desktop/card) dominants — c'est le rythme de fait.
---
## 7. Border radius
**Token racine :** `--radius: 0.5rem` (8px) — `globals.css`. **IMMUTABLE.**
| Token Tailwind | Calcul | Valeur |
|----------------|--------|--------|
| `rounded-sm` | `calc(var(--radius) - 4px)` | **4px** |
| `rounded-md` | `calc(var(--radius) - 2px)` | **6px** |
| `rounded-lg` | `var(--radius)` | **8px** |
Les autres rayons proviennent de l'échelle Tailwind par défaut (`rounded` 4px, `rounded-xl` 12px, `rounded-2xl` 16px, `rounded-3xl` 24px, `rounded-full` 9999px).
**Usage réel :**
| Classe | Occ. | Rayon |
|--------|------|-------|
| `rounded-lg` | 452 | 8px — **rayon standard de l'app** |
| `rounded-full` | 241 | pilules, avatars, badges |
| `rounded-md` | 188 | 6px — contrôles (Button, Input) |
| `rounded-xl` | 95 | 12px |
| `rounded` | 83 | 4px |
| `rounded-2xl` | 62 | 16px |
| `rounded-3xl` | 10 | 24px |
**Rayons canoniques retenus (IMMUTABLES) :** `rounded-md` (contrôles) · `rounded-lg` (cartes, conteneurs) · `rounded-full` (pilules/avatars).
`rounded-xl`, `2xl`, `3xl` sont des dérives ponctuelles (landing, modales) — non canoniques.
---
## 8. Shadows
Aucune extension `boxShadow` dans `tailwind.config.ts` → **échelle Tailwind par défaut**.
| Classe | Occ. | Valeur CSS |
|--------|------|-----------|
| `shadow-sm` | 123 | `0 1px 2px 0 rgb(0 0 0 / 0.05)` |
| `shadow-lg` | 76 | `0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px …` |
| `shadow` | 39 | `0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px …` |
| `shadow-xl` | 36 | `0 20px 25px -5px rgb(0 0 0 / 0.1), …` |
| `shadow-md` | 28 | `0 4px 6px -1px rgb(0 0 0 / 0.1), …` |
| `shadow-2xl` | 23 | `0 25px 50px -12px rgb(0 0 0 / 0.25)` |
| `shadow-brand` | 2 | ❗ **classe inexistante** — aucun token `shadow-brand` déclaré. Rend un no-op. |
**Élévation canonique constatée :** `shadow-sm` pour les cartes (+ `hover:shadow-md`), `shadow-lg` pour sidebar/dropdowns, `shadow-xl`/`2xl` pour modales.
---
## 9. Borders
| Élément | Valeur | Source |
|---------|--------|--------|
| Couleur par défaut globale | `hsl(var(--border))` = `#E2E8F0` | `globals.css` — règle `* { @apply border-border }` |
| Bordure des inputs | `hsl(var(--input))` = `#E2E8F0` | `globals.css` + `ui/input.tsx` |
| Épaisseur standard | `1px` (`border`) | — |
| Bouton outline (CSS legacy) | `2px` (`border-2 border-primary`) | `globals.css` `.btn-outline` |
| Input legacy CSS | `border-neutral-300` = `#B0B6DA` | `globals.css` `.input` |
| Bordures de cartes en pages | `border-gray-200` = `#E5E7EB` | ⚠️ hardcodé, doublon de `--border` |
| Séparateurs internes | `border-gray-100` = `#F3F4F6` / `divide-gray-100` | ⚠️ hardcodé |
| Sidebar admin (sur navy) | `border-white/10` | `admin/layout.tsx` |
---
## 10. Icons
| Aspect | Valeur |
|--------|--------|
| Bibliothèque | **`lucide-react` v0.294.0** — unique et exclusive |
| Tailles employées | `w-3 h-3` (12), `w-4 h-4` (16, dominante), `w-5 h-5` (20, nav), `w-6 h-6` (24), `h-3 w-3` |
| Épaisseur | `strokeWidth` par défaut (2), sauf SVG inline hamburger/close (`strokeWidth={2}` explicite) |
| Couleur | Hérite de `currentColor` |
| Exceptions | SVG inline manuscrits pour le menu burger et la fermeture de sidebar (`dashboard/layout.tsx`, `admin/layout.tsx`) — **doublons de `Menu`/`X` de lucide** |
**Icônes de marque / logos :**
| Asset | Chemin | Usage |
|-------|--------|-------|
| Logo noir | `public/assets/logos/logo-black.svg` | Sidebar dashboard, favicon, OG image |
| Logo blanc (SVG) | `public/assets/logos/logo-white.svg` | Sidebar admin, footer navy |
| Logo blanc (PNG) | `public/assets/logos/logo-white.png` | Fallback |
| Logos partenaires | `public/assets/logos/partner/*.png` (7 fichiers, noms avec espaces) | Landing — bandeau partenaires |
| Images | `background-login.png`, `background-section-1-landingpage.png` | Auth, landing |
| Vidéos | `demo-xpeditis.mp4` / `.webm` | Modale démo |
**IMMUTABLE :** logos, fichiers de marque, et le choix de `lucide-react`.
---
## 11. Buttons
Trois systèmes coexistent — **c'est une incohérence, pas une charte** :
### 11.1 `src/components/ui/button.tsx` (composant React)
Adoption : **17 usages dans 8 fichiers sur 126**.
Base : `inline-flex items-center justify-center rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:opacity-50 disabled:pointer-events-none`
| Variant | Classes |
|---------|---------|
| `default` | `bg-primary text-primary-foreground hover:bg-primary/90` → navy |
| `destructive` | `bg-destructive text-destructive-foreground hover:bg-destructive/90` → rouge |
| `outline` | `border border-input bg-background hover:bg-accent hover:text-accent-foreground` → hover **turquoise plein** |
| `secondary` | `bg-secondary text-secondary-foreground hover:bg-secondary/80` |
| `ghost` | `hover:bg-accent hover:text-accent-foreground` → hover **turquoise plein** |
| `link` | `text-primary underline-offset-4 hover:underline` |
| Size | Classes | Hauteur |
|------|---------|---------|
| `default` | `h-10 px-4 py-2` | 40px |
| `sm` | `h-9 rounded-md px-3` | 36px |
| `lg` | `h-11 rounded-md px-8` | 44px |
| `icon` | `h-10 w-10` | 40×40 |
> ⚠️ `hover:bg-accent` sur `ghost`/`outline` produit un aplat **turquoise #34CCCD** au survol (car `accent` est overridé) — visuellement agressif et non intentionnel.
> ⚠️ Pas de `cn()` / `tailwind-merge` : la concaténation de string empêche l'override propre des classes.
> ⚠️ Pas de `asChild` : `<Link><Button>` génère un `<a>` contenant un `<button>` (nesting invalide, utilisé sur le dashboard).
### 11.2 Classes `@layer components` (`globals.css`)
Adoption : **réelle mais minoritaire** — `.btn-primary` dans 8 fichiers, `.btn-secondary` 3, `.btn-success` 2, `.btn-outline` 2. Les classes `.label` et `.link` sont largement employées dans `/register` et `/admin/login`.
⚠️ Ces classes ne peuvent donc **pas** être supprimées avant refonte des pages concernées (lots L8/L9).
| Classe | Style |
|--------|-------|
| `.btn-primary` | `bg-accent text-white font-heading font-semibold px-6 py-3 rounded-lg hover:bg-accent/90 shadow-sm hover:shadow` |
| `.btn-secondary` | `bg-primary text-white font-heading font-semibold px-6 py-3 rounded-lg hover:bg-neutral-800` |
| `.btn-success` | `bg-success text-white font-heading font-semibold px-6 py-3 rounded-lg hover:bg-success-dark` |
| `.btn-outline` | `border-2 border-primary text-primary font-heading font-semibold px-6 py-3 rounded-lg hover:bg-primary hover:text-white` |
> Noter l'inversion sémantique : `.btn-primary` est **turquoise** alors que `Button variant="default"` est **navy**.
### 11.3 `<button>` bruts
**299 occurrences** de `<button` contre **17** de `<Button`. La très grande majorité des boutons de l'application sont stylés à la main, majoritairement en `bg-blue-600 hover:bg-blue-700`.
---
## 12. Inputs
### 12.1 `src/components/ui/input.tsx`
Adoption : **16 usages dans 5 fichiers**.
`flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50`
- Hauteur : **40px** · Rayon : **6px** · Texte : **14px** · Focus : anneau 2px **bleu (`--ring`)** ❗
### 12.2 `.input` (globals.css)
`border border-neutral-300 rounded-lg px-4 py-2.5 font-body text-body focus:ring-2 focus:ring-accent focus:border-transparent`
→ rayon **8px**, focus **turquoise**, texte **16px**. Incohérent avec 12.1.
### 12.3 `.label` (globals.css)
`text-label text-neutral-600 font-body font-semibold mb-1.5 block uppercase tracking-wider`
### 12.4 `<input>` bruts
**175 occurrences** contre 16 `<Input>`. Pattern dominant en page :
`w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent` ❗ (focus bleu hors charte)
### 12.5 Select / Popover / Command — ❗ NON FONCTIONNELS
`package.json` déclare `@radix-ui/react-dialog`, `react-dropdown-menu`, `react-label`, `react-select`, `react-slot`, `react-tabs`.
**Aucun import `@radix-ui/*` n'existe dans le code source.** Les fichiers `select.tsx`, `popover.tsx`, `command.tsx`, `dialog.tsx` sont des ré-implémentations manuelles incomplètes :
- `SelectValue` retourne `<span>{placeholder}</span>` — **n'affiche jamais la valeur sélectionnée**.
- `Popover` n'est pas portalisé (`position: relative` local) → clipping par `overflow: hidden`.
- Aucun focus trap, aucune gestion clavier, aucun `aria-*` complet.
---
## 13. Cards
### `src/components/ui/card.tsx`
Adoption : **19 fichiers** — le primitif le mieux adopté.
| Sous-composant | Classes |
|----------------|---------|
| `Card` | `rounded-lg border bg-card text-card-foreground shadow-sm` |
| `CardHeader` | `flex flex-col space-y-1.5 p-6` |
| `CardTitle` | `text-2xl font-semibold leading-none tracking-tight` (rendu en `<h3>`) |
| `CardDescription` | `text-sm text-muted-foreground` |
| `CardContent` | `p-6 pt-0` |
| `CardFooter` | `flex items-center p-6 pt-0` |
> ⚠️ `CardTitle` à `text-2xl` (24px) est trop gros : **toutes les pages l'écrasent** en `text-base font-semibold text-gray-900`. Le défaut est donc systématiquement contourné.
> ⚠️ `CardHeader p-6` également écrasé en `p-4` / `pb-3` / `pb-4` partout.
### `.card` (globals.css)
`bg-white rounded-lg shadow-md p-6` + `.card-header` `font-heading font-semibold text-h4 text-brand-navy mb-4` — non utilisé.
### Pattern de carte réel en pages
`border border-gray-200 shadow-sm hover:shadow-md transition-shadow bg-white` + `CardContent className="p-4"`.
---
## 14. Tables
### `src/components/ui/table.tsx`
Adoption : **2 fichiers** seulement. **26 `<table>` bruts** ailleurs.
| Élément | Classes |
|---------|---------|
| Wrapper | `w-full overflow-auto` |
| `table` | `w-full caption-bottom text-sm` |
| `thead` | `[&_tr]:border-b` |
| `tbody` | `[&_tr:last-child]:border-0` |
| `tr` | `border-b transition-colors hover:bg-muted/50 data-[state=selected]:bg-muted` |
| `th` | `h-12 px-4 text-left align-middle font-medium text-muted-foreground` |
| `td` | `p-4 align-middle` |
| `caption` | `mt-4 text-sm text-muted-foreground` |
- Hauteur de ligne d'en-tête : **48px** · Padding cellule : **16px** · Texte : **14px**
- `@tanstack/react-table` v8 et `@tanstack/react-virtual` v3 sont installés — utilisés uniquement dans `rate-search/RateResultsTable.tsx`.
- **21 fichiers** gèrent le débordement horizontal via `overflow-x-auto` à la main.
- **Aucun composant de table responsive** : pas de bascule carte-sur-mobile mutualisée.
---
## 15. Navigation
### 15.1 Chrome public (landing)
`src/components/layout/LandingHeader.tsx` (388 l.) + `LandingFooter.tsx`. Utilise les tokens de marque (navy/turquoise).
### 15.2 Chrome application (`app/[locale]/dashboard/layout.tsx`)
- **Topbar** : `sticky top-0 z-10`, hauteur `h-14` mobile / `h-16` desktop, `bg-white border-b`.
Contenu : burger (mobile) · titre dérivé de l'item de nav actif · `LanguageSwitcher` · `NotificationDropdown` · avatar `bg-blue-600` ❗
- **Titre de topbar** : `text-base lg:text-xl font-semibold text-gray-900` — **doublonne le `<h1>` de chaque page** (double titre à l'écran).
### 15.3 Bottom nav mobile
`fixed bottom-0 z-30`, `grid-cols-5 h-16`, items : Home / Bookings / Documents / Tracking / Profile.
Actif : `text-blue-600` ❗ · Labels `text-[10px]`.
> ⚠️ Ne couvre que 5 des 8 entrées de la sidebar ; Organisation, Wiki, Clés API sont **inaccessibles en navigation mobile primaire** (uniquement via le burger).
### 15.4 Breadcrumbs
**Aucun composant fil d'Ariane n'existe** dans le projet.
---
## 16. Sidebar
### Dashboard (`dashboard/layout.tsx`)
| Propriété | Valeur |
|-----------|--------|
| Largeur | `w-64` (256px), fixe, non repliable |
| Fond | `bg-white shadow-lg` |
| Comportement | `fixed inset-y-0 left-0 z-50`, `lg:translate-x-0`, drawer `-translate-x-full` sous `lg` |
| Transition | `transform transition-transform duration-300 ease-in-out` |
| Overlay mobile | `fixed inset-0 z-40 bg-gray-600 bg-opacity-75 lg:hidden` |
| En-tête | `h-16 px-6 border-b`, logo `logo-black.svg` 50×60 |
| Item nav | `flex items-center px-4 py-3 text-sm font-medium rounded-lg transition-colors` |
| Item actif | `bg-blue-50 text-blue-700` ❗ |
| Item inactif | `text-gray-700 hover:bg-gray-100` |
| Item verrouillé | `text-gray-400 hover:bg-gray-50` + icône `Lock w-4 h-4 text-gray-300`, redirige vers `/pricing` |
| Icône nav | `mr-3 h-5 w-5` |
| Espacement | `px-4 py-6 space-y-2` |
| Lien Admin | `bg-brand-navy text-white hover:bg-brand-navy/90` ✅ (seul élément de chrome à la charte) |
| Pied | avatar `w-10 h-10 bg-blue-600 rounded-full` ❗ + nom + email + `StatusBadge` + bouton logout `text-red-700 bg-red-50` |
| Décalage contenu | `lg:pl-64` |
### Admin (`admin/layout.tsx`)
Même géométrie, mais fond `bg-brand-navy text-white`, logo `logo-white.svg` 44×52, séparateurs `border-white/10`. ✅ Conforme charte.
**Navigation dashboard (8 entrées) :** Dashboard · Réservations · Documents · Tracking · Wiki · Organisation · Clés API · Utilisateurs (ADMIN/MANAGER). Chaque entrée peut porter un `requiredFeature` (gating par plan via `useSubscription`).
**Navigation admin (7 entrées) :** Utilisateurs · Organisations · Réservations · Documents · Grilles CSV · Blog · Logs.
---
## 17. Modals
`src/components/ui/dialog.tsx` — implémentation **manuelle**, adoption : **1 fichier**.
| Élément | Classes |
|---------|---------|
| Overlay | `fixed inset-0 z-50 bg-background/80 backdrop-blur-sm` |
| Panneau | `fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg sm:rounded-lg` |
| Header | `flex flex-col space-y-1.5 text-center sm:text-left` |
| Title | `text-lg font-semibold leading-none tracking-tight` (`<h3>`) |
| Description | `text-sm text-muted-foreground` |
| Footer | `flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2` |
**Défauts :** pas de portal · pas de focus trap · pas de fermeture par `Escape` · pas de scroll-lock du body · pas de `role="dialog"` / `aria-modal` · pas d'animation d'entrée/sortie · `React.cloneElement` sur tous les enfants (casse dès qu'un enfant est un texte ou un fragment).
**Conséquence :** la quasi-totalité des modales de l'app sont réimplémentées inline dans les pages (`documents/page.tsx` en contient 3 : ajout, remplacement, suppression).
---
## 18. Toasts
**❌ Aucun système de toast / notification transitoire n'existe dans le projet.**
Le retour utilisateur est assuré par :
- **44 appels à `alert()` natif**
- **14 appels à `confirm()` natif**
- des bandeaux d'erreur inline ad hoc (`bg-red-50 border-red-200 text-red-700`)
Aucune dépendance `sonner` / `react-hot-toast` / `@radix-ui/react-toast` n'est installée.
> **Note pour la refonte :** introduire un système de toast est un **ajout de composant**, pas une modification de charte — les couleurs employées devront provenir strictement de §1.
---
## 19. States
| État | Traitement actuel | Source |
|------|-------------------|--------|
| **Hover (bouton)** | `hover:bg-{color}/90` ou `/80` | `button.tsx` |
| **Hover (nav)** | `hover:bg-gray-100` | layouts |
| **Hover (ligne de table)** | `hover:bg-muted/50` ou `hover:bg-gray-50` | `table.tsx` / pages |
| **Hover (carte)** | `hover:shadow-md transition-shadow` | pages |
| **Focus** | `focus:ring-2 focus:ring-blue-500` (454 occ. `focus:`) ❗ | pages |
| **Focus-visible** | `focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2` (13 occ. seulement) | primitives |
| **Active** | ❌ Aucun style `active:` systématique | — |
| **Disabled** | `disabled:opacity-50 disabled:pointer-events-none` (Button) / `disabled:cursor-not-allowed disabled:opacity-50` (Input) | primitives |
| **Loading** | `animate-spin` sur bordure ou `Loader2` — **40 fichiers**, aucune mutualisation | pages |
| **Skeleton** | **1 seul fichier** ; ailleurs `bg-gray-100 animate-pulse rounded` inline | `dashboard/page.tsx` |
| **Empty** | 27 fichiers avec `length === 0`, chaque empty state réécrit à la main | pages |
| **Error** | 29 fichiers, bandeaux inline ad hoc | pages |
| **Selected** | `data-[state=selected]:bg-muted` (table uniquement) | `table.tsx` |
| **Locked (feature gating)** | `text-gray-400` + icône `Lock` + redirection `/pricing` | `dashboard/layout.tsx` |
**Transitions employées :** `transition-colors` (dominante), `transition-shadow`, `transition-transform`, `transition-all`. Durée par défaut Tailwind (150ms), sauf sidebar `duration-300 ease-in-out`.
---
## 20. Responsive behavior
**Breakpoints** = valeurs Tailwind par défaut, non modifiées :
| Nom | Min-width | Occurrences dans le code |
|-----|-----------|--------------------------|
| `sm` | 640px | **287** |
| `md` | 768px | **120** |
| `lg` | 1024px | **259** |
| `xl` | 1280px | **14** |
| `2xl` | 1536px | **0** ❗ |
`container.screens.2xl` = 1400px (config).
**Conteneurs de page :** `max-w-7xl` (1280px) — 63 occurrences, dominante. Puis `max-w-2xl` (37), `max-w-md` (36, formulaires auth), `max-w-4xl` (27), `max-w-3xl` (25).
**Bascule principale de l'app shell :** `lg` (1024px) — sidebar fixe ≥ lg, drawer + bottom-nav < lg.
**Constats :**
- **Aucune adaptation au-delà de 1280px.** Sur un écran 1920/2560px, le contenu reste en colonne de 1280px centrée avec de larges marges vides.
- Le palier **`md` (tablette 768–1023px) est sous-traité** : la sidebar est encore un drawer, mais le layout de contenu est déjà en grille desktop → zone la plus fragile.
- 21 tables reposent sur `overflow-x-auto` : sur mobile, la lecture se fait par défilement horizontal, sans alternative en cartes.
---
## 21. Dark / light mode
| Aspect | État |
|--------|------|
| Stratégie Tailwind | `darkMode: ['class']` — configurée |
| Variables `.dark` | **Définies intégralement** dans `globals.css` (14 tokens) |
| Classes `dark:` dans le code | **5 occurrences au total** (dont 1 dans `ui/alert.tsx`) |
| Toggle de thème | **❌ Inexistant** |
| Attribut `class="dark"` posé | **❌ Jamais** — `<html>` ne reçoit que les variables de police |
| `prefers-color-scheme` | Non géré |
**Verdict : l'application est en mode clair exclusif.** Le dark mode est un vestige de scaffolding shadcn, non fonctionnel.
**Palette `.dark` définie (dormante) :** `--background: 222.2 84% 4.9%` · `--foreground: 210 40% 98%` · `--card: 222.2 84% 4.9%` · `--primary: 217.2 91.2% 59.8%` · `--secondary/--muted/--accent: 217.2 32.6% 17.5%` · `--destructive: 0 62.8% 30.6%` · `--border/--input: 217.2 32.6% 17.5%` · `--ring: 224.3 76.3% 48%`.
---
## 22. Animation principles
### Déclarées dans `tailwind.config.ts`
| Animation | Keyframes | Durée / easing |
|-----------|-----------|----------------|
| `accordion-down` | `height: 0 → var(--radix-accordion-content-height)` | `0.2s ease-out` |
| `accordion-up` | inverse | `0.2s ease-out` |
Plugin **`tailwindcss-animate`** installé → `animate-in`, `animate-out`, `fade-in`, `zoom-in`, `slide-in-from-*`, `duration-*`, `ease-*` disponibles. **Quasi inutilisé.**
### Bibliothèque
**`framer-motion` v12** — présent dans **16 fichiers** (principalement landing / marketing).
### Animations réellement en production
| Effet | Usage |
|-------|-------|
| `animate-spin` | 40 fichiers — loaders |
| `animate-pulse` | skeletons inline |
| `transition-colors` | dominante, 150ms |
| `transition-transform duration-300 ease-in-out` | ouverture/fermeture sidebar |
| `transition-shadow` | hover des cartes |
**Principes constatés (à formaliser) :** durée courte (150–300ms), easing `ease-out`/`ease-in-out`, animation limitée à `color`, `background-color`, `box-shadow`, `transform`, `opacity`. Aucune animation de layout coûteuse.
**Aucun respect de `prefers-reduced-motion` nulle part.**
---
## 23. ❗ Constat central : deux identités visuelles parallèles
C'est le résultat le plus important de cet audit.
| Zone | Identité appliquée |
|------|--------------------|
| Landing, pages marketing, footer, sidebar **admin** | ✅ **Charte Xpeditis** — navy `#10183A` + turquoise `#34CCCD` |
| **Toute l'application produit** (dashboard, réservations, documents, recherche, paramètres, wiki) | ❌ **Bleu Tailwind générique** `blue-600 #2563EB` / `blue-50` / `blue-700` |
**Mesure :** `blue-*` apparaît **816 fois dans 64 des 126 fichiers `.tsx`**, contre ~700 occurrences cumulées de l'ensemble des tokens de marque. Les foyers principaux :
| Fichier | Occ. `blue-*` |
|---------|---------------|
| `dashboard/bookings/new/page.tsx` | 31 |
| `admin/blog/page.tsx` | 30 |
| `dashboard/search-advanced/page.tsx` | 28 |
| `dashboard/track-trace/page.tsx` | 27 |
| `dashboard/documents/page.tsx` | 27 |
| `dashboard/booking/[id]/pay/page.tsx` | 20 |
| *(+ 58 autres fichiers)* | |
Éléments de chrome concernés : item de sidebar actif, avatars utilisateur (×2), item de bottom-nav actif, CTA principal du dashboard, anneaux de focus de tous les formulaires, barre de progression du tunnel de réservation, `Bar fill` des graphiques.
**Position d'audit :** ce bleu **n'appartient pas à la charte**. Il n'est déclaré dans aucune des quatre sources du §0. Le remplacer par `brand-navy` / `brand-turquoise` selon le rôle constitue une **restauration de la charte**, pas une modification — mais comme il s'agit du changement visuel le plus visible de la refonte, **il est soumis à validation explicite avant toute exécution** (voir §24).
---
## 24. Règles de verrouillage pour la refonte
### ❌ Interdit — modification de charte
1. Modifier `brand-navy`, `brand-turquoise`, `brand-green`, `brand-gray`.
2. Modifier `primary.*`, `accent.*`, `success.*`.
3. Modifier l'échelle `neutral-50…900`.
4. Changer Manrope / Montserrat, ou leurs affectations (titres / corps).
5. Modifier l'échelle `fontSize` de `tailwind.config.ts`.
6. Modifier `--radius: 0.5rem` ou les dérivés `sm`/`md`/`lg`.
7. Remplacer `lucide-react`.
8. Modifier ou substituer les logos et assets de marque.
9. Introduire une couleur d'accent nouvelle, un dégradé décoratif, une teinte inventée.
### ✅ Autorisé — travail de design
1. Remplacer une couleur **hors charte** par le token de charte correspondant (`blue-600` → `brand-turquoise`/`brand-navy`, `gray-*` → `neutral-*`).
2. Corriger `--ring` pour pointer vers le turquoise de marque (restauration).
3. **Ajouter** des tokens purement structurels absents (échelle d'élévation nommée, durées d'animation, largeurs de sidebar) — sans introduire de couleur nouvelle.
4. Réviser composition, hiérarchie, densité, espacement, responsive, états, accessibilité.
5. Mutualiser, refactorer et compléter les composants.
6. Ajouter les composants manquants (Toast, Skeleton, EmptyState, Breadcrumb, Drawer, Pagination, CommandMenu).
### Procédure de contrôle
Avant chaque commit de refonte :
```bash
cd apps/frontend
git diff --stat tailwind.config.ts app/globals.css # doit rester vide, sauf --ring validé
grep -rE "\b(bg|text|border|ring)-blue-" app src --include="*.tsx" | wc -l # doit décroître, jamais croître
npm run type-check && npm run lint && npm run build
```
Toute modification non validée d'un token existant → **retour à la valeur d'origine**.