35 KiB
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 degray-*au lieu de l'échelleneutralnavy 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 |
bodyportefont-body(Montserrat) —app/[locale]/layout.tsx+globals.css.h1–h6portentfont-heading(Manrope) via@layer basedansglobals.css..prose h1..h4forcent 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 vstext-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-accentsurghost/outlineproduit un aplat turquoise #34CCCD au survol (caraccentest overridé) — visuellement agressif et non intentionnel. ⚠️ Pas decn()/tailwind-merge: la concaténation de string empêche l'override propre des classes. ⚠️ Pas deasChild:<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-primaryest turquoise alors queButton 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 :
SelectValueretourne<span>{placeholder}</span>— n'affiche jamais la valeur sélectionnée.Popovern'est pas portalisé (position: relativelocal) → clipping paroverflow: 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 entext-base font-semibold text-gray-900. Le défaut est donc systématiquement contourné. ⚠️CardHeader p-6également écrasé enp-4/pb-3/pb-4partout.
.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-tablev8 et@tanstack/react-virtualv3 sont installés — utilisés uniquement dansrate-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, hauteurh-14mobile /h-16desktop,bg-white border-b. Contenu : burger (mobile) · titre dérivé de l'item de nav actif ·LanguageSwitcher·NotificationDropdown· avatarbg-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
- Modifier
brand-navy,brand-turquoise,brand-green,brand-gray. - Modifier
primary.*,accent.*,success.*. - Modifier l'échelle
neutral-50…900. - Changer Manrope / Montserrat, ou leurs affectations (titres / corps).
- Modifier l'échelle
fontSizedetailwind.config.ts. - Modifier
--radius: 0.5remou les dérivéssm/md/lg. - Remplacer
lucide-react. - Modifier ou substituer les logos et assets de marque.
- Introduire une couleur d'accent nouvelle, un dégradé décoratif, une teinte inventée.
✅ Autorisé — travail de design
- Remplacer une couleur hors charte par le token de charte correspondant (
blue-600→brand-turquoise/brand-navy,gray-*→neutral-*). - Corriger
--ringpour pointer vers le turquoise de marque (restauration). - Ajouter des tokens purement structurels absents (échelle d'élévation nommée, durées d'animation, largeurs de sidebar) — sans introduire de couleur nouvelle.
- Réviser composition, hiérarchie, densité, espacement, responsive, états, accessibilité.
- Mutualiser, refactorer et compléter les composants.
- Ajouter les composants manquants (Toast, Skeleton, EmptyState, Breadcrumb, Drawer, Pagination, CommandMenu).
Procédure de contrôle
Avant chaque commit de refonte :
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.