From d2ae91ac25d1a4961ebac84472b76bd3e29169e3 Mon Sep 17 00:00:00 2001 From: senke Date: Wed, 4 Feb 2026 00:44:40 +0100 Subject: [PATCH] chore(storybook): improve configuration and cleanup --- apps/web/.storybook/main.ts | 1 + apps/web/.storybook/preview.ts | 22 --- apps/web/.storybook/preview.tsx | 33 +++-- apps/web/README.md | 2 +- apps/web/docs/ARCHITECTURE.md | 120 ++++++++++++++++ .../admin/AdminDashboardView.stories.tsx | 9 +- .../admin/AdminModerationView.stories.tsx | 9 +- .../layout/DashboardLayout.stories.tsx | 6 +- .../src/components/layout/Header.stories.tsx | 6 +- .../src/components/layout/Navbar.stories.tsx | 4 +- apps/web/src/components/layout/Navbar.tsx | 8 +- .../playlists/PlaylistsView.stories.tsx | 10 +- .../NotificationMenu.stories.tsx | 10 +- .../settings/account/AccountSettings.tsx | 13 +- .../appearance/AppearanceSettingsView.tsx | 13 +- apps/web/src/components/ui/button.tsx | 14 +- apps/web/src/context/ThemeContext.test.tsx | 60 -------- apps/web/src/context/ThemeContext.tsx | 135 ------------------ .../auth/pages/ForgotPasswordPage.stories.tsx | 24 +--- .../features/auth/pages/LoginPage.stories.tsx | 18 +-- .../auth/pages/RegisterPage.stories.tsx | 25 +--- .../dashboard/pages/DashboardPage.stories.tsx | 26 +--- .../error/pages/NotFoundPage.stories.tsx | 2 + .../error/pages/ServerErrorPage.stories.tsx | 2 + .../components/UploadModal.stories.tsx | 9 ++ .../library/pages/LibraryPage.stories.tsx | 24 ++-- .../pages/NotificationsPage.stories.tsx | 21 ++- .../CreatePlaylistDialog.stories.tsx | 10 +- .../components/PlaylistList.stories.tsx | 9 +- .../pages/PlaylistListPage.stories.tsx | 27 ++-- .../tracks/components/ViewToggle.stories.tsx | 6 +- .../tracks/pages/TrackDetailPage.stories.tsx | 31 ++-- .../marketplace/MarketplaceHome.stories.tsx | 24 ++-- apps/web/src/stories/decorators.tsx | 72 ++++++++++ apps/web/src/utils/toast.ts | 38 ++--- 35 files changed, 398 insertions(+), 445 deletions(-) delete mode 100644 apps/web/.storybook/preview.ts create mode 100644 apps/web/docs/ARCHITECTURE.md delete mode 100644 apps/web/src/context/ThemeContext.test.tsx delete mode 100644 apps/web/src/context/ThemeContext.tsx create mode 100644 apps/web/src/stories/decorators.tsx diff --git a/apps/web/.storybook/main.ts b/apps/web/.storybook/main.ts index 74eda5d7a..b7e0334f3 100644 --- a/apps/web/.storybook/main.ts +++ b/apps/web/.storybook/main.ts @@ -16,6 +16,7 @@ const config: StorybookConfig = { getAbsolutePath('@storybook/addon-a11y'), getAbsolutePath('@storybook/addon-interactions'), ], + "staticDirs": ['../public'], "framework": getAbsolutePath('@storybook/react-vite'), "docs": { "defaultName": "Documentation" diff --git a/apps/web/.storybook/preview.ts b/apps/web/.storybook/preview.ts deleted file mode 100644 index 5fa9e7456..000000000 --- a/apps/web/.storybook/preview.ts +++ /dev/null @@ -1,22 +0,0 @@ -import type { Preview } from '@storybook/react-vite'; -import '../src/index.css'; - -const preview: Preview = { - parameters: { - controls: { - matchers: { - color: /(background|color)$/i, - date: /Date$/i, - }, - }, - - a11y: { - // 'todo' - show a11y violations in the test UI only - // 'error' - fail CI on a11y violations - // 'off' - skip a11y checks entirely - test: 'todo', - }, - }, -}; - -export default preview; \ No newline at end of file diff --git a/apps/web/.storybook/preview.tsx b/apps/web/.storybook/preview.tsx index 82b896ab8..095041f18 100644 --- a/apps/web/.storybook/preview.tsx +++ b/apps/web/.storybook/preview.tsx @@ -1,9 +1,15 @@ -import type { Preview } from '@storybook/react-vite'; +import type { Preview } from '@storybook/react'; import '../src/index.css'; +import '../src/styles/design-system.css'; +import '../src/styles/global-effects.css'; +import '../src/styles/header.css'; +import '../src/lib/i18n'; // Initialize i18n import React from 'react'; +import { ThemeProvider } from '../src/components/theme/ThemeProvider'; import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import { MemoryRouter } from 'react-router-dom'; import { ToastProvider } from '../src/components/feedback/ToastProvider'; +import { AudioProvider } from '../src/context/AudioContext'; // Create a client for stories const queryClient = new QueryClient({ @@ -50,9 +56,6 @@ const preview: Preview = { expanded: true, }, a11y: { - // 'todo' - show a11y violations in the test UI only - // 'error' - fail CI on a11y violations - // 'off' - skip a11y checks entirely test: 'todo', }, viewport: { @@ -77,15 +80,19 @@ const preview: Preview = { // Apply dark class based on background selection const isDark = context.globals.backgrounds?.value !== '#ffffff'; return ( -
- - - - - - - -
+ +
+ + + + + + + + + +
+
); }, ], diff --git a/apps/web/README.md b/apps/web/README.md index 07847b17c..dcb529271 100644 --- a/apps/web/README.md +++ b/apps/web/README.md @@ -156,7 +156,7 @@ See `eslint.config.js` for full rule configuration. ## Documentation -- **Architecture**: See `docs/` directory +- **Architecture Guide**: `docs/ARCHITECTURE.md` (MUST READ) - **Component Usage**: `src/components/COMPONENT_USAGE.md` - **Color Usage**: `src/styles/COLOR_USAGE.md` - **Typography**: `src/styles/TYPOGRAPHY_GUIDE.md` diff --git a/apps/web/docs/ARCHITECTURE.md b/apps/web/docs/ARCHITECTURE.md new file mode 100644 index 000000000..1b13c87c7 --- /dev/null +++ b/apps/web/docs/ARCHITECTURE.md @@ -0,0 +1,120 @@ +# Veza Frontend Architecture Guide + +**Status:** Living Document +**Version:** 2.0 (Post-Audit 2026) + +This document outlines the architectural principles, patterns, and rules that govern the Veza frontend. It supersedes previous ad-hoc audit reports. + +--- + +## 1. Core Philosophy: "Visual First" + +> "If it can't be rendered in Storybook, it is architecturally broken." + +We follow a **Storybook-Driven Development** (SDD) approach. +- **Isolation:** Every component must be renderable in isolation. Dependencies (Providers, Router, Store) must be explicit. +- **Verification:** Storybook is our primary verification tool for UI logic and layout. + +--- + +## 2. State Management Strategy + +We distinguish three types of state. Mixing them is strictly forbidden. + +### 2.1. Server State (Data) -> `React Query` +Data that belongs to the backend (Users, Tracks, Playlists). +- **Tool:** `@tanstack/react-query` +- **Rule:** Never copy server data into a global store (Zustand) unless strictly necessary for client-side manipulation (e.g., a complex audio editor buffer). +- **Caching:** Managed automatically by query keys. + +### 2.2. Client Global State (App) -> `Zustand` +Data that is truly global to the client session (Auth Token, Shopping Cart, Audio Player Status). +- **Tool:** `zustand` +- **Rule:** Use atomic selectors to prevent render-thrashing. +- **Structure:** + - `authStore`: User session. + - `cartStore`: E-commerce state (Items, Total). + - `playerStore`: Audio playback state. + +**Legacy Note:** `React Context` is BANNED for high-frequency state updates. It is reserved for dependency injection (Theme, i18n). + +### 2.3. UI Local State -> `useState` / `useReducer` +Ephemeral state specific to a component (Modal Open/Close, Form Inputs). +- **Rule:** If it doesn't need to persist when navigating away, it stays local. + +--- + +## 3. Component Engineering + +We adhere to the **Smart vs Dumb** (Container vs Presentational) separation to ensure testability. + +### 3.1. Dumb Components (UI) +- **Role:** Render props into HTML. Emit events via callbacks. +- **Dependencies:** ZERO. No explicit side-effects, no API calls, no Context consumers (except Theme). +- **Testing:** Storybook. + +```tsx +// ✅ Correct Dumb Component +export const ProductCard = ({ title, price, onAddToCart }: Props) => ( +
{title} - {price}
+); +``` + +### 3.2. Smart Components (Containers) +- **Role:** Wire data to UI. +- **Dependencies:** Allowed (`useQuery`, `useCartStore`, `useParams`). +- **Testing:** Integration Tests (MSW + Storybook play functions). + +```tsx +// ✅ Correct Smart Component +export const ProductCardContainer = ({ id }) => { + const { data } = useProduct(id); + const addToCart = useCartStore(s => s.addItem); + return addToCart(data)} />; +}; +``` + +--- + +## 4. Design System & Styling + +We use **Tailwind CSS** with a rigorous Design System (Kodo). + +- **Tokens Only:** Do not use arbitrary values (e.g., `w-[350px]`). Use design tokens (`w-sidebar`). +- **Dark Mode:** All UI/Layout components must implement `dark:` variants. +- **Icons:** `lucide-react`. Icons must inherit color via `currentColor`. + +--- + +## 5. Storybook Usage + +Storybook is not optional. It is the definition of "Done". + +### 5.1. Decorators +Use granular decorators from `src/stories/decorators.tsx` instead of global wrapping in `preview.tsx`. +- `withToast`: Injects ToastProvider. +- `withRouter`: Injects MemoryRouter. +- `withStoreState`: Mocks Zustand state. + +### 5.2. Interaction Testing +Critical user flows (e.g., Add to Cart) must have a `.play` function in their story to verify interaction without manual testing. + +--- + +## 6. Testing Pyramid + +1. **Unit (Vitest):** Utilities, Store Reducers, Hooks. +2. **Integration (Storybook + Vitest):** Component wiring, Props interface. +3. **E2E (Playwright):** Critical Paths (Login, Checkout, Signup). + +--- + +## 7. Anti-Patterns (Dos & Don'ts) + +| ❌ Don't | ✅ Do | +| :--- | :--- | +| `useContext(CartContext)` | `useCartStore(selector)` | +| `w-[17px]` | `w-4` or `w-5` (stick to grid) | +| Props Drilling (> 3 levels) | Composition (Slots) or Context (if static) | +| API calls in `useEffect` | `useQuery` | +| `any` type | Generated types from OpenAPI | diff --git a/apps/web/src/components/admin/AdminDashboardView.stories.tsx b/apps/web/src/components/admin/AdminDashboardView.stories.tsx index 0421b058e..2c9555750 100644 --- a/apps/web/src/components/admin/AdminDashboardView.stories.tsx +++ b/apps/web/src/components/admin/AdminDashboardView.stories.tsx @@ -1,5 +1,6 @@ import type { Meta, StoryObj } from '@storybook/react'; import { AdminDashboardView } from './AdminDashboardView'; +import { ToastProvider } from '../../components/feedback/ToastProvider'; /** * AdminDashboardView - Centre de commande admin @@ -21,9 +22,11 @@ const meta: Meta = { tags: ['autodocs'], decorators: [ (Story) => ( -
- -
+ +
+ +
+
), ], }; diff --git a/apps/web/src/components/admin/AdminModerationView.stories.tsx b/apps/web/src/components/admin/AdminModerationView.stories.tsx index beb9969a0..6588ac832 100644 --- a/apps/web/src/components/admin/AdminModerationView.stories.tsx +++ b/apps/web/src/components/admin/AdminModerationView.stories.tsx @@ -1,5 +1,6 @@ import type { Meta, StoryObj } from '@storybook/react'; import { AdminModerationView } from './AdminModerationView'; +import { ToastProvider } from '../../components/feedback/ToastProvider'; /** * AdminModerationView - Queue de modération @@ -21,9 +22,11 @@ const meta: Meta = { tags: ['autodocs'], decorators: [ (Story) => ( -
- -
+ +
+ +
+
), ], }; diff --git a/apps/web/src/components/layout/DashboardLayout.stories.tsx b/apps/web/src/components/layout/DashboardLayout.stories.tsx index e43120c88..fac3332dd 100644 --- a/apps/web/src/components/layout/DashboardLayout.stories.tsx +++ b/apps/web/src/components/layout/DashboardLayout.stories.tsx @@ -2,6 +2,8 @@ import type { Meta, StoryObj } from '@storybook/react'; import { DashboardLayout } from './DashboardLayout'; import { BrowserRouter } from 'react-router-dom'; import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; +import { ToastProvider } from '@/components/feedback/ToastProvider'; + const queryClient = new QueryClient(); const meta = { @@ -12,9 +14,9 @@ const meta = { (Story) => ( - + - + ), diff --git a/apps/web/src/components/layout/Header.stories.tsx b/apps/web/src/components/layout/Header.stories.tsx index a8cca5ed5..ae4fd14f7 100644 --- a/apps/web/src/components/layout/Header.stories.tsx +++ b/apps/web/src/components/layout/Header.stories.tsx @@ -2,6 +2,8 @@ import type { Meta, StoryObj } from '@storybook/react'; import { Header } from './Header'; import { BrowserRouter } from 'react-router-dom'; import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; +import { ToastProvider } from '@/components/feedback/ToastProvider'; + const queryClient = new QueryClient(); const meta = { @@ -12,7 +14,7 @@ const meta = { (Story) => ( - +
{/* Header is fixed, so we need a container that mimics the body */} @@ -20,7 +22,7 @@ const meta = {

Page Content

-
+
), diff --git a/apps/web/src/components/layout/Navbar.stories.tsx b/apps/web/src/components/layout/Navbar.stories.tsx index 2fbabbdad..52822c23b 100644 --- a/apps/web/src/components/layout/Navbar.stories.tsx +++ b/apps/web/src/components/layout/Navbar.stories.tsx @@ -1,6 +1,6 @@ import type { Meta, StoryObj } from '@storybook/react'; import { Navbar } from './Navbar'; -import { ThemeProvider } from '../../context/ThemeContext'; +import { ThemeProvider } from '../theme/ThemeProvider'; const meta: Meta = { @@ -12,7 +12,7 @@ const meta: Meta = { tags: ['autodocs'], decorators: [ (Story) => ( - +
diff --git a/apps/web/src/components/layout/Navbar.tsx b/apps/web/src/components/layout/Navbar.tsx index c83977df0..130a91e82 100644 --- a/apps/web/src/components/layout/Navbar.tsx +++ b/apps/web/src/components/layout/Navbar.tsx @@ -13,7 +13,7 @@ import { import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { useCartStore } from '../../stores/cartStore'; -import { useTheme } from '../../context/ThemeContext'; +import { useTheme } from '../theme/ThemeProvider'; import { Notification } from '../../types'; import { NotificationBell } from '../notifications/NotificationBell'; @@ -52,7 +52,11 @@ const mockNotifications: Notification[] = [ ]; export const Navbar: React.FC = ({ onNavigate, onLogout }) => { - const { toggleTheme, theme } = useTheme(); + const { theme, setTheme } = useTheme(); + + const toggleTheme = () => { + setTheme(theme === 'dark' ? 'light' : 'dark'); + }; // Selector ensures we re-render only when the calculated count changes const itemCount = useCartStore((state) => state.getItemCount()); const [showUserMenu, setShowUserMenu] = useState(false); diff --git a/apps/web/src/components/library/playlists/PlaylistsView.stories.tsx b/apps/web/src/components/library/playlists/PlaylistsView.stories.tsx index cfe6e4472..41262de27 100644 --- a/apps/web/src/components/library/playlists/PlaylistsView.stories.tsx +++ b/apps/web/src/components/library/playlists/PlaylistsView.stories.tsx @@ -1,6 +1,8 @@ import type { Meta, StoryObj } from '@storybook/react'; import { PlaylistsView } from './PlaylistsView'; +import { ToastProvider } from '../../feedback/ToastProvider'; + const meta: Meta = { title: 'Components/Library/Playlists/PlaylistsView', component: PlaylistsView, @@ -8,9 +10,11 @@ const meta: Meta = { tags: ['autodocs'], decorators: [ (Story) => ( -
- -
+ +
+ +
+
), ], }; diff --git a/apps/web/src/components/notifications/NotificationMenu.stories.tsx b/apps/web/src/components/notifications/NotificationMenu.stories.tsx index 48cca0b6b..3bc2296b5 100644 --- a/apps/web/src/components/notifications/NotificationMenu.stories.tsx +++ b/apps/web/src/components/notifications/NotificationMenu.stories.tsx @@ -3,6 +3,8 @@ import { NotificationMenu } from './NotificationMenu'; import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import { BrowserRouter } from 'react-router-dom'; +import { ToastProvider } from '../feedback/ToastProvider'; + const queryClient = new QueryClient(); const meta = { @@ -13,9 +15,11 @@ const meta = { (Story) => ( -
- -
+ +
+ +
+
), diff --git a/apps/web/src/components/settings/account/AccountSettings.tsx b/apps/web/src/components/settings/account/AccountSettings.tsx index 2c1464ccc..60b5e8f3d 100644 --- a/apps/web/src/components/settings/account/AccountSettings.tsx +++ b/apps/web/src/components/settings/account/AccountSettings.tsx @@ -1,7 +1,7 @@ import React, { useState } from 'react'; import { Card } from '../../ui/card'; import { Button } from '../../ui/button'; -import { useTheme } from '../../../context/ThemeContext'; +import { useTheme } from '../../../components/theme/ThemeProvider'; import { ThemeVariant } from '../../../types'; import { Mail, @@ -143,7 +143,16 @@ export const AccountSettings: React.FC = () => { ].map((opt) => (