From 39b2b642d268365bc5f90c9719355086b51f4b7c Mon Sep 17 00:00:00 2001 From: senke Date: Sun, 8 Feb 2026 17:15:58 +0100 Subject: [PATCH] =?UTF-8?q?feat(web):=20UI=20premium=20Discord/Spotify-lik?= =?UTF-8?q?e=20=E2=80=94=20tokens,=20shadows,=20focus,=20layout?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- .cursorrules | 9 +- apps/web/.gitignore | 4 + apps/web/AUDIT_STRATEGIQUE_FRONTEND_2025.md | 280 +++++++++++++++ apps/web/README.md | 10 +- apps/web/docs/APP_SHELL.md | 68 ++++ apps/web/docs/DESIGN_TOKENS.md | 109 ++++++ apps/web/docs/FULL_LAYOUT_PAGE.md | 61 ++++ apps/web/e2e/tests/visual/sidebar.spec.ts | 30 ++ apps/web/e2e/visual-complete.spec.ts | 171 +++++++++ apps/web/eslint.config.js | 28 ++ apps/web/package.json | 7 + apps/web/playwright.config.visual.ts | 58 ++-- apps/web/scripts/capture-visual-baseline.mjs | 131 +++++++ apps/web/scripts/compare-visual.mjs | 189 ++++++++++ apps/web/scripts/generate-visual-report.mjs | 197 +++++++++++ apps/web/scripts/report-arbitrary-values.mjs | 107 ++++++ apps/web/src/components/BulkModeBanner.tsx | 2 +- apps/web/src/components/commerce/CartItem.tsx | 2 +- .../src/components/commerce/WishlistView.tsx | 2 +- .../components/dashboard/ActivityGraph.tsx | 8 +- .../web/src/components/dashboard/StatCard.tsx | 2 +- .../src/components/developer/WebhooksView.tsx | 2 +- .../developer/modals/CreateAPIKeyModal.tsx | 12 +- .../src/components/education/CourseCard.tsx | 6 +- .../components/education/modals/QuizModal.tsx | 2 +- apps/web/src/components/feedback/Progress.tsx | 4 +- apps/web/src/components/feedback/Toast.tsx | 4 +- .../forms/PasswordStrengthIndicator.tsx | 2 +- .../gamification/AchievementCard.tsx | 6 +- .../components/gamification/ProfileXPView.tsx | 2 +- .../web/src/components/gamification/XPBar.tsx | 6 +- .../components/inventory/EquipmentCard.tsx | 6 +- .../web/src/components/layout/AudioPlayer.tsx | 4 +- .../layout/DashboardLayout.stories.tsx | 131 +++++-- .../layout/DashboardLayout.test.tsx | 7 +- .../src/components/layout/DashboardLayout.tsx | 12 +- apps/web/src/components/layout/Header.tsx | 10 +- apps/web/src/components/layout/Layout.tsx | 4 +- apps/web/src/components/layout/Navbar.tsx | 2 +- apps/web/src/components/layout/Sidebar.tsx | 37 +- .../components/marketplace/ProductCard.tsx | 4 +- apps/web/src/components/player/FullPlayer.tsx | 2 +- .../web/src/components/player/LyricsPanel.tsx | 2 +- apps/web/src/components/player/MiniPlayer.tsx | 12 +- .../src/components/player/PlayerControls.tsx | 2 +- .../web/src/components/social/ExploreView.tsx | 2 +- .../components/social/groups/GroupCard.tsx | 4 +- .../src/components/theme/ThemeSwitcher.tsx | 2 +- apps/web/src/components/ui/ErrorDisplay.tsx | 2 +- apps/web/src/components/ui/FAB.tsx | 4 +- apps/web/src/components/ui/KodoEmptyState.tsx | 2 +- apps/web/src/components/ui/Sidebar.tsx | 2 +- .../ui/accordion/AccordionContent.tsx | 2 +- apps/web/src/components/ui/badge.tsx | 6 +- apps/web/src/components/ui/button.tsx | 4 +- apps/web/src/components/ui/card.tsx | 8 +- apps/web/src/components/ui/checkbox.tsx | 6 +- apps/web/src/components/ui/collapsible.tsx | 4 +- .../ui/dropdown-menu/DropdownMenuItem.tsx | 2 +- .../ui/dropdown-menu/DropdownMenuTrigger.tsx | 10 +- apps/web/src/components/ui/dropdown.tsx | 2 +- apps/web/src/components/ui/floating-input.tsx | 6 +- apps/web/src/components/ui/input.tsx | 4 +- .../ui/lazy-component/LazyErrorFallback.tsx | 2 +- .../ui/optimized-image/OptimizedImage.tsx | 2 +- apps/web/src/components/ui/progress.tsx | 6 +- apps/web/src/components/ui/slider.tsx | 4 +- apps/web/src/components/ui/textarea.tsx | 2 +- .../src/components/upload/BulkUploadModal.tsx | 4 +- .../src/components/upload/FileUploadZone.tsx | 8 +- .../components/upload/UploadProgressBar.tsx | 4 +- .../views/admin-view/AdminViewSidebar.tsx | 2 +- .../CheckoutViewOrderSummary.tsx | 2 +- .../views/discover-view/DiscoverViewHero.tsx | 2 +- .../file-manager-view/FileManagerViewGrid.tsx | 2 +- .../views/live-view/LiveViewChat.tsx | 2 +- .../views/live-view/LiveViewPlayer.tsx | 2 +- .../views/live-view/LiveViewRecommended.tsx | 2 +- .../views/live-view/LiveViewStreamInfo.tsx | 2 +- .../MarketplaceViewHeader.tsx | 2 +- .../MarketplaceViewSidebar.tsx | 2 +- .../settings-view/SettingsViewHeader.tsx | 2 +- .../views/social-view/SocialViewFeedItem.tsx | 2 +- .../views/social-view/SocialViewSidebar.tsx | 2 +- .../views/social-view/SocialViewTrending.tsx | 4 +- .../features/auth/components/AuthButton.tsx | 2 +- .../features/auth/components/AuthLayout.tsx | 2 +- .../features/auth/components/LoginForm.tsx | 4 +- .../features/auth/components/RegisterForm.tsx | 4 +- .../features/chat/components/ChatInput.tsx | 2 +- .../features/chat/components/ChatMessage.tsx | 10 +- .../src/features/chat/components/ChatRoom.tsx | 4 +- .../chat-sidebar/ConversationItem.tsx | 10 +- apps/web/src/features/chat/pages/ChatPage.tsx | 4 +- .../dashboard/pages/DashboardPage.tsx | 34 +- .../src/features/error/pages/NotFoundPage.tsx | 12 +- .../pages/library-page/LibraryPageEmpty.tsx | 6 +- .../pages/library-page/LibraryPageGrid.tsx | 23 +- .../pages/library-page/LibraryPageList.tsx | 6 +- .../pages/library-page/LibraryPageToolbar.tsx | 2 +- .../player/components/GlobalPlayer.tsx | 27 +- .../features/player/components/MiniPlayer.tsx | 2 +- .../player/components/PlayerControls.tsx | 10 +- .../player/components/PlayerExpanded.tsx | 4 +- .../player/components/PlayerQueue.tsx | 18 +- .../player/components/VolumeControl.tsx | 2 +- .../playlists/components/PlaylistCard.tsx | 6 +- .../components/PlaylistTrackItem.tsx | 8 +- .../components/playlist-list/PlaylistList.tsx | 2 +- .../playlist-list/PlaylistListToolbar.tsx | 4 +- .../playlists/pages/PlaylistListPage.tsx | 2 +- .../PlaylistDetailPageActionsBar.tsx | 4 +- .../PlaylistDetailPageCoverAndInfo.tsx | 4 +- .../UserProfilePageHeader.tsx | 4 +- .../user-profile-page/UserProfilePageTabs.tsx | 4 +- .../src/features/roles/pages/RolesPage.tsx | 2 +- .../search-page/SearchPageHeader.tsx | 2 +- .../search/components/search/SearchInput.tsx | 2 +- .../cloud-file-browser/FileGridCard.tsx | 2 +- .../TrackDetailPageCoverAndActions.tsx | 2 +- apps/web/src/index.css | 327 +++++++++++++++--- .../src/pages/marketplace/MarketplaceHome.tsx | 2 +- apps/web/src/styles/card.css | 4 +- apps/web/src/styles/design-system.css | 39 ++- apps/web/visual-tests/PHASE1_REPORT.md | 97 ++++++ .../visual-tests/PHASE2_COMPARATIVE_REPORT.md | 285 +++++++++++++++ apps/web/visual-tests/PHASE3_CHANGELOG.md | 62 ++++ apps/web/visual-tests/README.md | 66 ++++ apps/web/visual-tests/VISUAL_AUDIT_REPORT.md | 74 ++++ apps/web/visual-tests/baselines/.gitkeep | 0 .../baselines/404-desktop-dark.png | Bin 0 -> 35795 bytes .../visual-tests/baselines/404-desktop.png | Bin 0 -> 4253 bytes .../baselines/dashboard-desktop-dark.png | Bin 0 -> 65994 bytes .../baselines/dashboard-desktop.png | Bin 0 -> 4253 bytes .../baselines/header-desktop-dark.png | Bin 0 -> 10475 bytes .../baselines/library-desktop-dark.png | Bin 0 -> 65994 bytes .../baselines/library-desktop.png | Bin 0 -> 4253 bytes .../baselines/login-desktop-dark.png | Bin 0 -> 65994 bytes .../visual-tests/baselines/login-desktop.png | Bin 0 -> 4253 bytes .../baselines/playlists-desktop-dark.png | Bin 0 -> 65994 bytes .../baselines/playlists-desktop.png | Bin 0 -> 4253 bytes .../baselines/register-desktop-dark.png | Bin 0 -> 31739 bytes .../baselines/register-desktop.png | Bin 0 -> 4253 bytes apps/web/visual-tests/current/.gitkeep | 0 .../visual-tests/current/404-desktop-dark.png | Bin 0 -> 35878 bytes apps/web/visual-tests/current/404-desktop.png | Bin 0 -> 4253 bytes .../current/dashboard-desktop-dark.png | Bin 0 -> 65994 bytes .../current/dashboard-desktop.png | Bin 0 -> 4253 bytes .../current/header-desktop-dark.png | Bin 0 -> 10475 bytes .../current/library-desktop-dark.png | Bin 0 -> 65994 bytes .../visual-tests/current/library-desktop.png | Bin 0 -> 4253 bytes .../current/login-desktop-dark.png | Bin 0 -> 65994 bytes .../visual-tests/current/login-desktop.png | Bin 0 -> 4253 bytes .../current/playlists-desktop-dark.png | Bin 0 -> 65994 bytes .../current/playlists-desktop.png | Bin 0 -> 4253 bytes .../current/register-desktop-dark.png | Bin 0 -> 33727 bytes .../visual-tests/current/register-desktop.png | Bin 0 -> 4253 bytes apps/web/visual-tests/diffs/.gitkeep | 0 .../visual-tests/diffs/404-desktop-dark.png | Bin 0 -> 37685 bytes apps/web/visual-tests/diffs/404-desktop.png | Bin 0 -> 4851 bytes .../diffs/dashboard-desktop-dark.png | Bin 0 -> 33677 bytes .../visual-tests/diffs/dashboard-desktop.png | Bin 0 -> 4851 bytes .../diffs/header-desktop-dark.png | Bin 0 -> 7022 bytes .../diffs/library-desktop-dark.png | Bin 0 -> 33677 bytes .../visual-tests/diffs/library-desktop.png | Bin 0 -> 4851 bytes .../visual-tests/diffs/login-desktop-dark.png | Bin 0 -> 33677 bytes apps/web/visual-tests/diffs/login-desktop.png | Bin 0 -> 4851 bytes .../diffs/playlists-desktop-dark.png | Bin 0 -> 33677 bytes .../visual-tests/diffs/playlists-desktop.png | Bin 0 -> 4851 bytes .../diffs/register-desktop-dark.png | Bin 0 -> 17255 bytes .../visual-tests/diffs/register-desktop.png | Bin 0 -> 4851 bytes apps/web/visual-tests/reports/.gitkeep | 0 apps/web/visual-tests/reports/index.html | 191 ++++++++++ apps/web/visual-tests/reports/results.json | 136 ++++++++ name Plan UI premium 6.txt | 306 ++++++++++++++++ ...mplet_et_vues_full_layout_afbefcf6.plan.md | 172 +++++++++ 176 files changed, 3525 insertions(+), 362 deletions(-) create mode 100644 apps/web/.gitignore create mode 100644 apps/web/AUDIT_STRATEGIQUE_FRONTEND_2025.md create mode 100644 apps/web/docs/APP_SHELL.md create mode 100644 apps/web/docs/DESIGN_TOKENS.md create mode 100644 apps/web/docs/FULL_LAYOUT_PAGE.md create mode 100644 apps/web/e2e/tests/visual/sidebar.spec.ts create mode 100644 apps/web/e2e/visual-complete.spec.ts create mode 100644 apps/web/scripts/capture-visual-baseline.mjs create mode 100644 apps/web/scripts/compare-visual.mjs create mode 100644 apps/web/scripts/generate-visual-report.mjs create mode 100644 apps/web/scripts/report-arbitrary-values.mjs create mode 100644 apps/web/visual-tests/PHASE1_REPORT.md create mode 100644 apps/web/visual-tests/PHASE2_COMPARATIVE_REPORT.md create mode 100644 apps/web/visual-tests/PHASE3_CHANGELOG.md create mode 100644 apps/web/visual-tests/README.md create mode 100644 apps/web/visual-tests/VISUAL_AUDIT_REPORT.md create mode 100644 apps/web/visual-tests/baselines/.gitkeep create mode 100644 apps/web/visual-tests/baselines/404-desktop-dark.png create mode 100644 apps/web/visual-tests/baselines/404-desktop.png create mode 100644 apps/web/visual-tests/baselines/dashboard-desktop-dark.png create mode 100644 apps/web/visual-tests/baselines/dashboard-desktop.png create mode 100644 apps/web/visual-tests/baselines/header-desktop-dark.png create mode 100644 apps/web/visual-tests/baselines/library-desktop-dark.png create mode 100644 apps/web/visual-tests/baselines/library-desktop.png create mode 100644 apps/web/visual-tests/baselines/login-desktop-dark.png create mode 100644 apps/web/visual-tests/baselines/login-desktop.png create mode 100644 apps/web/visual-tests/baselines/playlists-desktop-dark.png create mode 100644 apps/web/visual-tests/baselines/playlists-desktop.png create mode 100644 apps/web/visual-tests/baselines/register-desktop-dark.png create mode 100644 apps/web/visual-tests/baselines/register-desktop.png create mode 100644 apps/web/visual-tests/current/.gitkeep create mode 100644 apps/web/visual-tests/current/404-desktop-dark.png create mode 100644 apps/web/visual-tests/current/404-desktop.png create mode 100644 apps/web/visual-tests/current/dashboard-desktop-dark.png create mode 100644 apps/web/visual-tests/current/dashboard-desktop.png create mode 100644 apps/web/visual-tests/current/header-desktop-dark.png create mode 100644 apps/web/visual-tests/current/library-desktop-dark.png create mode 100644 apps/web/visual-tests/current/library-desktop.png create mode 100644 apps/web/visual-tests/current/login-desktop-dark.png create mode 100644 apps/web/visual-tests/current/login-desktop.png create mode 100644 apps/web/visual-tests/current/playlists-desktop-dark.png create mode 100644 apps/web/visual-tests/current/playlists-desktop.png create mode 100644 apps/web/visual-tests/current/register-desktop-dark.png create mode 100644 apps/web/visual-tests/current/register-desktop.png create mode 100644 apps/web/visual-tests/diffs/.gitkeep create mode 100644 apps/web/visual-tests/diffs/404-desktop-dark.png create mode 100644 apps/web/visual-tests/diffs/404-desktop.png create mode 100644 apps/web/visual-tests/diffs/dashboard-desktop-dark.png create mode 100644 apps/web/visual-tests/diffs/dashboard-desktop.png create mode 100644 apps/web/visual-tests/diffs/header-desktop-dark.png create mode 100644 apps/web/visual-tests/diffs/library-desktop-dark.png create mode 100644 apps/web/visual-tests/diffs/library-desktop.png create mode 100644 apps/web/visual-tests/diffs/login-desktop-dark.png create mode 100644 apps/web/visual-tests/diffs/login-desktop.png create mode 100644 apps/web/visual-tests/diffs/playlists-desktop-dark.png create mode 100644 apps/web/visual-tests/diffs/playlists-desktop.png create mode 100644 apps/web/visual-tests/diffs/register-desktop-dark.png create mode 100644 apps/web/visual-tests/diffs/register-desktop.png create mode 100644 apps/web/visual-tests/reports/.gitkeep create mode 100644 apps/web/visual-tests/reports/index.html create mode 100644 apps/web/visual-tests/reports/results.json create mode 100644 name Plan UI premium 6.txt create mode 100644 shell_complet_et_vues_full_layout_afbefcf6.plan.md diff --git a/.cursorrules b/.cursorrules index bea5c20ef..da33799f8 100644 --- a/.cursorrules +++ b/.cursorrules @@ -2,13 +2,14 @@ ## 1. Standards Tailwind & Layout -- **Interdiction formelle** d'utiliser des valeurs arbitraires (ex: `w-[300px]`) pour les espacements, tailles ou marges. +- **Interdiction formelle** d'utiliser des valeurs arbitraires (ex: `w-[300px]`, `gap-[7px]`, `rounded-[12px]`, `shadow-[...]`) pour les espacements, tailles, rayons ou ombres sans justification (voir exceptions dans `apps/web/docs/DESIGN_TOKENS.md`). - Utilise exclusivement l'échelle de spacing native de Tailwind ou les **Layout Primitives** définies dans `apps/web/src/index.css` : - `max-w-layout-content` (1600px) - `min-h-layout-main` (calc(100vh - 4rem)) - `min-h-layout-page` (600px) - `min-h-layout-page-sm` (400px) - `min-h-layout-story` (192px, pour les stories) +- Référence unique : **DESIGN_TOKENS.md** et **APP_SHELL.md** dans `apps/web/docs/` pour layout, espacements, ombres, typo, transitions. ## 2. Cycle de Vie des Composants (Storybook-First) @@ -29,3 +30,9 @@ ## 4. Audit & Qualité - Avant de finaliser une tâche, lance `npm run test:storybook` (depuis `apps/web`) après build + serve sur 6007 pour garantir 0 erreur réseau/console. + +## 5. Tests visuels et valeurs arbitraires + +- **Commandes visuelles** (depuis `apps/web`) : `npm run visual:capture` (capture écrans → `visual-tests/current/`), `npm run visual:compare` (diff vs baselines), `npm run visual:update` (mise à jour des baselines). Procédure détaillée dans `apps/web/visual-tests/README.md`. +- **Rapport des valeurs arbitraires** : `node scripts/report-arbitrary-values.mjs` (optionnel `--json` ou `--dir src/features`) pour lister les patterns à migrer ; pas de remplacement automatique sans revue. +- **Nouvelle page full layout** : suivre `apps/web/docs/FULL_LAYOUT_PAGE.md` (route, story, MSW, viewport). diff --git a/apps/web/.gitignore b/apps/web/.gitignore new file mode 100644 index 000000000..173a85420 --- /dev/null +++ b/apps/web/.gitignore @@ -0,0 +1,4 @@ +# Test / E2E artifacts (generated by Playwright visual tests) +e2e/test-results-visual/ +e2e/playwright-report-visual/ +e2e/playwright-report-visual/**/data/ diff --git a/apps/web/AUDIT_STRATEGIQUE_FRONTEND_2025.md b/apps/web/AUDIT_STRATEGIQUE_FRONTEND_2025.md new file mode 100644 index 000000000..719ccd2fe --- /dev/null +++ b/apps/web/AUDIT_STRATEGIQUE_FRONTEND_2025.md @@ -0,0 +1,280 @@ +# 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 + +1. **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. + +2. **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. + +3. **Patterns de chargement et d’erreur explicites** + `LOADING_STATES_PATTERN.md`, composant `LoadingState` (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. + +4. **Optimistic UI et utilitaires métier** + `OPTIMISTIC_UPDATES.md` et `optimisticUpdates.ts` (createOptimisticUpdate, createArrayOptimisticUpdate, createToggleOptimisticUpdate). Utilisation dans playlists, likes, commentaires. Rollback et invalidation documentés. + +5. **Surface de tests unitaires et d’intégration** + ~186 fichiers `*.test.tsx` et ~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 + +1. **Double source de vérité pour l’auth** + `AuthContext` + `useAuth()` (Storybook, ProtectedRoute, quelques pages) vs `authStore` (Zustand) + `useAuthStore()` (App, Sidebar, Login, etc.). `App.tsx` et 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. + +2. **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 `.cursorrules` et `index.css`, mais environ **130 fichiers** utilisent `w-[`, `h-[`, `z-[`, etc. La Sidebar elle‑même utilise `w-64`, `z-[90]`, `z-[95]`, `left-6`, `top-20`. Le design system est sous‑exploité et la cohérence visuelle fragilisée. + +3. **Correctifs applicatifs visibles et fragiles** + `main.tsx` et `App.tsx` chargent 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é. + +4. **Route config et couplage produit** + `routeConfig.tsx` injecte 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. + +5. **Accessibilité et qualité perçue en option** + Storybook : `a11y: { test: 'todo' }`. Beaucoup de composants ont des `aria-*` ou `role`, mais de façon non systématique (ex. `KodoEmptyState` sans role/aria pour l’état vide, Sidebar sans `role="navigation"` ni `aria-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 : + +1. **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. + +2. **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). + +3. **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. + +4. **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 `staleTime` est configuré ; auditer les queryKeys et invalidation pour éviter des cascades. + +5. **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. + +6. **Suspense aux limites de route** + - Envelopper les arbres de route dans `}>` 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. + +--- + +## 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** +1. **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()` vers `useAuthStore()` (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. + +2. **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. + +3. **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. + +4. **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). + +**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** +1. **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. + +2. **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. + +3. **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). + +4. **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** +1. **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. + +2. **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. + +3. **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). + +4. **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** +1. **Frontières de domaines** + - Clarifier ce qui vit dans `features/` vs `components/` (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 dans `features/`. + +2. **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. + +3. **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. + +4. **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 (grep `useAuth` / `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. diff --git a/apps/web/README.md b/apps/web/README.md index dcb529271..490444ef3 100644 --- a/apps/web/README.md +++ b/apps/web/README.md @@ -96,6 +96,8 @@ npm run validate:all ### Code Quality - `npm run lint` - Run ESLint - `npm run lint:fix` - Fix ESLint issues +- `npm run lint:ui` - Run ESLint on `src/components` and `src/features` only +- `npm run report:arbitrary` - Report Tailwind arbitrary values (w-[...], gap-[...], etc.) for migration - `npm run typecheck` - Type check without emitting files - `npm run fmt` - Format code with Prettier @@ -127,14 +129,14 @@ apps/web/ ## Design System -The application uses the Kodo design system: +The application uses the Kodo design system. **Single source of truth** for layout, spacing, shadows, and transitions: `docs/DESIGN_TOKENS.md`. Shell layout: `docs/APP_SHELL.md`. - **Colors**: Kodo color palette (see `src/styles/COLOR_USAGE.md`) - **Components**: Design system components in `src/components/ui/` -- **Typography**: Type scale system (see `src/styles/TYPOGRAPHY_GUIDE.md`) -- **Spacing**: Spacing scale system (see `src/styles/SPACING_GUIDE.md`) +- **Typography**: Type scale and hierarchy (see `docs/DESIGN_TOKENS.md`, `src/styles/TYPOGRAPHY_GUIDE.md`) +- **Spacing**: Spacing scale (see `docs/SPACING_GUIDE.md`) — no arbitrary values (e.g. `w-[300px]`, `gap-[7px]`) without justification. -See `src/components/COMPONENT_USAGE.md` for component usage guidelines. +**Visual regression**: `npm run visual:capture`, `npm run visual:compare`, `npm run visual:update` (see `visual-tests/README.md`). **Arbitrary values report**: `npm run report:arbitrary` to list Tailwind arbitrary patterns for migration. **New full-layout page**: see `docs/FULL_LAYOUT_PAGE.md`. ## ESLint Rules diff --git a/apps/web/docs/APP_SHELL.md b/apps/web/docs/APP_SHELL.md new file mode 100644 index 000000000..36648ab58 --- /dev/null +++ b/apps/web/docs/APP_SHELL.md @@ -0,0 +1,68 @@ +# App Shell — Référence + +Vue d’ensemble du shell applicatif (layout principal) et des tokens CSS associés. Toute évolution du shell (sidebar, header, main, player) doit s’appuyer sur ces variables et classes pour rester cohérente. + +## Rôle du shell + +- **Sidebar** : navigation fixe à gauche (largeur pilotée par `--sidebar-width-expanded` / `--sidebar-width-collapsed`). Voir [index.css](../src/index.css) section "Sidebar layout". +- **Header** : barre fixe en haut (hauteur `--header-height`), dont le bord gauche suit la sidebar (expanded/collapsed). +- **Main** : zone scrollable (contenu des pages). Marges gauche pilotées par l’état de la sidebar ; padding top/bottom pour ne pas passer sous le header ni le player. +- **Player** : barre de lecture fixe en bas (rendue par `GlobalPlayer` dans `DashboardLayout`). + +Fichiers principaux : + +- [DashboardLayout.tsx](../src/components/layout/DashboardLayout.tsx) — assemblage Sidebar, zone main, Header, GlobalPlayer. +- [Header.tsx](../src/components/layout/Header.tsx) — barre supérieure (recherche, actions, user menu). + +## Variables CSS (shell) + +Définies dans `:root` dans [index.css](../src/index.css), section "App shell" : + +| Variable | Valeur | Rôle | +|----------|--------|------| +| `--header-height` | 4rem | Hauteur de la barre header fixe | +| `--main-offset-top` | 5rem | Padding-top du `
` (dégager le header) | +| `--main-offset-bottom` | 8rem | Padding-bottom du `
` (réserve pour le player) | +| `--main-margin-left-expanded` | 18rem | Marge gauche du conteneur main quand sidebar ouverte (15rem sidebar + 3rem gap) | +| `--main-margin-left-collapsed` | 7rem | Marge gauche du main quand sidebar fermée (5rem + 2rem gap) | +| `--header-left-expanded` | 18rem | Position `left` de la barre header quand sidebar ouverte | +| `--header-left-collapsed` | 5rem | Position `left` de la barre header quand sidebar fermée | + +Les largeurs sidebar sont définies à part : `--sidebar-width-expanded` (15rem), `--sidebar-width-collapsed` (5rem). Pour garder la cohérence, ne pas modifier les marges main/header sans ajuster ces tokens de façon cohérente. + +## Classes utilitaires (shell) + +Définies dans [index.css](../src/index.css) après les classes sidebar (`.left-sidebar`, `.z-sidebar`, etc.) : + +| Classe | Propriété | Usage | +|--------|-----------|--------| +| `.h-header` | height | Barre header et div interne du header | +| `.pt-main` | padding-top | Élément `
` | +| `.pb-main` | padding-bottom | Élément `
` | +| `.ml-main-expanded` | margin-left | Conteneur principal (avec préfixe `lg:`) quand sidebar ouverte | +| `.ml-main-collapsed` | margin-left | Conteneur principal (avec `lg:`) quand sidebar fermée | +| `.left-header-expanded` | left | Barre header quand sidebar ouverte | +| `.left-header-collapsed` | left | Barre header quand sidebar fermée | + +À utiliser avec le préfixe responsive `lg:` pour les marges/positions desktop (ex. `lg:ml-main-expanded`). En dessous de `lg`, la sidebar est en overlay et le main utilise `ml-0`, le header `left-0` (`max-lg:left-0`). + +## Comportement responsive + +- **lg (1024px et plus)** : sidebar fixe à gauche, main et header utilisent les classes tokenisées (expanded/collapsed selon `sidebarOpen`). +- **En dessous de lg** : sidebar en overlay (ouverte/fermée par toggle), conteneur main en pleine largeur (`ml-0`), header en pleine largeur (`max-lg:left-0`). + +## Breakpoints et viewports de test + +| Breakpoint | Largeur | Comportement attendu | +|------------|---------|----------------------| +| Mobile | 320px | Sidebar overlay, main pleine largeur, header pleine largeur. | +| Tablet | 768px | Idem (sidebar overlay jusqu’à lg). | +| Desktop | 1024px (lg) | Sidebar fixe, main et header avec marges/positions tokenisées. | +| Large desktop | 1280px | Même comportement que lg, contenu limité par `max-w-layout-content` si applicable. | + +Vérifier visuellement (ou via tests Playwright) que Sidebar, Header et Main se comportent correctement sur ces largeurs. + +## Référence croisée + +- Tokens sidebar (largeurs, offsets, z-index) : [index.css](../src/index.css) — "Sidebar layout" et classes `.w-sidebar-expanded`, `.left-sidebar`, `.top-sidebar`, `.bottom-sidebar`, `.z-sidebar`, `.z-sidebar-overlay`. +- Layout primitives (max-width contenu, min-height pages) : mêmes variables `--layout-content-max-width`, `--layout-page-min-height`, etc. Le `
` contient un wrapper `max-w-layout-content` pour le contenu. diff --git a/apps/web/docs/DESIGN_TOKENS.md b/apps/web/docs/DESIGN_TOKENS.md new file mode 100644 index 000000000..94223b7ea --- /dev/null +++ b/apps/web/docs/DESIGN_TOKENS.md @@ -0,0 +1,109 @@ +# Design Tokens — Référence + +Source de vérité pour les tokens du design system Veza/Kodo. Toute valeur d’espacement, couleur, ombre, typographie ou transition doit s’appuyer sur ces tokens ou sur l’échelle Tailwind documentée ici. + +## 1. Espacements (layout et sections) + +Définis dans [index.css](../src/index.css) (`:root`) et [design-tokens.css](../src/styles/design-tokens.css) (scale Tailwind). + +| Variable | Valeur | Rôle | +|----------|--------|------| +| `--layout-gap` | 1rem (16px) | Gap par défaut entre sections (align Tailwind gap-4) | +| `--layout-gap-sm` | 0.75rem (12px) | Gap serré (gap-3) | +| `--layout-gap-lg` | 1.5rem (24px) | Gap large (gap-6) | + +Scale complète (4px base) : voir [SPACING_GUIDE.md](SPACING_GUIDE.md) et `design-tokens.css` (`--spacing-0` à `--spacing-96`). Utiliser les classes Tailwind `gap-*`, `p-*`, `m-*`, `space-*` de préférence aux valeurs arbitraires. + +## 2. Couleurs + +- **Kodo (primaire)** : [index.css](../src/index.css) — `--background`, `--foreground`, `--primary`, `--muted`, `--border`, etc. +- **Sidebar** : `--sidebar`, `--sidebar-foreground`, `--sidebar-primary`, etc. (index.css). +- **Mapping et usage** : [src/styles/COLOR_USAGE.md](../src/styles/COLOR_USAGE.md). + +Ne pas utiliser les couleurs par défaut Tailwind (slate, gray, zinc, etc.) ; privilégier les tokens Kodo (kodo-void, kodo-cyan, etc.). + +## 3. Typographie + +Échelle Tailwind utilisée (tailles de texte) : + +| Classe | Taille (rem) | Usage | +|--------|--------------|--------| +| `text-xs` | 0.75rem | Caption, labels secondaires | +| `text-sm` | 0.875rem | Labels, métadonnées | +| `text-base` | 1rem | Corps de texte | +| `text-lg` | 1.125rem | Sous-titres | +| `text-xl` | 1.25rem | Titres de section | +| `text-2xl` | 1.5rem | Titres de page | +| `text-3xl` | 1.875rem | Titres principaux | +| `text-4xl` | 2.25rem | Hero / titres très visibles | + +Line-height et font-weight : suivre les utilitaires Tailwind (`leading-*`, `font-normal`, `font-medium`, `font-semibold`, `font-bold`). Ne pas utiliser de tailles arbitraires (`text-[14px]`, etc.) sauf exception documentée (ex. SVG charts). + +### Hiérarchie visuelle (usage recommandé) + +| Rôle | Classe | Contexte | +|------|--------|----------| +| Titre de page | `text-2xl` ou `text-3xl` + `font-bold` | H1 de la page (Dashboard, Playlists, etc.) | +| Sous-titre / section | `text-lg` ou `text-xl` | Titres de blocs | +| Label / métadonnée | `text-sm` | Labels de champs, infos secondaires | +| Corps | `text-base` | Texte courant | +| Caption / timestamps | `text-xs` | Légendes, durées, timestamps | + +Contraste : privilégier `text-foreground` pour l’info principale et `text-muted-foreground` pour l’info secondaire (style Spotify : titre + artiste, description en gris). + +## 4. Ombres + +Définies dans [design-system.css](../src/styles/design-system.css). + +| Variable | Rôle | +|----------|------| +| `--shadow-sm` | Ombres légères | +| `--shadow-md` | Ombres moyennes, tooltips | +| `--shadow-lg` | Cartes surélevées | +| `--shadow-xl` | Modales | +| `--shadow-soft` | Ombres douces | +| `--shadow-card` | Cartes (alias `--shadow-soft`) | +| `--shadow-modal` | Modales (alias `--shadow-xl`) | +| `--shadow-tooltip` | Tooltips (alias `--shadow-md`) | + +Classes utilitaires dans index.css : `.shadow-card`, `.shadow-modal`, `.shadow-tooltip`, `.shadow-card-hover`, `.shadow-card-glow-cyan`, `.shadow-card-glow-magenta`. Tokens additionnels : `--sidebar-active-indicator`, `--button-primary-glow`, `--button-primary-glow-hover`, `--player-thumb-glow`, `--player-hover-glow`, `--queue-item-current-glow`, `--slider-thumb-glow`, `--status-dot-glow-cyan`, `--status-dot-glow-lime`, `--status-dot-glow-magenta` (design-system.css) avec classes `.sidebar-active-indicator`, `.shadow-button-primary-glow`, `.shadow-button-primary-glow-hover`, `.shadow-player-thumb`, `.shadow-player-hover`, `.shadow-queue-item-current`, `.shadow-slider-thumb`, `.shadow-status-dot-cyan`, `.shadow-status-dot-lime`, `.shadow-status-dot-magenta`. Couverture / hero : `--shadow-cover-depth` (`.shadow-cover-depth`). Gaming / XP : `--shadow-gold-glow` (`.shadow-gold-glow`). FAB : `--shadow-fab-glow`, `--shadow-fab-glow-hover` (`.shadow-fab-glow`, `.shadow-fab-glow-hover`). + +## 5. Layout shell (sidebar, header, main) + +Définis dans [index.css](../src/index.css). Référence complète : [APP_SHELL.md](APP_SHELL.md). + +- **Sidebar** : `--sidebar-width-expanded`, `--sidebar-width-collapsed`, `--sidebar-offset-*`, `--sidebar-z-index`. +- **Header** : `--header-height`, `--header-left-expanded`, `--header-left-collapsed`. +- **Main** : `--main-offset-top`, `--main-offset-bottom`, `--main-margin-left-expanded`, `--main-margin-left-collapsed`. +- **Max heights (drawers, panels, lists)** : `--layout-drawer-max-height` (60vh), `--layout-panel-max-height` (70vh), `--layout-list-max-height` (25rem) — classes `.max-h-layout-drawer`, `.max-h-layout-panel`, `.max-h-layout-list`. + +Ne pas définir de variables concurrentes (ex. `--sidebar-width`, `--header-height`) ailleurs ; index.css est la source unique pour le shell. + +## 6. Transitions et animations + +Définies dans [design-system.css](../src/styles/design-system.css). + +| Variable | Valeur | Usage | +|----------|--------|--------| +| `--duration-instant` | 100ms | Feedback immédiat | +| `--duration-fast` | 150ms | Hover, focus | +| `--duration-normal` | 250ms | Sidebar, modales | +| `--duration-immersive` | 200ms | Micro-interactions (hover, focus) | +| `--duration-slow` | 400ms | Animations visibles | +| `--duration-slower` | 600ms | Transitions longues | +| `--ease-out` | cubic-bezier(0.33, 1, 0.68, 1) | Sortie | +| `--ease-in-out` | cubic-bezier(0.65, 0, 0.35, 1) | Entrée/sortie | +| `--ease-bounce` | cubic-bezier(0.34, 1.56, 0.64, 1) | Rebond | +| `--ease-spring` | cubic-bezier(0.175, 0.885, 0.32, 1.275) | Spring | + +Utiliser ces variables dans les transitions (ex. `transition-all var(--duration-normal) var(--ease-out)`). + +## 7. Exceptions (valeurs arbitraires) + +Les règles ESLint interdisent les classes arbitraires pour `w-[...]`, `h-[...]`, `rounded-[...]`, `shadow-[...]`, etc. Exceptions autorisées avec commentaire `eslint-disable` : + +- SVG ou canvas (ex. `text-[2px]` pour axes de graphiques). +- Composants tiers dont on ne contrôle pas le markup. +- Cas documentés dans ce fichier ou en PR avec justification. + +Voir aussi : [.cursorrules](../.cursorrules) (layout primitives, pas de valeurs arbitraires pour espacements/tailles). diff --git a/apps/web/docs/FULL_LAYOUT_PAGE.md b/apps/web/docs/FULL_LAYOUT_PAGE.md new file mode 100644 index 000000000..0ba5f38ba --- /dev/null +++ b/apps/web/docs/FULL_LAYOUT_PAGE.md @@ -0,0 +1,61 @@ +# Comment ajouter une nouvelle page full layout + +Ce document décrit comment ajouter une nouvelle page principale dans le shell (DashboardLayout) et sa story full layout dans Storybook. + +## Prérequis + +- La page doit être rendue dans le même shell que Dashboard, Playlists, Library, Settings, Profile : **DashboardLayout** (voir [APP_SHELL.md](APP_SHELL.md)). +- Les routes protégées utilisent `wrapProtected` dans [routeConfig.tsx](../src/router/routeConfig.tsx), qui enveloppe le contenu avec `ProtectedLayoutRoute` → `DashboardLayout`. + +## Étapes + +### 1. Route + +Dans [routeConfig.tsx](../src/router/routeConfig.tsx) : + +- Importer (lazy) le composant de page depuis `LazyComponent` ou l’ajouter dans [lazy-component/lazyExports.ts](../src/components/ui/lazy-component/lazyExports.ts). +- Ajouter une entrée dans `getProtectedRoutes()` : + `{ path: '/votre-page', element: wrapProtected() }`. + +### 2. Story full layout + +Dans [DashboardLayout.stories.tsx](../src/components/layout/DashboardLayout.stories.tsx) : + +- Importer la page : + `import { VotrePage } from '@/features/...';` +- Ajouter une story sur le même modèle que `LibraryFullLayout` ou `SettingsFullLayout` : + +```tsx +export const VotrePageFullLayout: Story = { + name: 'Votre page – full layout', + render: () => ( + + + + ), + parameters: { + layout: 'fullscreen', + router: { initialEntries: ['/votre-page'] }, + }, +}; +``` + +### 3. MSW (handlers) + +Si la page charge des données via l’API, ajouter ou réutiliser les handlers dans [handlers.ts](../src/mocks/handlers.ts) pour éviter un loading infini dans Storybook (ex. `http.get('*/api/v1/votre-resource', ...)`). + +### 4. Viewport + +Les stories full layout utilisent le viewport par défaut du meta (desktop). Les captures visuelles Playwright utilisent 1280×720 (voir [visual-tests/README.md](../visual-tests/README.md)). Aucune config supplémentaire n’est nécessaire pour une nouvelle story. + +### 5. Validation visuelle (optionnel) + +Pour inclure la page dans la régression visuelle : + +- Ajouter une entrée dans `SCREENS` dans [e2e/visual-complete.spec.ts](../e2e/visual-complete.spec.ts) si vous voulez une capture full page (ex. `{ name: 'votre-page', url: '/votre-page', auth: true, full: true }`). +- Lancer `npm run visual:capture` puis `npm run visual:compare` ; si les changements sont validés, `npm run visual:update` et committer les baselines. + +## Règles UI + +- Pas de valeurs arbitraires (w-[...], h-[...], gap-[...], etc.) : utiliser l’échelle Tailwind ou les tokens (voir [DESIGN_TOKENS.md](DESIGN_TOKENS.md)). +- Espacements et typo : respecter [SPACING_GUIDE.md](SPACING_GUIDE.md) et la hiérarchie dans DESIGN_TOKENS.md. diff --git a/apps/web/e2e/tests/visual/sidebar.spec.ts b/apps/web/e2e/tests/visual/sidebar.spec.ts new file mode 100644 index 000000000..b659cfbed --- /dev/null +++ b/apps/web/e2e/tests/visual/sidebar.spec.ts @@ -0,0 +1,30 @@ +import { test, expect } from '@playwright/test'; + +test.describe('Visual Regression - Sidebar', () => { + test('should match design tokens implementation', async ({ page }) => { + // Visit the Storybook iframe directly for isolation + // ID derived from title: 'App/Layouts/Sidebar' -> 'app-layouts-sidebar' + await page.goto('http://localhost:6006/iframe.html?id=app-layouts-sidebar--default&viewMode=story'); + + // Wait for component to be stable + await page.waitForSelector('aside'); + + // Take snapshot of the entire customized layout (Side bar is fixed position in the story) + // We target the aside element directly for the component snapshot + const sidebar = page.locator('aside'); + await expect(sidebar).toBeVisible(); + + // Initial State Snapshot + await expect(sidebar).toHaveScreenshot('sidebar-default.png'); + + // Interaction Test: Hover over an item + const studioItem = page.getByText('Cloud Files'); + await studioItem.hover(); + + // Allow animation to complete (transition duration-200) + await page.waitForTimeout(300); + + // Hover State Snapshot + await expect(sidebar).toHaveScreenshot('sidebar-hover-item.png'); + }); +}); diff --git a/apps/web/e2e/visual-complete.spec.ts b/apps/web/e2e/visual-complete.spec.ts new file mode 100644 index 000000000..4604e7b22 --- /dev/null +++ b/apps/web/e2e/visual-complete.spec.ts @@ -0,0 +1,171 @@ +/** + * Suite complète de capture visuelle pour régression pixel-perfect. + * + * - Boucle sur URLs critiques (Login, Dashboard, Playlists, etc.) + * - Auth via storageState pour pages protégées ; pas d'auth pour login/register + * - Full page + screenshots ciblés (Sidebar, Player, Header) + * - waitForStableNetwork, masquage éléments dynamiques (dates, avatars) + * - Nommage : {screen-name}-desktop-dark.png + * + * Sortie : visual-tests/current/ (visual:capture) ou visual-tests/baselines/ (visual:update) + */ + +import { test } from '@playwright/test'; +import fs from 'fs'; +import path from 'path'; +import { visualOutputDir, screenshotName } from '../playwright.config.visual'; + +const BASE_URL = process.env.PLAYWRIGHT_BASE_URL || process.env.VITE_FRONTEND_URL || 'http://localhost:5173'; +const ANIMATION_SETTLE_MS = 800; +const NETWORK_IDLE_MS = 500; + +/** Désactive les animations/transitions CSS pour captures stables */ +async function disableAnimations(page: import('@playwright/test').Page) { + await page.addStyleTag({ + content: ` + *, *::before, *::after { + animation-duration: 0s !important; + animation-delay: 0s !important; + transition-duration: 0s !important; + transition-delay: 0s !important; + } + `, + }); +} + +/** Force le thème sombre sur le document */ +async function ensureDarkTheme(page: import('@playwright/test').Page) { + await page.evaluate(() => { + document.documentElement.classList.add('dark'); + document.documentElement.setAttribute('data-theme', 'dark'); + }); + await page.waitForTimeout(100); +} + +/** Attend réseau inactif puis un court délai pour éviter skeletons / images en cours de chargement */ +async function waitForStableNetwork(page: import('@playwright/test').Page) { + await page.waitForLoadState('networkidle', { timeout: 15000 }).catch(() => {}); + await page.waitForTimeout(NETWORK_IDLE_MS); +} + +/** Locators des éléments dynamiques à masquer (dates, avatars, temps) */ +async function getDynamicMasks(page: import('@playwright/test').Page): Promise { + const candidates = [ + page.locator('img[alt="Avatar"], img[alt*="avatar"]').first(), + page.locator('[role="timer"]').first(), + page.locator('time').first(), + ]; + const out: import('@playwright/test').Locator[] = []; + for (const loc of candidates) { + if ((await loc.count()) > 0) out.push(loc); + } + return out; +} + +/** Écrit un screenshot dans visual-tests/current ou baselines */ +async function saveScreenshot( + page: import('@playwright/test').Page, + name: string, + options: { fullPage?: boolean; locator?: import('@playwright/test').Locator } = {} +) { + fs.mkdirSync(visualOutputDir, { recursive: true }); + const filePath = path.join(visualOutputDir, screenshotName(name)); + const mask = await getDynamicMasks(page); + const screenshotOpts = { path: filePath, mask: mask.length > 0 ? mask : undefined }; + + if (options.locator) { + await options.locator.screenshot(screenshotOpts); + } else { + await page.screenshot({ fullPage: options.fullPage ?? true, ...screenshotOpts }); + } + test.info().attach(name, { path: filePath, contentType: 'image/png' }); +} + +const SCREENS: Array<{ + name: string; + url: string; + auth: boolean; + full: boolean; + locator?: string; +}> = [ + { name: 'login', url: '/login', auth: false, full: true }, + { name: 'register', url: '/register', auth: false, full: true }, + { name: 'dashboard', url: '/dashboard', auth: true, full: true }, + { name: 'playlists', url: '/playlists', auth: true, full: true }, + { name: 'library', url: '/library', auth: true, full: true }, + { name: '404', url: '/non-existent-route-404', auth: false, full: true }, +]; + +const COMPONENT_CAPTURES: Array<{ name: string; url: string; locator: string }> = [ + { name: 'sidebar', url: '/dashboard', locator: '[data-testid="app-sidebar"]' }, + { name: 'header', url: '/dashboard', locator: 'header' }, + { name: 'player', url: '/dashboard', locator: '[data-testid="global-player"]' }, +]; + +test.describe('Visual capture (complete)', () => { + test.beforeEach(async ({ page }) => { + await page.emulateMedia({ reducedMotion: 'reduce', colorScheme: 'dark' }); + }); + + test.describe('Full-page screens (no auth)', () => { + test.use({ storageState: { cookies: [], origins: [] } }); + for (const screen of SCREENS.filter((s) => !s.auth)) { + test(`${screen.name}`, async ({ page }) => { + const url = BASE_URL.replace(/\/$/, '') + screen.url; + await page.goto(url, { waitUntil: 'domcontentloaded', timeout: 20000 }); + await waitForStableNetwork(page); + await page.waitForSelector('body', { timeout: 8000 }).catch(() => {}); + + await disableAnimations(page); + await ensureDarkTheme(page); + await page.waitForTimeout(ANIMATION_SETTLE_MS); + + await saveScreenshot(page, screen.name, { fullPage: true }); + }); + } + }); + + test.describe('Full-page screens (authenticated)', () => { + for (const screen of SCREENS.filter((s) => s.auth)) { + test(`${screen.name}`, async ({ page }) => { + const url = BASE_URL.replace(/\/$/, '') + screen.url; + await page.goto(url, { waitUntil: 'domcontentloaded', timeout: 20000 }); + await waitForStableNetwork(page); + await page.waitForSelector('body', { timeout: 8000 }).catch(() => {}); + + await disableAnimations(page); + await ensureDarkTheme(page); + await page.waitForTimeout(ANIMATION_SETTLE_MS); + + await saveScreenshot(page, screen.name, { fullPage: true }); + }); + } + }); + + test.describe('Component screenshots (authenticated)', () => { + for (const comp of COMPONENT_CAPTURES) { + test(comp.name, async ({ page }) => { + const url = BASE_URL.replace(/\/$/, '') + comp.url; + await page.goto(url, { waitUntil: 'domcontentloaded', timeout: 20000 }); + await waitForStableNetwork(page); + + const loc = page.locator(comp.locator).first(); + const visible = await loc.waitFor({ state: 'visible', timeout: 10000 }).then(() => true).catch(() => false); + if (!visible) { + test.skip(true, `${comp.name} locator not visible`); + return; + } + + await disableAnimations(page); + await ensureDarkTheme(page); + await page.waitForTimeout(ANIMATION_SETTLE_MS); + + const mask = await getDynamicMasks(page); + fs.mkdirSync(visualOutputDir, { recursive: true }); + const filePath = path.join(visualOutputDir, screenshotName(comp.name)); + await loc.screenshot({ path: filePath, mask: mask.length > 0 ? mask : undefined }); + test.info().attach(comp.name, { path: filePath, contentType: 'image/png' }); + }); + } + }); +}); diff --git a/apps/web/eslint.config.js b/apps/web/eslint.config.js index 70775332a..c8eaa1936 100644 --- a/apps/web/eslint.config.js +++ b/apps/web/eslint.config.js @@ -206,6 +206,34 @@ export default [js.configs.recommended, { message: 'Use the Button component from @/components/ui/button instead of native diff --git a/apps/web/src/components/education/CourseCard.tsx b/apps/web/src/components/education/CourseCard.tsx index e96cd69ae..490232809 100644 --- a/apps/web/src/components/education/CourseCard.tsx +++ b/apps/web/src/components/education/CourseCard.tsx @@ -25,14 +25,14 @@ export const CourseCard: React.FC = ({
{course.title}
{course.certificateAvailable && ( -
+
CERTIFIED
)} @@ -45,7 +45,7 @@ export const CourseCard: React.FC = ({
= ({ {/* Progress */}