40 KiB
Nouvelle architecture UI & plan de refonte — Xpeditis
Date : 2026-08-12 · Branche : ui_update
Prérequis de lecture : design-system-audit.md (charte verrouillée) · 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
- 2. Vocabulaire visuel appliqué
- 3. App shell
- 4. Composants de navigation
- 5. Composants de contenu
- 6. Composants de saisie
- 7. Composants de surface
- 8. Composants d'état
- 9. Architecture de fichiers
- 10. Responsive
- 11. Micro-interactions
- 12. Accessibilité
- 13. Plan de refonte par lots
- 14. Protocole de validation
- 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) oubrand-navy(structure) selon le rôlegray-*→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'`) |
| 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.jsonetmessages/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) devientmax-w-[1600px]sur les vues de données (listes, tables, dashboard). Les vues de lecture (wiki, blog, légal) restent contraintes àmax-w-3xlpour la longueur de ligne. Introduction effective du palier2xl:(aujourd'hui 0 occurrence). - A9 — tablette. Le palier
mdreçoit un traitement propre : grilles KPI en 2 colonnes, tables à colonnes réduites,PageHeaderen pile. Cible :md:passe de 120 à un niveau comparable àsm:/lg:. - A10 — tables. Bascule
DataTable→DataCardsousmd, 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 :
@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
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
HEADdonne 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 :
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.linketButton 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
Calloutnon 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/blogetRichTextEditor(hors i18n) - Usage du turquoise en texte à auditer : 176 occurrences de
text-brand-turquoise, dont une partie sur fond clair - Défaut
/bookingdu 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 :
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 :
- ❌ Modifier
brand-navy,brand-turquoise,brand-green,brand-gray. - ❌ Modifier
primary.*,accent.*,success.*, l'échelleneutral-*. - ❌ Changer Manrope / Montserrat ou leurs affectations.
- ❌ Modifier l'échelle
fontSizedetailwind.config.ts. - ❌ Modifier
--radius: 0.5rem. - ❌ Remplacer
lucide-reactou toucher aux logos et assets de marque. - ❌ Introduire une couleur d'accent nouvelle, un dégradé décoratif, une teinte inventée.
- ❌ Glassmorphism, ombres portées lourdes, rayons > 8px dans le produit, cartes empilées gratuitement.
- ❌ Animer pour animer.
- ❌ 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. - ❌ Modifier le backend, les contrats d'API ou la logique métier. La refonte est strictement frontend.
- ❌ 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 |