Plan UI premium 6–8 semaines (design system, shell, Storybook, a11y): - Design system: DESIGN_TOKENS.md, APP_SHELL.md, FULL_LAYOUT_PAGE.md. Single source for layout/shell (index.css), shadows (design-system.css), durations/easing. - Tokens: shadow-cover-depth, shadow-gold-glow, shadow-fab-glow; layout max-height (max-h-layout-drawer, max-h-layout-panel, max-h-layout-list). All duration-200/300/500 replaced by --duration-fast/normal/slow. Arbitrary shadows replaced by token classes. - Shell & player: Sidebar, Header, GlobalPlayer, MiniPlayer, PlayerQueue, PlayerControls, AudioPlayer use tokens; focus-visible on Sidebar, PlayerQueue, DropdownMenuTrigger/Item, TabsTrigger. Typography: text-[10px]/[9px] → text-xs where applicable. - ESLint: no-restricted-syntax (warn) for w-/h-/rounded-/shadow-/text-/spacing arbitrary. - Scripts: report-arbitrary-values.mjs, capture/compare/generate visual; visual-complete.spec.ts. - Stories full layout: Dashboard, Playlists, Library, Settings, Profile in DashboardLayout.stories. - .cursorrules + README: DESIGN_TOKENS, APP_SHELL, visual commands, no arbitrary without justification. - apps/web/.gitignore: e2e test artifacts (test-results-visual, playwright-report-visual). Co-authored-by: Cursor <cursoragent@cursor.com>
20 KiB
Audit stratégique frontend — Veza
Date : 2025-02-08
Équipe fictive : Staff Frontend Engineer, Principal Product Designer, Architecte logiciel senior, Tech Lead perf/a11y/DX, Product Manager.
Objectif : Diagnostic sans complaisance, écart vs Discord/Spotify, feuille de route et recommandations actionnables.
A. Diagnostic brutal mais honnête
Niveau global de l’application
Verdict : intermédiaire à senior (6/10), avec des poches de niveau amateur et quelques bases solides.
Justification en une phrase : Vous avez mis en place une structure feature-based, un design system documenté, des patterns (skeletons, optimistic UI, MSW), et une batterie de tests/stories, mais l’exécution est inégale, les règles du projet sont régulièrement violées (valeurs arbitraires, double source d’auth), et le ressenti “produit fini” est entamé par des correctifs visibles (fix-.css, patches dans main.tsx) et une cohérence a11y/UX partielle.*
- Architecture : Correcte en intention (features/, services/, stores/, router lazy), mais artisanale dans les détails (double source d’auth, callbacks no-op en route config, stores globaux vs feature stores).
- Qualité du code : Bonne sur les parties auditées (auth, playlists, player), dette structurelle (duplication auth Context vs Store, ~130 fichiers avec valeurs Tailwind arbitraires malgré une règle explicite).
- Performance : Bonne base (lazy routes, React Query, skeletons), mais pas de stratégie Suspense/streaming ni de priorisation explicite perçue vs mesurée. Voir Checklist priorisation performance ci-dessous.
- Design system : Solide sur le papier (KŌDŌ v3, tokens, layout primitives, dark mode), fragile en pratique (règles de layout violées massivement, pas de lint pour les primitives).
- UX / micro-interactions : Suffisante pour un produit interne, insuffisante pour un produit “premium” (feedback incohérent, états vides/erreur pas toujours traités de façon homogène).
- Accessibilité : Présente (aria, roles sur une partie des composants), mais bonus et non pilier (a11y Storybook en
todo, pas de politique WCAG claire). - DX : Correcte (docs internes, MSW, Storybook centralisé), mais terrain miné sur certains sujets (deux façons d’accéder à l’auth, nombreux fix-* à comprendre, seuils de coverage peut‑être non tenus).
5 forces réelles
-
Design system pensé et documenté
index.css: palette sémantique, layout primitives (max-w-layout-content,min-h-layout-main, etc.), easings, keyframes, dark mode. Les règles du projet interdisent les valeurs arbitraires et imposent ces primitives — même si non respectées partout, la cible est claire. -
Storybook-first et MSW bien intégrés
Preview avec MSW en mode strict (onUnhandledRequest: 'error'), décorateur global (Theme, QueryClient, Router, Toast, Audio, Auth, i18n), pas d’import de providers dans les stories. Handlers MSW très complets (~1600 lignes). Environ 324 stories : bonne couverture de surface. -
Patterns de chargement et d’erreur explicites
LOADING_STATES_PATTERN.md, composantLoadingState(spinner / inline / skeleton / minimal), skeletons dédiés par page (UserProfilePage, CloudFileBrowser, CourseDetailView, etc.). Données servies par React Query avec états loading/error gérés au niveau page. -
Optimistic UI et utilitaires métier
OPTIMISTIC_UPDATES.mdetoptimisticUpdates.ts(createOptimisticUpdate, createArrayOptimisticUpdate, createToggleOptimisticUpdate). Utilisation dans playlists, likes, commentaires. Rollback et invalidation documentés. -
Surface de tests unitaires et d’intégration
~186 fichiers*.test.tsxet ~100*.test.ts, intégration auth/playlists/player, Vitest avec coverage (seuils 80 % configurés). E2E Playwright et visuels présents. Base saine pour monter en rigueur.
5 faiblesses structurelles majeures
-
Double source de vérité pour l’auth
AuthContext+useAuth()(Storybook, ProtectedRoute, quelques pages) vsauthStore(Zustand) +useAuthStore()(App, Sidebar, Login, etc.).App.tsxet hydratation utilisent le store ; le décorateur Storybook utilise le Context. Risque de désync, double appel à l’API, et confusion pour tout nouveau dev. Dette d’architecture prioritaire. -
Règles de layout systématiquement violées
La règle “pas de valeurs arbitraires (w-[…], z-[…], etc.)” et “utiliser les layout primitives” est explicite dans.cursorrulesetindex.css, mais environ 130 fichiers utilisentw-[,h-[,z-[, etc. La Sidebar elle‑même utilisew-64,z-[90],z-[95],left-6,top-20. Le design system est sous‑exploité et la cohérence visuelle fragilisée. -
Correctifs applicatifs visibles et fragiles
main.tsxetApp.tsxchargent plusieurs correctifs :fix-input-focus.css,fix-login-form.css,fixDisplayIssues,fixInputFocus. Cela indique des problèmes de conception (focus, formulaires) résolus par patch plutôt qu’à la source. Coût de maintenance et risque de régression élevé. -
Route config et couplage produit
routeConfig.tsxinjecte des callbacks vides dans les éléments de route :onCreateProduct={() => {}},onNavigateTrack={() => {}}. La routing ne doit pas porter de callbacks métier ; ces props devraient remonter d’un layout ou d’un contexte. Signe que la frontière entre routing et feature n’est pas claire. -
Accessibilité et qualité perçue en option
Storybook :a11y: { test: 'todo' }. Beaucoup de composants ont desaria-*ourole, mais de façon non systématique (ex.KodoEmptyStatesans role/aria pour l’état vide, Sidebar sansrole="navigation"niaria-label). Pas de politique WCAG ni de critères de sortie a11y. L’accessibilité reste un “plus” et non un pilier, ce qui limite la maturité produit et l’inclusivité.
B. Écart avec Discord & Spotify
Tableau comparatif technique et concret (pas d’idéalisation).
| Dimension | Discord (référence) | Spotify (référence) | Veza (état actuel) | Écart principal |
|---|---|---|---|---|
| Architecture | Domains clairs, state par feature, peu de global ; routing découplé du métier. | App shell léger, données par view, cache agressif. | Features + stores globaux + double auth (Context + Store). | Une seule source d’auth ; pas de callbacks dans la route config ; frontière nette layout/feature. |
| UX | Feedback immédiat, états loading/empty/error partout, modales et transitions prévisibles. | Transitions fluides, skeletons systématiques, pas de “trous” visuels. | Skeletons et LoadingState présents mais pas partout ; empty/error variables ; correctifs CSS. | Checklist loading/empty/error par écran ; suppression des fix-* par refonte des composants concernés. |
| Performance | Time to interactive court, lazy lourd, prioritisation du above-the-fold. | Streaming / progressive, cache et préchargement. | Lazy routes OK ; pas de Suspense boundary au niveau route ; pas de stratégie “perçu vs mesuré”. | Suspense + skeletons aux limites de route ; métriques perçues (LCP, INP) et objectifs chiffrés. |
| Design | Cohérence stricte, tokens partout, pas de valeurs en dur. | Design system unique, thème et spacing prévisibles. | Tokens et primitives définis mais ~130 fichiers en arbitraire ; primitives peu utilisées. | Lint (ou rule) qui interdit les arbitraires ; migration progressive vers primitives. |
| Sensation de qualité | App “qui ne bugue pas”, erreurs gracieuses, pas de demi-mesures visibles. | Polish, micro-interactions, pas de formulaires “cassés” en prod. | Correctifs visibles (fix-*), callbacks no-op, incohérences a11y. | Réduire la dette visible : un correctif = une issue de refonte ; zero no-op dans la config. |
En résumé : Veza a les briques (design system, patterns, tests, Storybook) mais manque de rigueur d’exécution (une seule auth, respect des règles de layout, accessibilité et UX traitées comme non négociables) et de finition (plus de polish, moins de patches) pour se rapprocher de Discord/Spotify.
Checklist priorisation performance
À traiter dans cet ordre pour maximiser l’impact ressenti sans tout refactorer :
-
First meaningful paint
- Éviter tout JS bloquant avant le premier rendu (splitting par route déjà en place).
- S’assurer que le shell (header + sidebar ou placeholder) + un skeleton de contenu s’affichent sans attendre les données.
-
Skeletons systématiques
- Chaque page qui fetch des données doit afficher un skeleton dédié (pas un spinner générique) pendant le chargement.
- Déjà partiellement fait ; compléter les écrans manquants (voir Phase 2).
-
Optimistic UI pour les actions fréquentes
- Like, follow, add to playlist, etc. : mise à jour immédiate + rollback si erreur.
- Déjà documenté et utilisé ; étendre aux actions qui n’ont pas encore le pattern.
-
Réduction des re-renders inutiles
- Vérifier les composants lourds (listes, tableaux) : memo, sélecteurs Zustand fins, ou découpage pour limiter les sous-arbre qui se re-rendent.
- React Query évite déjà des refetch inutiles si
staleTimeest configuré ; auditer les queryKeys et invalidation pour éviter des cascades.
-
Métriques et objectifs
- Mesurer LCP, INP (ou FID), TTI sur les parcours critiques (login, dashboard, lecture).
- Fixer des objectifs (ex. LCP < 2.5 s sur 3G) et un process de revue en cas de régression.
-
Suspense aux limites de route
- Envelopper les arbres de route dans
<Suspense fallback={<PageSkeleton />}>pour que le navigateur puisse afficher le fallback sans attendre tout le bundle enfant. - Optionnel mais utile dès que le bundle par route grossit.
- Envelopper les arbres de route dans
C. Feuille de route de transformation
Phase 1 — Fondations (ce qui doit être corrigé avant tout)
Objectifs
- Une seule source de vérité pour l’auth.
- Arrêt des correctifs “magiques” et renforcement du respect du design system.
Actions concrètes
-
Unifier l’auth
- Décider : soit tout sur Zustand (authStore), soit tout sur Context (AuthProvider). Recommandation : Zustand (déjà utilisé par App, hydratation, Sidebar).
- Migrer tous les usages de
useAuth()versuseAuthStore()(ou l’inverse si vous choisissez Context). - Faire du AuthProvider un simple consommateur du store (s’il est gardé pour Storybook/compat).
- Supprimer la duplication et documenter la décision dans un ADR.
-
Supprimer les callbacks de la route config
- Retirer
onCreateProduct,onNavigateTrack, etc. des éléments de route. - Fournir ces comportements via layout (contexte ou props depuis un parent commun) ou navigation programmatique + state.
- Retirer
-
Remplacer les fix- par des corrections à la source*
- Pour chaque
fix-input-focus,fix-login-form, etc. : identifier le composant (formulaire, input, focus trap) et corriger le focus, les états et le style dans le composant ou le design system. - Supprimer les imports de fix-* une fois le comportement correct par défaut.
- Pour chaque
-
Lint / règle pour les valeurs arbitraires
- Règle ESLint (ou plugin Tailwind) interdisant les classes du type
w-[...],h-[...],z-[...]pour layout/spacing (avec liste d’exceptions si besoin). - Introduire les exceptions progressivement et traiter les ~130 fichiers par batch (par répertoire ou par feature).
- Règle ESLint (ou plugin Tailwind) interdisant les classes du type
Risques
- Régression auth si la migration n’est pas testée (E2E login/logout, refresh, Storybook).
- Régressions visuelles si on supprime les fix-* sans vérifier tous les écrans.
Indicateurs de réussite
- Un seul mécanisme d’auth utilisé partout ; plus d’import de fix-* dans
main.tsx/App.tsx. - Aucun callback métier dans
routeConfig.tsx. - Lint en place et au moins un premier batch (ex.
components/layout) sans violations.
Phase 2 — Stabilisation et cohérence
Objectifs
- Loading / empty / error systématiques.
- Accessibilité traitée comme exigence, pas comme option.
- Design system respecté (primitives, tokens).
Actions concrètes
-
Checklist par écran
- Pour chaque page/feature : état Loading (skeleton dédié ou générique), Empty (KodoEmptyState ou équivalent), Error (ErrorDisplay + retry).
- Documenter la checklist et l’appliquer aux nouvelles features ; remédier les écrans existants sans état manquant.
-
Accessibilité
- Remplacer
a11y: { test: 'todo' }par une config réelle dans Storybook (règles axe-core, seuils). - Définir un niveau cible (ex. WCAG 2.1 AA) et une liste de composants critiques (formulaires, navigation, player).
- Corriger Sidebar (role="navigation", aria-label), KodoEmptyState (role/aria pour empty state), et autres composants identifiés par un audit axe.
- Remplacer
-
Migration vers layout primitives
- Remplacer progressivement les valeurs arbitraires par les classes du design system (
max-w-layout-content,min-h-layout-main, etc.) et l’échelle Tailwind. - Prioriser : layout (Sidebar, Navbar, content), puis composants partagés (modales, cartes).
- Remplacer progressivement les valeurs arbitraires par les classes du design system (
-
Optimistic UI et erreurs
- Vérifier que tous les usages d’optimistic updates ont un rollback et un feedback utilisateur en cas d’échec (toast, réversion visuelle).
- Centraliser la logique d’erreur (ex.
apiToastHelper, ErrorDisplay) pour éviter des traitements ad hoc.
Risques
- Scope trop large si on veut tout migrer d’un coup ; prioriser par trafic ou par criticité.
Indicateurs de réussite
- Chaque écran couvert par la checklist loading/empty/error.
- Storybook a11y activé et 0 violation sur les stories des composants critiques.
- Réduction mesurée du nombre de violations “arbitraires” (objectif chiffré par sprint).
Phase 3 — Polish et excellence perçue
Objectifs
- Transitions et micro-interactions cohérentes.
- Performance perçue (skeleton, priorisation) et mesurée (LCP, INP).
- Réduction des “petits bugs” visuels et comportementaux.
Actions concrètes
-
Suspense et streaming
- Définir des Suspense boundaries au niveau des routes (ou des sous-arbres) avec fallback skeleton.
- Vérifier que le premier paint utile (header + skeleton contenu) est rapide.
-
Métriques et objectifs
- Suivre LCP, INP (ou FID), TTI.
- Définir des objectifs (ex. LCP < 2.5 s sur 3G) et un process de revue si dégradation.
-
Micro-interactions
- Utiliser les variables déjà définies (
--duration-fast,--ease-out, etc.) partout où c’est pertinent (hover, focus, modales). - Passer en revue les CTA et les listes interactives pour un feedback immédiat (disabled + spinner ou état “loading” sur le bouton).
- Utiliser les variables déjà définies (
-
Nettoyage produit
- Supprimer ou documenter tout code mort et composants non utilisés.
- S’assurer qu’aucun no-op ou placeholder visible ne reste dans les parcours principaux.
Risques
- Sur-optimisation sans mesure ; garder les métriques comme garde-fou.
Indicateurs de réussite
- LCP/INP dans la cible sur les parcours critiques.
- Pas de régression a11y ; design system respecté sur les nouvelles livraisons.
Phase 4 — Scalabilité et long terme
Objectifs
- Code prêt pour 10× fonctionnalités et 10× équipe.
- Dette technique visible et pilotée.
- Expérimentations (A/B, feature flags) sans tout casser.
Actions concrètes
-
Frontières de domaines
- Clarifier ce qui vit dans
features/vscomponents/(ex. pas de duplication forms auth dans les deux). - Règle simple : composants réutilisables et design system dans
components/; logique métier et pages dansfeatures/.
- Clarifier ce qui vit dans
-
State et données
- Documenter quand utiliser React Query vs Zustand (server state vs client state).
- Éviter de dupliquer des données serveur dans des stores globaux (library, etc.) sans stratégie d’invalidation claire.
-
Feature flags et expérimentations
- Introduire un mécanisme léger de feature flags (build-time ou runtime) pour découpler déploiement et activation.
- Tester une feature derrière flag sans impacter les parcours principaux.
-
Documentation et onboarding
- Un README frontend à jour (structure, auth unique, design system, comment lancer Storybook et tests).
- Créer ou mettre à jour
docs/STORYBOOK_CONTRACT.md(référencé dans le code) : états requis (Loading, Error, Empty), pas de providers dans les stories, MSW pour l’API.
Risques
- Refactors trop larges ; privilégier des incréments et des ADR pour les décisions d’architecture.
Indicateurs de réussite
- Nouveau dev opérationnel (première feature livrée) en un temps cible (ex. < 2 jours).
- Dette listée et priorisée (ex. backlog “dette” avec critères de sortie).
D. Recommandations non évidentes
-
Ne pas “nettoyer” tout le code arbitraire d’un coup.
Une équipe junior aurait tendance à tout remplacer en un big bang. Mieux vaut : lint qui bloque en nouveau code, et migration par zones (layout d’abord, puis composants partagés). Les 130 fichiers se traitent par lots avec revue visuelle et régression. -
Traiter l’auth comme un projet de migration, pas comme une tâche.
Faire un inventaire exhaustif (grepuseAuth/useAuthStore), un plan de migration (ordre des fichiers), des tests E2E et Storybook qui valident login/logout/refresh, puis migrer avec une PR par zone. Un “quick fix” (tout basculer d’un coup) sans tests est risqué. -
Ne pas activer a11y dans Storybook sans budget de correction.
Activer axe avec des règles strictes du jour au lendemain va faire exploser les “failed”. Mieux : activer sur un sous-ensemble de stories (ex. composants ui/ + layout), corriger ces stories, puis élargir. a11y doit devenir un critère de merge sur les composants critiques. -
Documenter pourquoi chaque fix- existe.*
Avant de les supprimer, ajouter un commentaire ou un petit doc : “fix-login-form existe parce que …”. Cela évite de recréer le même patch plus tard et guide la refonte (quel composant doit prendre en charge le focus, etc.). -
Mettre des indicateurs de “maturité” par feature.
Une grille simple (loading / empty / error / a11y / tests / Storybook) par feature ou page. Permet de prioriser les remédiations et de ne pas prétendre que “tout est à niveau” alors que certaines zones sont encore fragiles. -
Réserver du temps “dette” dans chaque sprint.
Un projet “correct” reporte la dette ; un projet qui vise l’excellence en consacre une part fixe (ex. 20 %). Chaque sprint : X violations arbitraires en moins, Y écrans avec checklist complète, Z stories avec a11y passante. -
Éviter d’ajouter de nouveaux stores globaux “au fil de l’eau”.
Vous avez déjà auth, ui, library, cart, chat. Avant d’en ajouter un nouveau : vérifier si React Query + état local (ou store de feature) ne suffit pas. Les stores globaux multiplient les sources de vérité et compliquent le debugging et la cohérence.
Synthèse
- Niveau actuel : Intermédiaire à senior (6/10), avec de vraies bases (design system, patterns, MSW, tests, Storybook) et des faiblesses structurelles (double auth, violations de layout, correctifs, a11y en option).
- Écart Discord/Spotify : Moins une question de stack que de rigueur d’exécution (une auth, respect des règles, a11y et UX non négociables) et de finition (moins de patches, plus de polish).
- Ordre recommandé : Fondations (auth, route config, fix-*, lint) → Stabilisation (checklist loading/empty/error, a11y, primitives) → Polish (Suspense, métriques, micro-interactions) → Scalabilité (frontières, state, flags, doc).
- Ton : Exigeant mais réaliste : le projet peut viser un niveau “world-class” à condition de traiter la dette identifiée comme un plan structuré et de ne pas ajouter de nouvelle dette sur l’auth et le layout.