# 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 `` 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` : `