xpeditis2.0/docs/ui-architecture.md
2026-08-13 12:26:46 +02:00

696 lines
40 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.

# Nouvelle architecture UI & plan de refonte — Xpeditis
**Date :** 2026-08-12 · **Branche :** `ui_update`
**Prérequis de lecture :** [`design-system-audit.md`](./design-system-audit.md) (charte verrouillée) · [`ui-audit.md`](./ui-audit.md) (cartographie)
> **Contrainte cardinale.** Toute valeur visuelle produite ici provient du §1–§22 de `design-system-audit.md`.
> Aucune couleur, police, rayon ou token n'est introduit. Ce document ne fait que **réorganiser** et **appliquer** l'existant.
---
## Sommaire
- [1. Principes directeurs](#1-principes-directeurs)
- [2. Vocabulaire visuel appliqué](#2-vocabulaire-visuel-appliqué)
- [3. App shell](#3-app-shell)
- [4. Composants de navigation](#4-composants-de-navigation)
- [5. Composants de contenu](#5-composants-de-contenu)
- [6. Composants de saisie](#6-composants-de-saisie)
- [7. Composants de surface](#7-composants-de-surface)
- [8. Composants d'état](#8-composants-détat)
- [9. Architecture de fichiers](#9-architecture-de-fichiers)
- [10. Responsive](#10-responsive)
- [11. Micro-interactions](#11-micro-interactions)
- [12. Accessibilité](#12-accessibilité)
- [13. Plan de refonte par lots](#13-plan-de-refonte-par-lots)
- [14. Protocole de validation](#14-protocole-de-validation)
- [15. Ce que je ne ferai pas](#15-ce-que-je-ne-ferai-pas)
---
## 1. Principes directeurs
Quatre règles arbitrent chaque décision de la refonte. Elles sont volontairement peu nombreuses et opposables.
**P1 — Le navy porte la structure, le turquoise porte l'action.**
`brand-navy #10183A` = chrome, titres, surfaces d'autorité, état actif.
`brand-turquoise #34CCCD` = action primaire, focus, sélection, accent de données.
Cette règle remplace, à elle seule, les 816 usages de bleu générique. Elle n'invente rien : c'est la lecture littérale de la charte.
**P2 — La densité est une fonctionnalité.**
Xpeditis est un outil de transitaire : listes longues, colonnes nombreuses, comparaison de taux. La refonte va vers **plus d'information par écran**, pas moins. Concrètement : cartes KPI alignées à gauche et non centrées, tables en densité compacte par défaut, padding `p-4`/`p-5` plutôt que `p-6`, suppression des conteneurs décoratifs intermédiaires.
**P3 — Une seule manière de faire chaque chose.**
Un bouton, un champ, une table, une modale, un état vide. Les trois systèmes de boutons deviennent un. Les 7 paginations deviennent une. Toute variante doit être justifiée par un besoin fonctionnel distinct, pas par une préférence locale.
**P4 — Le mouvement sert la compréhension.**
Une animation n'est acceptée que si elle explique une transition d'état ou une relation spatiale. 120–200 ms, `ease-out`, sur `opacity` / `transform` uniquement. Tout le reste est supprimé.
---
## 2. Vocabulaire visuel appliqué
### 2.1 Rôles de couleur — dérivés stricts de la charte
| Rôle | Token | Valeur | Application |
|------|-------|--------|-------------|
| Surface application | `bg-white` | `#FFFFFF` | Cartes, panneaux, tables |
| Surface page | `neutral-50` | `#F8F9FC` | Fond de zone de contenu |
| Surface d'autorité | `brand-navy` | `#10183A` | Sidebar admin, sections navy, en-têtes sombres |
| Bordure | `--border` | `#E2E8F0` | Bordures 1px, séparateurs |
| Texte principal | `brand-navy` | `#10183A` | Titres, valeurs KPI |
| Texte courant | `neutral-800` | `#1E2859` | Corps |
| Texte secondaire | `neutral-500` | `#5A6BB8` | Descriptions, métadonnées |
| Action primaire | `brand-turquoise` | `#34CCCD` | CTA, boutons primaires |
| Focus | `brand-turquoise` | `#34CCCD` | Anneau de focus (via `--ring`) |
| État actif (nav) | `brand-navy` + fond `neutral-100` | `#10183A` / `#EDEEF5` | Item de sidebar actif |
| Succès | `success` / `brand-green` | `#067224` | Accepté, validé, payé |
| Erreur | `--destructive` | `#EF4444` | Refusé, erreur, suppression |
| Avertissement | `amber-500` | `#F59E0B` | En attente, expiration proche |
> **Trois substitutions systématiques, et rien d'autre :**
> `blue-*` → `brand-turquoise` (action) ou `brand-navy` (structure) selon le rôle
> `gray-*` → `neutral-*` (échelle déjà définie dans la charte)
> `purple-*` / `emerald-*` / `orange-*` / `indigo-*` → rôle sémantique ci-dessus
### 2.2 Élévation — 3 niveaux, pas 6
L'audit relève 6 niveaux d'ombre utilisés sans règle. La refonte en retient trois :
| Niveau | Classe | Usage |
|--------|--------|-------|
| 0 | `border` seul | Cartes, panneaux, tables — **défaut** |
| 1 | `shadow-sm` | Éléments flottants ancrés : dropdowns, popovers, sidebar mobile |
| 2 | `shadow-lg` | Éléments détachés : modales, drawers, toasts |
`shadow-md`, `shadow-xl`, `shadow-2xl` et la classe fantôme `shadow-brand` sont retirés du produit.
**La carte par défaut passe de `shadow-sm` à `border` seul** — c'est le principal levier pour un rendu net et non « cartonné ».
### 2.3 Rayons — 3 valeurs
| Classe | Valeur | Usage |
|--------|--------|-------|
| `rounded-md` | 6px | Contrôles : boutons, champs, selects, badges carrés |
| `rounded-lg` | 8px | Conteneurs : cartes, panneaux, modales, dropdowns |
| `rounded-full` | — | Pilules, avatars, points d'état |
`rounded-xl`, `rounded-2xl`, `rounded-3xl` sont retirés du produit. (Conservés uniquement sur la landing, traitée en dernier lot — voir §13, L9.)
### 2.4 Typographie appliquée
L'échelle `text-h1…h6` / `text-body-*` / `text-label-*` de la charte est **enfin utilisée**, en remplacement de l'échelle Tailwind par défaut :
| Élément | Token de charte |
|---------|-----------------|
| Titre de page | `text-h3` (24px/600, Manrope) |
| Description de page | `text-body-sm` + `neutral-500` |
| Titre de section / carte | `text-h5` (18px/500) |
| Valeur KPI | `text-h2` (32px/600) + `brand-navy` |
| Corps | `text-body-sm` (14px) — densité produit |
| En-tête de table | `text-label` (12px/600, +0.05em, capitales) |
| Cellule de table | `text-body-sm` |
| Libellé de champ | `text-label-lg` (14px/600) |
| Aide / erreur de champ | `text-body-xs` (12px) |
> Le titre de page passe de `text-3xl` (30px) à `text-h3` (24px). C'est volontaire : combiné à la suppression du double titre (§3), l'écran gagne en hiérarchie **et** en espace utile.
### 2.5 Rythme d'espacement — échelle 4/8
Multiples retenus : **4, 8, 12, 16, 24, 32, 48**. (`gap-1` `gap-2` `gap-3` `gap-4` `gap-6` `gap-8` `gap-12`)
| Contexte | Valeur |
|----------|--------|
| Padding de carte | `p-5` (20px) desktop, `p-4` mobile |
| Padding de cellule de table | `px-4 py-3` (compact) / `px-4 py-4` (confortable) |
| Gap de grille | `gap-4` |
| Espacement entre sections | `space-y-6` |
| Padding de zone de contenu | `p-4 lg:p-6 2xl:p-8` |
---
## 3. App shell
### 3.1 Structure cible
```
┌──────────────────────────────────────────────────────────────┐
│ Sidebar (w-60 / w-16 replié) │ Topbar (h-14) │
│ ───────────────────────────── ├──────────────────────────────│
│ Logo [«] │ Breadcrumb ⌘K 🔔 🌐 👤 │
│ ├──────────────────────────────│
│ ── Pilotage │ │
│ ▸ Tableau de bord │ PageHeader │
│ ▸ Réservations │ Titre (h3) + description │
│ ▸ Documents │ [onglets] [actions] │
│ ▸ Suivi │ ────────────────────────── │
│ │ │
│ ── Ressources │ Contenu │
│ ▸ Wiki │ │
│ │ │
│ ── Organisation │ │
│ ▸ Paramètres │ │
│ · Organisation │ │
│ · Membres │ │
│ · Clés API │ │
│ ───────────────────────────── │ │
│ 👤 Utilisateur [⋮] │ │
└──────────────────────────────────────────────────────────────┘
```
### 3.2 Décisions
| Décision | Avant | Après | Motif |
|----------|-------|-------|-------|
| **Suppression du double titre** | Topbar affiche le nom de nav + page affiche son `<h1>` | Topbar affiche le **breadcrumb** ; la page seule porte le `<h1>` | Corrige A5 |
| **Sidebar repliable** | `w-64` fixe | `w-60` / `w-16` replié, persisté en `localStorage` | Rend 240px aux tables larges |
| **Groupes de navigation** | 8 entrées à plat | 3 groupes (Pilotage / Ressources / Organisation) avec libellés `text-label` | Lisibilité, prépare la croissance |
| **Paramètres regroupés** | 3 entrées de nav dispersées | 1 entrée + sous-navigation | Corrige la dispersion relevée en B2 |
| **Un seul avatar** | Sidebar + topbar | Pied de sidebar uniquement, avec menu (Profil / Déconnexion) | Corrige la redondance |
| **Logout dans le menu** | Bouton rouge pleine largeur permanent | Entrée du menu utilisateur | Poids visuel proportionné |
| **Command menu ⌘K** | Absent | Recherche globale : navigation, réservations, ports, actions | Standard SaaS attendu |
| **Bottom-nav mobile** | 5 entrées sur 8 | 4 entrées + « Plus » ouvrant le drawer complet | Aucune section inaccessible |
| **Shell mutualisé** | 2 layouts dupliqués à 80 % | `AppShell` paramétré (`variant: 'app' | 'admin'`) | Corrige D2 |
| **Icônes** | SVG inline manuscrits | `Menu`, `X`, `PanelLeftClose` de lucide | Cohérence |
**Variante admin :** même `AppShell`, `variant="admin"` → sidebar `bg-brand-navy text-white`, logo blanc, séparateurs `border-white/10`. Le contenu des pages admin adopte le même vocabulaire que le dashboard (correction de la rupture chrome/contenu relevée en B3).
### 3.3 États de l'item de navigation
| État | Style |
|------|-------|
| Défaut | `text-neutral-700 hover:bg-neutral-100 hover:text-brand-navy` |
| Actif | `bg-neutral-100 text-brand-navy font-semibold` + barre `w-0.5 bg-brand-turquoise` à gauche |
| Verrouillé (plan) | `text-neutral-400` + icône `Lock` + `Tooltip` expliquant le plan requis, lien `/pricing` |
| Replié | icône seule centrée + `Tooltip` au survol |
> L'état actif abandonne le fond bleu au profit d'un fond neutre + **accent turquoise en filet**. Plus discret, plus précis, strictement dans la charte.
---
## 4. Composants de navigation
| Composant | Rôle | Notes d'implémentation |
|-----------|------|------------------------|
| `AppShell` | Enveloppe sidebar + topbar + contenu + bottom-nav | Remplace les 2 layouts |
| `Sidebar` | Navigation groupée, repliable, drawer sous `lg` | Focus trap en mode drawer |
| `Topbar` | Breadcrumb + ⌘K + notifications + langue + avatar | `h-14`, `sticky`, `border-b` |
| `Breadcrumb` | Fil d'Ariane dérivé de la route + libellés i18n | Tronque au milieu sur mobile |
| `PageHeader` | Titre `h3`, description, onglets, actions | **Extension** de l'existant (déjà sur 9 pages) |
| `Tabs` | Sous-navigation de page (paramètres, détail réservation) | Sur `@radix-ui/react-tabs` (installé) |
| `CommandMenu` | Palette ⌘K | Sur `ui/command.tsx` complété |
| `BottomNav` | Navigation mobile 4 + « Plus » | — |
---
## 5. Composants de contenu
### 5.1 `DataTable` — pièce maîtresse
Fondé sur `@tanstack/react-table` v8 (installé, utilisé une seule fois) et `@tanstack/react-virtual` (installé, jamais utilisé).
| Capacité | Détail |
|----------|--------|
| Colonnes | Définition déclarative, tri, redimensionnement, visibilité |
| Densité | `compact` (`py-3`) par défaut / `comfortable` (`py-4`) |
| Sélection | Cases à cocher + barre d'actions groupées flottante |
| En-tête | `sticky top-0`, `text-label`, fond `neutral-50` |
| Ligne | `hover:bg-neutral-50`, sélectionnée `bg-neutral-100` |
| Colonne d'actions | `sticky right-0`, menu sur `@radix-ui/react-dropdown-menu` — **remplace le positionnement manuel `menuPos`** relevé en B2 |
| **Responsive** | ≥ `lg` : table · `md` : colonnes secondaires masquées · < `md` : **liste de `DataCard`** (titre, 3 champs clés, menu d'actions) |
| Virtualisation | Activable, pour `/admin/logs` et les résultats de recherche |
| États | `loading` → `TableSkeleton` · `empty` → `EmptyState` · `error` → `ErrorState` |
| Pagination | `Pagination` intégré, taille de page persistée |
Ce composant absorbe **26 tables brutes, 7 paginations et 21 `overflow-x-auto`**.
### 5.2 Autres
| Composant | Rôle |
|-----------|------|
| `Card` | Rectifié : `border` seul, `p-5`, `CardTitle` en `text-h5` |
| `StatCard` | KPI : libellé `text-label`, valeur `text-h2`, delta, icône discrète. **Aligné à gauche.** |
| `DataCard` | Représentation mobile d'une ligne de table |
| `Badge` | Variants sémantiques : `neutral` `info` `success` `warning` `danger` — tokens de charte uniquement |
| `StatusBadge` | Conservé (badges d'abonnement Silver/Gold/Platinium) |
| `Callout` | Encadré éditorial : `info` `warning` `success` `danger` — absorbe les ~30 encadrés du wiki |
| `Pagination` | Précédent/Suivant + pages + sélecteur de taille + compteur |
| `DescriptionList` | Paires libellé/valeur des pages de détail |
| `Timeline` | Suivi d'expédition (`/track-trace`), étapes de réservation |
---
## 6. Composants de saisie
| Composant | Base | Correction apportée |
|-----------|------|---------------------|
| `Button` | `cva` + `cn()` + `@radix-ui/react-slot` | `asChild` (corrige le `<a><button>` invalide) · état `loading` · `active:` · **`hover:bg-accent` turquoise plein supprimé** sur `ghost`/`outline` |
| `Input` | Existant corrigé | Focus turquoise · états `error` · préfixe/suffixe · tailles `sm`/`md` |
| `Textarea` | Nouveau | Aligné sur `Input` |
| `Select` | **`@radix-ui/react-select`** | Réécriture — l'actuel n'affiche jamais sa valeur |
| `Combobox` | `Command` + `Popover` Radix | Remplace les autocomplétions manuelles (ports, compagnies) |
| `Checkbox` / `Radio` | Nouveaux | Aujourd'hui inputs bruts |
| `Switch` | Existant | Conservé, aligné sur les tokens |
| `DatePicker` | `react-day-picker` + `date-fns` (installé) | Dates de départ/arrivée |
| `FormField` | Nouveau | `Label` + contrôle + aide + erreur + `aria-describedby`/`aria-invalid`. **Absorbe 175 inputs bruts.** |
| `Form` | `react-hook-form` + `zod` (installés) | Validation unifiée ; remplace les 25 `useState` de `/register` |
| `FileDropzone` | Nouveau | Glisser-déposer + progression + validation. **Absorbe 4 implémentations.** |
| `SearchInput` | Nouveau | Champ de recherche debounced avec effacement |
| `FilterBar` | Nouveau | Filtres + jetons actifs + réinitialisation, état synchronisé à l'URL |
### Variants de `Button` (définitifs)
| Variant | Style | Usage |
|---------|-------|-------|
| `primary` | `bg-brand-turquoise text-white hover:bg-brand-turquoise/90` | Action principale — **une seule par écran** |
| `secondary` | `bg-brand-navy text-white hover:bg-brand-navy/90` | Action structurante |
| `outline` | `border border-[--border] text-brand-navy hover:bg-neutral-50` | Action secondaire |
| `ghost` | `text-neutral-700 hover:bg-neutral-100` | Actions de table, icônes |
| `destructive` | `bg-[--destructive] text-white hover:bg-[--destructive]/90` | Suppression |
| `link` | `text-brand-turquoise hover:underline underline-offset-4` | Liens en ligne |
Tailles : `sm` (32px) · `md` (36px, défaut) · `lg` (40px) · `icon` (36×36).
> Les hauteurs passent de 40/36/44 à 36/32/40 : cohérent avec P2 (densité) et avec la topbar `h-14`.
---
## 7. Composants de surface
| Composant | Base | Notes |
|-----------|------|-------|
| `Dialog` | **`@radix-ui/react-dialog`** | Portal, focus trap, `Escape`, scroll-lock, `aria-modal`. Tailles `sm`/`md`/`lg`/`full`. Absorbe les modales inline de `/documents` et `/admin/blog` |
| `ConfirmDialog` | Sur `Dialog` | **Remplace les 14 `confirm()` natifs.** Variante `destructive` avec libellé d'action explicite |
| `Drawer` | Sur `Dialog` Radix | Panneau latéral : détail de réservation, filtres mobiles, aperçu de document |
| `Popover` | **`@radix-ui/react-popover`** | Portalisé — corrige le clipping |
| `DropdownMenu` | **`@radix-ui/react-dropdown-menu`** | Menus d'actions de table, menu utilisateur |
| `Tooltip` | `@radix-ui/react-tooltip` | Items de sidebar repliée, icônes, valeurs tronquées |
| `Toast` / `Toaster` | Nouveau, monté dans `Providers` | **Remplace les 44 `alert()`.** Variants `success` `error` `warning` `info`. Bas-droite desktop, haut mobile. Auto-dismiss 4 s, action optionnelle, empilement max 3 |
| `Sheet` | Sur `Dialog` | Drawer mobile de la sidebar |
---
## 8. Composants d'état
Quatre composants qui absorbent ~126 réimplémentations.
| Composant | Remplace | Contenu |
|-----------|----------|---------|
| `Skeleton` + `TableSkeleton` / `CardSkeleton` / `FormSkeleton` | ~30 `animate-pulse` inline | `bg-neutral-100 animate-pulse rounded-md`, forme calquée sur le contenu réel |
| `EmptyState` | 27 blocs vides | Icône (cercle `neutral-100`), titre `text-h5`, description `text-body-sm`, action. Variantes : *aucune donnée* / *aucun résultat de filtrage* / *accès verrouillé* |
| `ErrorState` | 29 bandeaux inline | Icône, message, bouton « Réessayer », détail repliable |
| `Spinner` | 40 fichiers `animate-spin` | Tailles `sm`/`md`/`lg`, couleur `brand-turquoise` |
**Règle de complétude :** toute vue asynchrone doit traiter les **4 états** — chargement, vide, erreur, contenu. C'est un critère de revue par page.
---
## 9. Architecture de fichiers
```
apps/frontend/src/
├── components/
│ ├── ui/ # Primitives — aucune logique métier
│ │ ├── button.tsx input.tsx textarea.tsx select.tsx combobox.tsx
│ │ ├── checkbox.tsx radio.tsx switch.tsx date-picker.tsx
│ │ ├── card.tsx badge.tsx callout.tsx separator.tsx avatar.tsx
│ │ ├── dialog.tsx drawer.tsx popover.tsx dropdown-menu.tsx tooltip.tsx
│ │ ├── toast.tsx toaster.tsx
│ │ ├── table.tsx pagination.tsx tabs.tsx command.tsx
│ │ ├── skeleton.tsx spinner.tsx empty-state.tsx error-state.tsx
│ │ └── index.ts # export unique
│ ├── shell/ # Chrome applicatif
│ │ ├── app-shell.tsx sidebar.tsx topbar.tsx
│ │ ├── breadcrumb.tsx bottom-nav.tsx command-menu.tsx
│ │ └── user-menu.tsx
│ ├── data/ # Affichage de données
│ │ ├── data-table.tsx data-card.tsx stat-card.tsx
│ │ ├── description-list.tsx timeline.tsx
│ │ └── filter-bar.tsx search-input.tsx
│ ├── forms/
│ │ ├── form.tsx form-field.tsx file-dropzone.tsx
│ │ └── port-autocomplete.tsx
│ └── [feature]/ # bookings/ documents/ rate-search/ admin/ …
├── hooks/
│ ├── use-toast.ts use-media-query.ts use-sidebar.ts
│ └── use-url-state.ts # synchronisation filtres ↔ URL
└── lib/
└── utils.ts # cn() — déjà présent
```
**Conventions :**
- `ui/` ne connaît ni l'API, ni l'i18n, ni le routage. Composants purs et testables.
- Tout composant utilise `cn()` pour fusionner les classes (aujourd'hui : concaténation de strings).
- Aucun texte en dur : tout passe par `next-intl`. **`messages/fr.json` et `messages/en.json` (4 600 lignes chacun) restent synchronisés clé pour clé.**
- Les pages orchestrent, les composants affichent. Objectif : aucune page > 300 lignes.
---
## 10. Responsive
### 10.1 Paliers et intentions
| Palier | Largeur | Layout |
|--------|---------|--------|
| **Mobile** | < 640px | 1 colonne · bottom-nav · sidebar en drawer · tables → `DataCard` · filtres en `Drawer` · actions en `DropdownMenu` |
| **Tablette** | 640–1023px | 2 colonnes · sidebar en drawer · tables réduites aux colonnes essentielles · filtres en ligne repliables |
| **Desktop** | 1024–1535px | Sidebar fixe · tables complètes · 3–4 colonnes de KPI · panneaux latéraux |
| **Grand écran** | ≥ 1536px | Contenu jusqu'à **1600px** · 4–6 colonnes de KPI · densité augmentée · `p-8` |
### 10.2 Corrections ciblées
- **A8 — grand écran.** `max-w-7xl` (1280px) devient `max-w-[1600px]` sur les vues de données (listes, tables, dashboard). Les vues de lecture (wiki, blog, légal) restent contraintes à `max-w-3xl` pour la longueur de ligne. Introduction effective du palier `2xl:` (aujourd'hui **0 occurrence**).
- **A9 — tablette.** Le palier `md` reçoit un traitement propre : grilles KPI en 2 colonnes, tables à colonnes réduites, `PageHeader` en pile. Cible : `md:` passe de 120 à un niveau comparable à `sm:`/`lg:`.
- **A10 — tables.** Bascule `DataTable` → `DataCard` sous `md`, native au composant. Aucun scroll horizontal subi.
- **Cibles tactiles.** Minimum 44×44px sur mobile pour toute action (bottom-nav, menus, boutons de table).
---
## 11. Micro-interactions
Technologies déjà présentes uniquement : **`tailwindcss-animate`** (installé, quasi inutilisé) pour l'UI, **`framer-motion`** (installé) réservé à la landing.
| Interaction | Effet | Durée |
|-------------|-------|-------|
| Survol de bouton | `background-color` | 120 ms `ease-out` |
| Pression de bouton | `scale(0.98)` | 80 ms |
| Survol de ligne de table | `background-color` | 100 ms |
| Ouverture de modale | `fade-in` + `zoom-in-95` | 180 ms `ease-out` |
| Ouverture de dropdown/popover | `fade-in` + `slide-in-from-top-1` | 140 ms |
| Toast | `slide-in-from-bottom` / `fade-out` | 200 ms |
| Drawer / sidebar mobile | `translate-x` | 220 ms `ease-out` |
| Onglets | Indicateur glissant | 160 ms |
| Sidebar repliée | `width` | 200 ms `ease-out` |
| Skeleton | `animate-pulse` | — |
**Règles :** `opacity` et `transform` uniquement (pas de `height`/`width` animées hors sidebar) · jamais de délai avant une réponse à un clic · pas d'animation d'entrée sur le contenu de page (perçu comme lent) · **`prefers-reduced-motion` respecté globalement** :
```css
@media (prefers-reduced-motion: reduce) {
*, *::before, *::after {
animation-duration: 0.01ms !important;
transition-duration: 0.01ms !important;
}
}
```
---
## 12. Accessibilité
Cible : **WCAG 2.1 AA**.
| Point | Action |
|-------|--------|
| Focus | `focus-visible` partout (aujourd'hui 454 `focus:` contre 13 `focus-visible:`), anneau `ring-2 ring-brand-turquoise ring-offset-2` |
| `--ring` | Corrigé vers le turquoise de marque — **restauration de charte**, pas modification |
| Clavier | Navigation complète : tables, menus, modales, combobox, tunnel de recherche |
| Focus trap | Modales, drawers, sidebar mobile (via Radix) |
| Libellés | Tout contrôle a un `<label>` associé ou un `aria-label` ; tout bouton-icône a un `aria-label` (29 aujourd'hui pour 299 boutons) |
| Régions | `<nav>`, `<main>`, `<aside>`, `<header>` + lien d'évitement |
| Annonces | `aria-live` sur les toasts et les résultats de recherche |
| Contraste | Vérifier `neutral-400`/`neutral-500` sur blanc ; `brand-turquoise #34CCCD` **ne passe pas AA en texte sur blanc** → réservé aux fonds, bordures et grandes tailles, jamais au texte courant sur blanc |
| Mouvement | `prefers-reduced-motion` |
> Le point contraste est important : il ne change pas la charte, il **encadre l'usage** du turquoise. Texte turquoise sur blanc → remplacé par navy, le turquoise restant sur les surfaces d'action (texte blanc sur turquoise = conforme).
---
## 13. Plan de refonte par lots
11 lots, séquencés par dépendance. Chaque lot est autonome, compilable et validable.
| Lot | Contenu | Fichiers | Effort |
|-----|---------|----------|--------|
| **L0 — Fondations** | Corriger `--ring` · `cn()` partout · réécrire `Button`/`Input`/`Select`/`Dialog`/`Popover` sur Radix · créer `Toast`, `Skeleton`, `EmptyState`, `ErrorState`, `Spinner`, `Pagination`, `Breadcrumb`, `Callout`, `FormField`, `Tooltip`, `DropdownMenu`, `Checkbox`, `Radio`, `Textarea` · monter `Toaster` dans `Providers` · `prefers-reduced-motion` | ~25 nouveaux, 8 réécrits | 2–3 j |
| **L1 — App shell** | `AppShell` mutualisé · sidebar repliable groupée · topbar + breadcrumb · suppression du double titre · `CommandMenu` ⌘K · bottom-nav complète · `UserMenu` · **restauration charte sur le chrome** | 2 layouts → ~8 composants | 2 j |
| **L2 — Données** | `DataTable` + `DataCard` + `FilterBar` + `SearchInput` + `StatCard` + `useUrlState` · virtualisation | ~8 composants | 2–3 j |
| **L3 — Dashboard & réservations** | `/dashboard` (hiérarchie KPI, charts aux couleurs de charte, suppression du `#8884d8`) · `/bookings` · `/bookings/[id]` · **arbitrage D1** | 4 pages | 3 j |
| **L4 — Documents & recherche** | `/documents` (démonolithisation 1169 l., `FileDropzone`, modales Radix, 9 `alert()` → toasts) · `/search` · `/search-advanced` (état en URL, `Combobox`) · `/results` · `/track-trace` | 5 pages | 3–4 j |
| **L5 — Paramètres & profil** | `SettingsLayout` à onglets · `/settings/organization` · `/settings/users` · `/settings/api-keys` · `/profile` · `/notifications` | 6 pages | 2 j |
| **L6 — Admin** | Alignement chrome/contenu · `DataTable` sur les 5 listes · démonolithisation `/admin/blog` (1341 l.) · 11+9 `alert()` → toasts · virtualisation `/admin/logs` | 8 pages | 3–4 j |
| **L7 — Wiki & docs** | `Callout` (~30) · `.prose` + `@tailwindcss/typography` · sommaire + ancres · largeur de lecture · breadcrumb · nav précédent/suivant · **93 `blue-*` corrigés** | 13 + 2 pages | 2 j |
| **L8 — Auth & portails** | `AuthLayout` mutualisé · `/register` sur `react-hook-form`+`zod` (25 `useState`) · fusion accept/reject · états d'expiration de lien · robustesse mot de passe | 11 pages | 2 j |
| **L9 — Marketing** | Extraction de `/` (1082 l.) en sections · alignement rayons/ombres · suppression des nuances navy inventées (`#1A2550`, `#1A2A5E`) · `LegalPageLayout` | 11 pages | 2 j |
| **L10 — Nettoyage** | Supprimer `/test-image`, `/demo-carte`, `src/legacy-pages/` · trancher `src/app/rates/csv-search` · `gray-*` → `neutral-*` · retirer `.btn-*`/`.input`/`.card` de `globals.css` · trancher le dark mode · mettre à jour `DESIGN_SYSTEM.md` | — | 1 j |
**Total estimé : 24–29 jours de travail.**
**Chemin critique :** L0 → L1 → L2 conditionnent tout le reste. L3 à L9 sont ensuite parallélisables.
---
## 14. Protocole de validation
### Après chaque lot
```bash
cd "apps/frontend"
# 1. La charte n'a pas bougé
git diff tailwind.config.ts # doit être VIDE
git diff app/globals.css # seul --ring modifié (L0), validé
# 2. Les dérives reculent
grep -rE "\b(bg|text|border|ring|from|to)-blue-" app src --include="*.tsx" | wc -l # jamais croissant
grep -rE "[^.](alert|confirm)\(" app src --include="*.tsx" | wc -l # jamais croissant
# 3. Qualité
npm run type-check
npm run lint
npm run build
npm test
# 4. i18n synchronisé
python3 -c "import json;a=json.load(open('messages/fr.json'));b=json.load(open('messages/en.json'));\
def k(d,p=''):\
s=set()\
for x,v in d.items():\
s|=k(v,p+x+'.') if isinstance(v,dict) else {p+x}\
return s
print('écart:', k(a)^k(b))"
```
### Indicateurs de progression
Mesures prises avec `scripts` de comptage homogènes (occurrences via `grep -o`, jamais des lignes).
> ⚠️ **Correction de métrique (lot L5).** Deux lignes de ce tableau étaient fausses dans les versions précédentes du document :
> - **Dialogues natifs** : le motif de comptage excluait `window.confirm(` (précédé d'un point). Le vrai point de départ est **62**, pas 58.
> - **Fichiers > 600 lignes** : la valeur de départ « 12 » était une estimation non mesurée. Le comptage réel sur `HEAD` donne **17**.
>
> Les colonnes ci-dessous sont recalculées avec la métrique corrigée. Les autres lignes étaient exactes.
| Indicateur | Départ | L0 | L1 | L2 | L3 | L4 | L5 | L6 | L7 | Cible |
|------------|--------|----|----|----|----|----|----|----|----|-------|
| `blue-*` | 816 | 729 | 722 | 722 | 691 | 552 | 438 | 312 | **177** | **0** |
| `gray-*` | 2 485 | 2 355 | 2 331 | 2 331 | 2 108 | 1 833 | 1 523 | 1 088 | **759** | **0** (→ `neutral-*`) |
| Dialogues natifs | 62 | — | — | — | — | 48 | 44 | **7** | 7 | **0** |
| `<button>` bruts | 299 | 291 | 287 | 290 | 278 | 262 | 262 | 262 | 262 | < 30 |
| `<input>` bruts | 175 | 146 | 147 | 147 | 146 | 143 | 143 | 143 | 143 | < 20 |
| `<table>` bruts | 26 | 26 | 26 | 27 | 26 | 25 | 25 | 25 | 25 | **0** |
| Fichiers > 600 lignes | 17 | — | — | — | — | — | 12 | 12 | 12 | **0** |
| `2xl:` | 0 | 0 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | > 40 |
| `focus-visible:` | 13 | 41 | 62 | 71 | 71 | 82 | 93 | 93 | **97** | > 100 |
| Paquets Radix importés | 0 | 7 | 7 | 7 | 7 | 7 | **8** | 8 | 8 | ≥ 8 |
| Tests frontend | 123 | 123 | **135** | **144** | 144 | 144 | 144 | 144 | 144 | croissant |
**Après L8 :** `blue-*` **148** · `gray-*` **610** · dialogues natifs **6**.
**Après L9 : `blue-*` = 0 · `gray-*` = 0.**
## 🎯 Objectif atteint — la charte est restaurée dans toute l'application
| Indicateur | Départ | Aujourd'hui |
|---|---|---|
| `blue-*` (accent hors charte) | **816** | **0** |
| `gray-*` (échelle neutre concurrente) | **2 485** | **0** |
| Nuances de marque inventées (`#1a2550`, `#0e9999`…) | 9 | **0** |
| Dialogues natifs | 62 | **6** (dont 4 en code mort ou faux positifs) |
Vérification reproductible :
```bash
cd apps/frontend
grep -rhoE '\b[a-z:-]*-(blue|gray)-[0-9]+' app src --include='*.tsx' | wc -l # 0
git diff --quiet tailwind.config.ts && echo "charte intacte" # charte intacte
```
---
## 19. État à l'issue du lot L10
| Indicateur | Départ | Fin | Cible |
|---|---|---|---|
| `blue-*` | 816 | **0** ✅ | 0 |
| `gray-*` | 2 485 | **0** ✅ | 0 |
| Nuances de marque inventées | 9 | **0** ✅ | 0 |
| Dialogues natifs | 62 | **2** | 0 |
| Paquets Radix importés | 0 | **8** ✅ | ≥ 8 |
| Tests frontend | 123 | **144** ✅ | croissant |
| `focus-visible:` | 13 | 97 | > 100 |
| `<button>` bruts | 299 | 255 | < 30 |
| `<input>` bruts | 175 | 143 | < 20 |
| `<table>` bruts | 26 | 24 | 0 |
| `2xl:` | 0 | 1 | > 40 |
| Fichiers > 600 lignes | 17 | 12 | 0 |
Les 2 dialogues natifs restants sont des **faux positifs** : une fonction locale nommée `confirm()` dans `dashboard/booking/[id]/payment-success/page.tsx`, sans rapport avec le dialogue du navigateur.
### Code mort supprimé
| Cible | Motif |
|---|---|
| `src/legacy-pages/` (3 fichiers) | Aucune référence dans le projet |
| `src/app/` (`rates/csv-search`) | **Jamais construit** : `app/` existe à la racine, donc Next.js ignore `src/app/`. Absent du manifeste de build. |
| `dashboard/search` + `dashboard/bookings/new` | Îlot orphelin fermé (L10a) |
| `test-image`, `demo-carte` | Pages de test |
### Corrections d'accessibilité appliquées à `globals.css`
- `.btn-primary` : texte blanc → **navy** sur turquoise (2,05:1 → 8,49:1)
- `.badge-info` : libellé turquoise → **navy** sur fond turquoise clair
- `.link` et `Button variant="link"` : **soulignement permanent** (un lien identifié par la seule couleur échoue au critère WCAG 1.4.1) et passage au navy
Les classes `@layer components` **ne sont pas supprimées** : `.btn-primary` sert dans 8 fichiers, `.label` et `.link` dans `/register` et `/admin/login`. Leur retrait est conditionné au passage de ces pages aux primitives.
---
## 20. Travaux restants
Ce qui n'a pas été fait, explicitement.
### Démonolithisation
| Fichier | Lignes |
|---|---|
| `src/components/docs/DocsPageContent.tsx` | 1 506 |
| `app/[locale]/admin/blog/page.tsx` | 1 341 |
| `app/[locale]/page.tsx` | 1 082 |
| `app/[locale]/dashboard/search-advanced/page.tsx` | 1 067 |
| `app/[locale]/dashboard/settings/users/page.tsx` | 884 |
| `app/[locale]/register/page.tsx` · `admin/organizations` | 832 |
| + 5 autres | 621–736 |
### Adoption des primitives
255 `<button>`, 143 `<input>` et 24 `<table>` restent écrits à la main. Leur mise en forme est conforme à la charte, mais ils ne bénéficient ni des états, ni de l'accessibilité, ni de la cohérence des primitives.
### Responsive grand écran
`2xl:` n'apparaît qu'une fois (dans `AppShell`). Les pages n'exploitent pas la largeur au-delà de 1 280 px.
### Autres
- `Callout` non substitué aux ~30 encadrés du wiki
- Sommaire, ancres et navigation précédent/suivant du wiki
- Chaînes françaises codées en dur dans `admin/blog` et `RichTextEditor` (hors i18n)
- Usage du turquoise en texte à auditer : 176 occurrences de `text-brand-turquoise`, dont une partie sur fond clair
- **Défaut `/booking` du middleware** (§18) — en attente d'arbitrage
---
## 18. Défaut fonctionnel relevé — liens magiques de réservation
Découvert en vérifiant le rendu des portails token au lot L8. **Sans rapport avec la refonte** : `middleware.ts` n'a été modifié à aucun moment.
`apps/frontend/middleware.ts` liste `/carrier` dans `prefixPublicPaths`, mais **pas `/booking`**. Or `/booking/confirm/[token]` et `/booking/reject/[token]` sont des pages atteintes par lien magique, envoyées par e-mail à des clients qui ne sont pas connectés.
**Conséquence :** ces deux liens redirigent vers `/login?redirect=…`. Le destinataire ne peut ni confirmer ni refuser sa réservation.
**Vérification :**
```bash
curl -s -o /dev/null -w "%{http_code}" http://localhost:3001/fr/carrier/accept/xxx # 200
curl -s -o /dev/null -w "%{http_code}" http://localhost:3001/fr/booking/confirm/xxx # 307
```
**Correction :** ajouter `'/booking'` à `prefixPublicPaths`. Une ligne.
**Non appliquée** : modifier une frontière d'authentification dépasse le périmètre d'une refonte UI et relève d'une décision produit. À arbitrer.
**Les 7 dialogues natifs restants** (contre 62 au départ) :
| Emplacement | Nombre | Traitement |
|---|---|---|
| `src/legacy-pages/CarrierManagement.tsx` | 3 | Code mort — supprimé en L10 |
| `src/app/rates/csv-search/page.tsx` | 1 | Route orpheline — arbitrage L10 |
| `carrier/documents/[token]/page.tsx` | 1 | L8 |
| `dashboard/booking/[id]/payment-success/page.tsx` | 2 | **Faux positifs** — une fonction locale nommée `confirm()`, sans rapport avec le dialogue natif |
Soit **2 appels natifs réels** hors code mort et route orpheline.
**Fichiers restant au-dessus de 600 lignes après L5** (12) — cible des lots suivants :
| Fichier | Lignes | Lot |
|---|---|---|
| `src/components/docs/DocsPageContent.tsx` | 1 506 | L7 |
| `app/[locale]/admin/blog/page.tsx` | 1 341 | L6 |
| `app/[locale]/page.tsx` | 1 082 | L9 |
| `app/[locale]/dashboard/search-advanced/page.tsx` | 1 067 | reporté |
| `app/[locale]/dashboard/settings/users/page.tsx` | 884 | reporté |
| `app/[locale]/register/page.tsx` | 832 | L8 |
| `app/[locale]/admin/organizations/page.tsx` | 832 | L6 |
| `app/[locale]/dashboard/booking/new/page.tsx` | 736 | reporté |
| `app/[locale]/admin/bookings/page.tsx` | 735 | L6 |
| `app/[locale]/admin/documents/page.tsx` | 684 | L6 |
| `app/[locale]/contact/page.tsx` | 681 | L9 |
| `app/[locale]/dashboard/booking/[id]/pay/page.tsx` | 621 | reporté |
**Réduction de volume :**
| Fichier | Avant | Après | Lot |
|---|---|---|---|
| `dashboard/documents/page.tsx` | 1 169 l. | **549 l.** | L4 |
| `dashboard/bookings/page.tsx` | 908 l. | **501 l.** | L3 |
| `dashboard/page.tsx` | 454 l. | **372 l.** | L3 |
| `dashboard/bookings/[id]/page.tsx` | 284 l. | **239 l.** | L3 |
---
## 17. Correction de contraste — texte sur turquoise
Constat fait en cours de lot L4, vérifié par calcul de luminance relative WCAG :
| Couple | Contraste | Verdict |
|---|---|---|
| Blanc sur `brand-turquoise #34CCCD` | **2,05:1** | ❌ Échoue AA (4,5:1 requis) |
| `brand-navy #10183A` sur `brand-turquoise` | **8,49:1** | ✅ Passe AAA |
Le token `accent.foreground` de `tailwind.config.ts` vaut `#FFFFFF`. **Il n'est pas modifié** — la charte reste verrouillée. Ce qui change est le couple appliqué dans les composants : les aplats turquoise portent désormais du texte **navy**.
Ce n'est pas une invention : la sidebar d'administration d'origine employait déjà `bg-brand-turquoise text-brand-navy`. La refonte généralise une pratique déjà présente dans le projet, et la rend conforme.
> Les légères hausses de `<button>` (+3) et `<table>` (+1) après L2 sont attendues : ce sont les **composants mutualisés eux-mêmes** (`DataTable`, `FilterBar`, `SearchInput`) qui contiennent désormais ces éléments — une fois pour toutes, à la place des 26 tables et des 290 boutons dispersés qu'ils vont absorber à partir de L3.
> Le recul entre « Départ » et « Après L0 » provient de la suppression du code mort validé (L10a). L0 construit les fondations sans toucher aux pages.
> Après L1, **le chrome applicatif ne contient plus aucun bleu générique** : les 722 occurrences restantes sont toutes dans le corps des pages, traitées par les lots L3 à L9.
### Revue visuelle par lot
Captures avant/après aux 4 paliers (375 / 768 / 1440 / 1920 px), sur `chromium`, `firefox`, `webkit` via la configuration Playwright existante.
---
## 15. Ce que je ne ferai pas
Engagements explicites, opposables en revue :
1. ❌ Modifier `brand-navy`, `brand-turquoise`, `brand-green`, `brand-gray`.
2. ❌ Modifier `primary.*`, `accent.*`, `success.*`, l'échelle `neutral-*`.
3. ❌ Changer Manrope / Montserrat ou leurs affectations.
4. ❌ Modifier l'échelle `fontSize` de `tailwind.config.ts`.
5. ❌ Modifier `--radius: 0.5rem`.
6. ❌ Remplacer `lucide-react` ou toucher aux logos et assets de marque.
7. ❌ Introduire une couleur d'accent nouvelle, un dégradé décoratif, une teinte inventée.
8. ❌ Glassmorphism, ombres portées lourdes, rayons > 8px dans le produit, cartes empilées gratuitement.
9. ❌ Animer pour animer.
10. ❌ Ajouter une dépendance non justifiée. Nouvelles dépendances envisagées, **toutes déjà partiellement présentes ou triviales** : `@radix-ui/react-popover`, `@radix-ui/react-tooltip`, `@radix-ui/react-checkbox`, `react-day-picker`. Chacune sera soumise à validation avant installation.
11. ❌ Modifier le backend, les contrats d'API ou la logique métier. **La refonte est strictement frontend.**
12. ❌ Supprimer une route ou du code mort sans arbitrage explicite (voir §F de `ui-audit.md`).
---
## 16. Arbitrages requis avant démarrage
| # | Question | Bloque |
|---|----------|--------|
| 1 | **Validez-vous le remplacement `blue-*` → navy/turquoise sur les 64 fichiers ?** C'est le changement le plus visible de la refonte. Sans lui, l'application conserve deux identités. | L1 et tous les suivants |
| 2 | Lequel des deux tunnels de réservation est actif : `/dashboard/bookings/new` ou `/dashboard/booking/new` ? | L3 |
| 3 | `src/app/rates/csv-search` (hors i18n, hors middleware) : migrer ou supprimer ? | L10 |
| 4 | `src/legacy-pages/` : code mort confirmé ? | L10 |
| 5 | Dark mode : l'implémenter réellement, ou retirer le scaffolding mort ? | L0 |
| 6 | Wiki : le contenu reste-t-il en JSX, ou passe-t-il en MDX ? | L7 |
| 7 | Les 4 dépendances du §15.10 sont-elles autorisées ? | L0 |