stabilisation: fix commit

This commit is contained in:
senke 2026-02-03 09:56:11 +01:00
parent 963fdf4f53
commit a2576c4eae
161 changed files with 10147 additions and 365 deletions

2231
STORYBOOK_AUDIT_REPORT.md Normal file

File diff suppressed because it is too large Load diff

562
STORYBOOK_ROADMAP.md Normal file
View file

@ -0,0 +1,562 @@
# 📚 ROADMAP STORYBOOK 100% COVERAGE
**Projet**: Veza Music Platform
**Objectif**: Couverture complète de tous les composants
**Durée**: 12 semaines
**Date de début**: 3 Février 2026
**Date cible de fin**: 27 Avril 2026
---
## 📊 Vue d'Ensemble
| Métrique | Actuel | Cible | Gap |
|----------|--------|-------|-----|
| **Stories** | 164 | 344 | +180 |
| **Composants** | 384 | 384 | - |
| **Couverture** | 42% | 100% | +58% |
| **Heures estimées** | - | 270h | ~23h/semaine |
### Progression Hebdomadaire Ciblée
```
Semaine 1 ▓▓▓▓▓▓▓▓▓▓▓▓░░░░░░░░░░░░░░░░░░░░░░░ 47% (+5%)
Semaine 2 ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓░░░░░░░░░░░░░░░░░░░░ 52% (+5%)
Semaine 3 ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓░░░░░░░░░░░░░░░░░ 57% (+5%)
Semaine 4 ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓░░░░░░░░░░░░░░ 62% (+5%)
Semaine 5 ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓░░░░░░░░░░░ 67% (+5%)
Semaine 6 ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓░░░░░░░░ 72% (+5%)
Semaine 7 ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓░░░░░░ 77% (+5%)
Semaine 8 ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓░░░░ 82% (+5%)
Semaine 9 ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓░░ 87% (+5%)
Semaine 10 ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓░ 92% (+5%)
Semaine 11 ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ 96% (+4%)
Semaine 12 ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ 100% (+4%)
```
---
## 🎯 Milestones
| Milestone | Date | Couverture | Description |
|-----------|------|------------|-------------|
| **M1** | 16 Fév 2026 | 52% | Pages critiques + Admin |
| **M2** | 9 Mars 2026 | 67% | Core Features (Player, Playlists, Tracks) |
| **M3** | 30 Mars 2026 | 82% | Full Features (Upload, Chat, Settings) |
| **M4** | 27 Avr 2026 | 100% | Couverture complète |
---
## 📅 SPRINT 1 - Pages Critiques & Erreurs
**Dates**: 3-9 Février 2026
**Priorité**: 🔴 P0 (Critique)
**Heures**: 18h
**Objectif**: Couvrir toutes les pages d'erreur et d'authentification
### Composants à Créer
| # | Composant | Chemin | Effort | Heures | Variants |
|---|-----------|--------|--------|--------|----------|
| 1 | NotFoundPage | `src/features/error/pages/NotFoundPage.tsx` | 🟢 Low | 1h | Default, WithSuggestions |
| 2 | ServerErrorPage | `src/features/error/pages/ServerErrorPage.tsx` | 🟢 Low | 1h | Default, WithRetry, NetworkError |
| 3 | LoginPage | `src/features/auth/pages/LoginPage.tsx` | 🟡 Medium | 2h | Default, WithError, Loading |
| 4 | RegisterPage | `src/features/auth/pages/RegisterPage.tsx` | 🟡 Medium | 2h | Default, WithError, Success |
| 5 | ForgotPasswordPage | `src/features/auth/pages/ForgotPasswordPage.tsx` | 🟡 Medium | 2h | Default, Sent, Error |
| 6 | ResetPasswordPage | `src/features/auth/pages/ResetPasswordPage.tsx` | 🟡 Medium | 2h | Default, Success, InvalidToken |
| 7 | VerifyEmailPage | `src/features/auth/pages/VerifyEmailPage.tsx` | 🟢 Low | 1.5h | Pending, Verified, Error |
| 8 | ForgotPasswordForm | `src/features/auth/components/ForgotPasswordForm.tsx` | 🟡 Medium | 1.5h | Default, Loading, Success |
| 9 | TwoFactorVerify | `src/features/auth/components/TwoFactorVerify.tsx` | 🟡 Medium | 2h | Default, Error, Loading |
| 10 | TwoFactorSetup | `src/features/auth/components/TwoFactorSetup.tsx` | 🟡 Medium | 2h | QRCode, Verification, Complete |
| 11 | AuthErrorMessage | `src/features/auth/components/AuthErrorMessage.tsx` | 🟢 Low | 1h | Default, Network, Validation |
### Checklist Sprint 1
- [ ] NotFoundPage.stories.tsx
- [ ] ServerErrorPage.stories.tsx
- [ ] LoginPage.stories.tsx
- [ ] RegisterPage.stories.tsx
- [ ] ForgotPasswordPage.stories.tsx
- [ ] ResetPasswordPage.stories.tsx
- [ ] VerifyEmailPage.stories.tsx
- [ ] ForgotPasswordForm.stories.tsx
- [ ] TwoFactorVerify.stories.tsx
- [ ] TwoFactorSetup.stories.tsx
- [ ] AuthErrorMessage.stories.tsx
**Couverture attendue**: 42% → 47%
---
## 📅 SPRINT 2 - Dashboard & Admin
**Dates**: 10-16 Février 2026
**Priorité**: 🔴 P0 (Critique)
**Heures**: 20h
**Objectif**: Couvrir le dashboard et toutes les vues admin
### Composants à Créer
| # | Composant | Chemin | Effort | Heures | Variants |
|---|-----------|--------|--------|--------|----------|
| 1 | DashboardPage | `src/features/dashboard/pages/DashboardPage.tsx` | 🔴 High | 3h | Default, Loading, Empty |
| 2 | AdminDashboardView | `src/components/admin/AdminDashboardView.tsx` | 🔴 High | 3h | Default, Loading |
| 3 | AdminUsersView | `src/components/admin/AdminUsersView.tsx` | 🔴 High | 3h | Default, Empty, Loading, WithFilters |
| 4 | AdminModerationView | `src/components/admin/AdminModerationView.tsx` | 🔴 High | 3h | Default, Queue, Empty |
| 5 | AdminSettingsView | `src/components/admin/AdminSettingsView.tsx` | 🟡 Medium | 2h | Default, Saving |
| 6 | AdminAuditLogsView | `src/components/admin/AdminAuditLogsView.tsx` | 🟡 Medium | 2h | Default, Filtered, Empty |
| 7 | AdminView | `src/components/views/AdminView.tsx` | 🟡 Medium | 2h | Default |
| 8 | BanUserModal | `src/components/admin/BanUserModal.tsx` | 🟡 Medium | 1.5h | Default, Confirm |
| 9 | UserTableRow | `src/components/admin/UserTableRow.tsx` | 🟢 Low | 1h | Default, Selected, Banned |
### Checklist Sprint 2
- [ ] DashboardPage.stories.tsx
- [ ] AdminDashboardView.stories.tsx
- [ ] AdminUsersView.stories.tsx
- [ ] AdminModerationView.stories.tsx
- [ ] AdminSettingsView.stories.tsx
- [ ] AdminAuditLogsView.stories.tsx
- [ ] AdminView.stories.tsx
- [ ] BanUserModal.stories.tsx
- [ ] UserTableRow.stories.tsx
**Couverture attendue**: 47% → 52%
---
## 📅 SPRINT 3 - Player & Playback
**Dates**: 17-23 Février 2026
**Priorité**: 🟠 P1 (Haute)
**Heures**: 18h
**Objectif**: Compléter la couverture du player
### Composants à Créer
| # | Composant | Chemin | Effort | Heures | Variants |
|---|-----------|--------|--------|--------|----------|
| 1 | AudioPlayer | `src/features/player/components/AudioPlayer.tsx` | 🔴 High | 3h | Playing, Paused, Loading, Error |
| 2 | FullPlayer | `src/components/player/FullPlayer.tsx` | 🔴 High | 3h | Default, WithQueue, WithLyrics |
| 3 | PlayerError | `src/features/player/components/PlayerError.tsx` | 🟢 Low | 1h | NetworkError, FormatError, Generic |
| 4 | PlaybackSpeedControl | `src/features/player/components/PlaybackSpeedControl.tsx` | 🟢 Low | 1h | Default, 1x, 1.5x, 2x |
| 5 | QueuePanel | `src/components/player/QueuePanel.tsx` | 🟡 Medium | 2h | Default, Empty, Reordering |
| 6 | QueueView | `src/components/views/QueueView.tsx` | 🟡 Medium | 2h | Default, Empty |
| 7 | SaveQueueAsPlaylistModal | `src/components/player/SaveQueueAsPlaylistModal.tsx` | 🟡 Medium | 1.5h | Default, Saving, Success |
| 8 | LyricsPanel | `src/components/studio/LyricsPanel.tsx` | 🟡 Medium | 1.5h | Default, Synced, Empty |
### Checklist Sprint 3
- [ ] AudioPlayer.stories.tsx
- [ ] FullPlayer.stories.tsx
- [ ] PlayerError.stories.tsx
- [ ] PlaybackSpeedControl.stories.tsx
- [ ] QueuePanel.stories.tsx
- [ ] QueueView.stories.tsx
- [ ] SaveQueueAsPlaylistModal.stories.tsx
- [ ] LyricsPanel.stories.tsx
**Couverture attendue**: 52% → 57%
---
## 📅 SPRINT 4 - Playlists Complet
**Dates**: 24 Février - 2 Mars 2026
**Priorité**: 🟠 P1 (Haute)
**Heures**: 20h
**Objectif**: 100% couverture feature playlists
### Composants à Créer
| # | Composant | Chemin | Effort | Heures | Variants |
|---|-----------|--------|--------|--------|----------|
| 1 | PlaylistList | `src/features/playlists/components/PlaylistList.tsx` | 🔴 High | 2.5h | Default, Grid, Empty, Loading |
| 2 | PlaylistDetailPage | `src/features/playlists/pages/PlaylistDetailPage.tsx` | 🔴 High | 3h | Default, Loading, NotFound |
| 3 | PlaylistListPage | `src/features/playlists/pages/PlaylistListPage.tsx` | 🔴 High | 2.5h | Default, Empty, Loading |
| 4 | PlaylistTrackList | `src/features/playlists/components/PlaylistTrackList.tsx` | 🔴 High | 2.5h | Default, Empty, Reordering |
| 5 | PlaylistTrackItem | `src/features/playlists/components/PlaylistTrackItem.tsx` | 🟡 Medium | 1.5h | Default, Playing, Selected |
| 6 | PlaylistSearch | `src/features/playlists/components/PlaylistSearch.tsx` | 🟡 Medium | 1.5h | Default, WithResults |
| 7 | PlaylistRecommendations | `src/features/playlists/components/PlaylistRecommendations.tsx` | 🟡 Medium | 1.5h | Default, Empty |
| 8 | AddCollaboratorModal | `src/features/playlists/components/AddCollaboratorModal.tsx` | 🟡 Medium | 1.5h | Default, Searching, Added |
| 9 | CollaboratorList | `src/features/playlists/components/CollaboratorList.tsx` | 🟢 Low | 1h | Default, Empty |
| 10 | SharePlaylistModal | `src/features/playlists/components/SharePlaylistModal.tsx` | 🟡 Medium | 1.5h | Default, Copied |
### Checklist Sprint 4
- [ ] PlaylistList.stories.tsx
- [ ] PlaylistDetailPage.stories.tsx
- [ ] PlaylistListPage.stories.tsx
- [ ] PlaylistTrackList.stories.tsx
- [ ] PlaylistTrackItem.stories.tsx
- [ ] PlaylistSearch.stories.tsx
- [ ] PlaylistRecommendations.stories.tsx
- [ ] AddCollaboratorModal.stories.tsx
- [ ] CollaboratorList.stories.tsx
- [ ] SharePlaylistModal.stories.tsx
**Couverture attendue**: 57% → 62%
---
## 📅 SPRINT 5 - Tracks & Search
**Dates**: 3-9 Mars 2026
**Priorité**: 🟠 P1 (Haute)
**Heures**: 18h
**Objectif**: Compléter tracks et recherche
### Composants à Créer
| # | Composant | Chemin | Effort | Heures | Variants |
|---|-----------|--------|--------|--------|----------|
| 1 | TrackDetailPage | `src/features/tracks/pages/TrackDetailPage.tsx` | 🔴 High | 3h | Default, Loading, NotFound |
| 2 | TrackSearch | `src/features/tracks/components/TrackSearch.tsx` | 🔴 High | 2.5h | Default, WithResults, NoResults |
| 3 | TrackSearchResults | `src/features/tracks/components/TrackSearchResults.tsx` | 🔴 High | 2h | Default, Empty, Loading |
| 4 | TrackSearchFilters | `src/features/tracks/components/TrackSearchFilters.tsx` | 🟡 Medium | 1.5h | Default, Applied |
| 5 | TrackListContainer | `src/features/tracks/components/TrackListContainer.tsx` | 🟡 Medium | 1.5h | Default |
| 6 | TrackAnalyticsView | `src/components/tracks/TrackAnalyticsView.tsx` | 🔴 High | 2.5h | Default, Loading, Empty |
| 7 | SearchPage | `src/features/search/pages/SearchPage.tsx` | 🔴 High | 3h | Default, Results, NoResults, Loading |
| 8 | GlobalSearchBar | `src/components/search/GlobalSearchBar.tsx` | 🟡 Medium | 2h | Default, Focused, WithSuggestions |
### Checklist Sprint 5
- [ ] TrackDetailPage.stories.tsx
- [ ] TrackSearch.stories.tsx
- [ ] TrackSearchResults.stories.tsx
- [ ] TrackSearchFilters.stories.tsx
- [ ] TrackListContainer.stories.tsx
- [ ] TrackAnalyticsView.stories.tsx
- [ ] SearchPage.stories.tsx
- [ ] GlobalSearchBar.stories.tsx
**Couverture attendue**: 62% → 67%
---
## 📅 SPRINT 6 - Upload & Library
**Dates**: 10-16 Mars 2026
**Priorité**: 🟠 P1 (Haute)
**Heures**: 22h
**Objectif**: Workflow upload et bibliothèque
### Composants à Créer
| # | Composant | Chemin | Effort | Heures | Variants |
|---|-----------|--------|--------|--------|----------|
| 1 | UploadView | `src/components/views/UploadView.tsx` | 🔴 High | 3h | Default, Uploading, Complete, Error |
| 2 | UploadProgressBar | `src/components/upload/UploadProgressBar.tsx` | 🟢 Low | 1h | Default, Complete, Error |
| 3 | FileUploadZone | `src/components/upload/FileUploadZone.tsx` | 🟡 Medium | 2h | Default, Dragging, Processing |
| 4 | BulkUploadModal | `src/components/upload/BulkUploadModal.tsx` | 🔴 High | 2.5h | Default, Uploading, Complete |
| 5 | MetadataEditor | `src/components/upload/MetadataEditor.tsx` | 🔴 High | 2.5h | Default, WithData, Saving |
| 6 | MetadataForm | `src/components/upload/metadata/MetadataForm.tsx` | 🟡 Medium | 2h | Default, WithErrors |
| 7 | CoverArtUploadModal | `src/components/upload/CoverArtUploadModal.tsx` | 🟡 Medium | 1.5h | Default, Uploading, Preview |
| 8 | LibraryPage | `src/features/library/pages/LibraryPage.tsx` | 🔴 High | 2.5h | Default, Empty, Loading |
### Checklist Sprint 6
- [ ] UploadView.stories.tsx
- [ ] UploadProgressBar.stories.tsx
- [ ] FileUploadZone.stories.tsx
- [ ] BulkUploadModal.stories.tsx
- [ ] MetadataEditor.stories.tsx
- [ ] MetadataForm.stories.tsx
- [ ] CoverArtUploadModal.stories.tsx
- [ ] LibraryPage.stories.tsx
**Couverture attendue**: 67% → 72%
---
## 📅 SPRINT 7 - Chat & Social
**Dates**: 17-23 Mars 2026
**Priorité**: 🟡 P2 (Moyenne)
**Heures**: 18h
**Objectif**: Features chat et social
### Composants à Créer
| # | Composant | Chemin | Effort | Heures | Variants |
|---|-----------|--------|--------|--------|----------|
| 1 | ChatPage | `src/features/chat/pages/ChatPage.tsx` | 🔴 High | 3h | Default, Loading, Empty |
| 2 | ChatView | `src/components/views/ChatView.tsx` | 🟡 Medium | 2h | Default |
| 3 | CreateRoomDialog | `src/features/chat/components/CreateRoomDialog.tsx` | 🟡 Medium | 1.5h | Default, Creating |
| 4 | MessageSearch | `src/features/chat/components/MessageSearch.tsx` | 🟡 Medium | 1.5h | Default, Results, NoResults |
| 5 | VirtualizedChatMessages | `src/features/chat/components/VirtualizedChatMessages.tsx` | 🔴 High | 2h | Default, Loading |
| 6 | SocialView | `src/components/views/SocialView.tsx` | 🟡 Medium | 2h | Default |
| 7 | ConnectionsView | `src/components/social/ConnectionsView.tsx` | 🟡 Medium | 1.5h | Default, Empty |
| 8 | FeedView | `src/components/views/FeedView.tsx` | 🔴 High | 2h | Default, Empty, Loading |
| 9 | CreatePostModal | `src/components/social/CreatePostModal.tsx` | 🟡 Medium | 1.5h | Default, Posting |
### Checklist Sprint 7
- [ ] ChatPage.stories.tsx
- [ ] ChatView.stories.tsx
- [ ] CreateRoomDialog.stories.tsx
- [ ] MessageSearch.stories.tsx
- [ ] VirtualizedChatMessages.stories.tsx
- [ ] SocialView.stories.tsx
- [ ] ConnectionsView.stories.tsx
- [ ] FeedView.stories.tsx
- [ ] CreatePostModal.stories.tsx
**Couverture attendue**: 72% → 77%
---
## 📅 SPRINT 8 - Settings & Preferences
**Dates**: 24-30 Mars 2026
**Priorité**: 🟡 P2 (Moyenne)
**Heures**: 20h
**Objectif**: Tous les panneaux de paramètres
### Composants à Créer
| # | Composant | Chemin | Effort | Heures | Variants |
|---|-----------|--------|--------|--------|----------|
| 1 | SettingsPage | `src/features/settings/pages/SettingsPage.tsx` | 🟡 Medium | 2h | Default |
| 2 | SettingsView | `src/components/views/SettingsView.tsx` | 🟡 Medium | 1.5h | Default |
| 3 | SecuritySettings | `src/components/settings/SecuritySettings.tsx` | 🔴 High | 2.5h | Default, Updating |
| 4 | SessionManagement | `src/components/settings/SessionManagement.tsx` | 🟡 Medium | 2h | Default, WithSessions |
| 5 | AppearanceSettingsView | `src/components/settings/AppearanceSettingsView.tsx` | 🟡 Medium | 1.5h | Default |
| 6 | AccessibilitySettingsView | `src/components/views/AccessibilitySettingsView.tsx` | 🟡 Medium | 1.5h | Default |
| 7 | ChangeEmailModal | `src/components/settings/ChangeEmailModal.tsx` | 🟡 Medium | 1.5h | Default, Sending, Sent |
| 8 | ChangeUsernameModal | `src/components/settings/ChangeUsernameModal.tsx` | 🟡 Medium | 1.5h | Default, Checking, Available |
| 9 | DeleteAccountView | `src/components/settings/DeleteAccountView.tsx` | 🟡 Medium | 1.5h | Default, Confirm |
| 10 | DataExportView | `src/components/settings/DataExportView.tsx` | 🟡 Medium | 1.5h | Default, Exporting, Ready |
### Checklist Sprint 8
- [ ] SettingsPage.stories.tsx
- [ ] SettingsView.stories.tsx
- [ ] SecuritySettings.stories.tsx
- [ ] SessionManagement.stories.tsx
- [ ] AppearanceSettingsView.stories.tsx
- [ ] AccessibilitySettingsView.stories.tsx
- [ ] ChangeEmailModal.stories.tsx
- [ ] ChangeUsernameModal.stories.tsx
- [ ] DeleteAccountView.stories.tsx
- [ ] DataExportView.stories.tsx
**Couverture attendue**: 77% → 82%
---
## 📅 SPRINT 9 - Marketplace & Commerce
**Dates**: 31 Mars - 6 Avril 2026
**Priorité**: 🟡 P2 (Moyenne)
**Heures**: 22h
**Objectif**: Marketplace et commerce complet
### Composants à Créer
| # | Composant | Chemin | Effort | Heures | Variants |
|---|-----------|--------|--------|--------|----------|
| 1 | MarketplaceView | `src/components/views/MarketplaceView.tsx` | 🔴 High | 2.5h | Default, Loading |
| 2 | MarketplaceHome | `src/pages/marketplace/MarketplaceHome.tsx` | 🔴 High | 2.5h | Default, Loading |
| 3 | ProductDetailView | `src/components/marketplace/ProductDetailView.tsx` | 🔴 High | 2.5h | Default, Loading, OutOfStock |
| 4 | CartView | `src/components/views/CartView.tsx` | 🔴 High | 2h | Default, Empty |
| 5 | CartItem | `src/components/commerce/CartItem.tsx` | 🟢 Low | 1h | Default, Removing |
| 6 | CheckoutView | `src/components/views/CheckoutView.tsx` | 🔴 High | 3h | Default, Processing, Success |
| 7 | OrderSummary | `src/components/commerce/OrderSummary.tsx` | 🟡 Medium | 1.5h | Default, WithDiscount |
| 8 | LicenceCard | `src/components/commerce/LicenceCard.tsx` | 🟡 Medium | 1.5h | Basic, Pro, Exclusive |
| 9 | PurchasesView | `src/components/views/PurchasesView.tsx` | 🟡 Medium | 2h | Default, Empty |
### Checklist Sprint 9
- [ ] MarketplaceView.stories.tsx
- [ ] MarketplaceHome.stories.tsx
- [ ] ProductDetailView.stories.tsx
- [ ] CartView.stories.tsx
- [ ] CartItem.stories.tsx
- [ ] CheckoutView.stories.tsx
- [ ] OrderSummary.stories.tsx
- [ ] LicenceCard.stories.tsx
- [ ] PurchasesView.stories.tsx
**Couverture attendue**: 82% → 87%
---
## 📅 SPRINT 10 - Views & Layouts
**Dates**: 7-13 Avril 2026
**Priorité**: 🟡 P2 (Moyenne)
**Heures**: 18h
**Objectif**: Vues restantes
### Composants à Créer
| # | Composant | Chemin | Effort | Heures | Variants |
|---|-----------|--------|--------|--------|----------|
| 1 | DiscoverView | `src/components/views/DiscoverView.tsx` | 🔴 High | 2.5h | Default, Loading |
| 2 | ExploreView | `src/components/views/ExploreView.tsx` | 🔴 High | 2.5h | Default |
| 3 | ProfileView | `src/components/views/ProfileView.tsx` | 🔴 High | 2.5h | Default, Loading |
| 4 | UserProfilePage | `src/features/profile/pages/UserProfilePage.tsx` | 🔴 High | 2.5h | Default, Own, NotFound |
| 5 | NotificationsPage | `src/features/notifications/pages/NotificationsPage.tsx` | 🟡 Medium | 2h | Default, Empty |
| 6 | NotificationsView | `src/components/views/NotificationsView.tsx` | 🟡 Medium | 1.5h | Default |
| 7 | PlaylistsView | `src/components/views/PlaylistsView.tsx` | 🟡 Medium | 1.5h | Default |
| 8 | AnalyticsView | `src/components/views/AnalyticsView.tsx` | 🔴 High | 2h | Default, Loading |
### Checklist Sprint 10
- [ ] DiscoverView.stories.tsx
- [ ] ExploreView.stories.tsx
- [ ] ProfileView.stories.tsx
- [ ] UserProfilePage.stories.tsx
- [ ] NotificationsPage.stories.tsx
- [ ] NotificationsView.stories.tsx
- [ ] PlaylistsView.stories.tsx
- [ ] AnalyticsView.stories.tsx
**Couverture attendue**: 87% → 92%
---
## 📅 SPRINT 11 - Studio & Education
**Dates**: 14-20 Avril 2026
**Priorité**: 🟢 P3 (Basse)
**Heures**: 20h
**Objectif**: Studio et éducation
### Composants à Créer
| # | Composant | Chemin | Effort | Heures | Variants |
|---|-----------|--------|--------|--------|----------|
| 1 | StudioView | `src/components/views/StudioView.tsx` | 🔴 High | 2.5h | Default |
| 2 | ProjectDetailView | `src/components/studio/ProjectDetailView.tsx` | 🔴 High | 2.5h | Default, Loading |
| 3 | CreateProjectModal | `src/components/studio/CreateProjectModal.tsx` | 🟡 Medium | 1.5h | Default, Creating |
| 4 | GoLiveView | `src/components/studio/GoLiveView.tsx` | 🔴 High | 2.5h | Setup, Live, Ended |
| 5 | LyricsEditorModal | `src/components/studio/LyricsEditorModal.tsx` | 🟡 Medium | 1.5h | Default, Syncing |
| 6 | EducationView | `src/components/views/EducationView.tsx` | 🟡 Medium | 2h | Default |
| 7 | CourseDetailView | `src/components/education/CourseDetailView.tsx` | 🔴 High | 2h | Default, Enrolled |
| 8 | CourseLearningView | `src/components/education/CourseLearningView.tsx` | 🔴 High | 2.5h | Default, Complete |
| 9 | LiveView | `src/components/views/LiveView.tsx` | 🟡 Medium | 2h | Default |
### Checklist Sprint 11
- [ ] StudioView.stories.tsx
- [ ] ProjectDetailView.stories.tsx
- [ ] CreateProjectModal.stories.tsx
- [ ] GoLiveView.stories.tsx
- [ ] LyricsEditorModal.stories.tsx
- [ ] EducationView.stories.tsx
- [ ] CourseDetailView.stories.tsx
- [ ] CourseLearningView.stories.tsx
- [ ] LiveView.stories.tsx
**Couverture attendue**: 92% → 96%
---
## 📅 SPRINT 12 - Sprint Final - 100%
**Dates**: 21-27 Avril 2026
**Priorité**: 🟢 P3 (Basse)
**Heures**: 25h
**Objectif**: Tous les composants restants
### Composants à Créer
| # | Composant | Chemin | Effort | Heures | Variants |
|---|-----------|--------|--------|--------|----------|
| 1 | GearView | `src/components/views/GearView.tsx` | 🟡 Medium | 1.5h | Default |
| 2 | EquipmentDetailView | `src/components/inventory/EquipmentDetailView.tsx` | 🟡 Medium | 1.5h | Default |
| 3 | AchievementsView | `src/components/gamification/AchievementsView.tsx` | 🟡 Medium | 1.5h | Default, Empty |
| 4 | AchievementCard | `src/components/gamification/AchievementCard.tsx` | 🟢 Low | 1h | Locked, Unlocked |
| 5 | LeaderboardView | `src/components/gamification/LeaderboardView.tsx` | 🟡 Medium | 1.5h | Default |
| 6 | XPBar | `src/components/gamification/XPBar.tsx` | 🟢 Low | 1h | Default, LevelUp |
| 7 | DeveloperDashboardView | `src/components/developer/DeveloperDashboardView.tsx` | 🔴 High | 2h | Default |
| 8 | APIPlaygroundView | `src/components/developer/APIPlaygroundView.tsx` | 🔴 High | 2.5h | Default |
| 9 | CreateAPIKeyModal | `src/components/developer/CreateAPIKeyModal.tsx` | 🟡 Medium | 1.5h | Default, Created |
| 10 | WebhooksView | `src/features/webhooks/WebhooksView.tsx` | 🔴 High | 2h | Default, Empty |
| 11 | MonitoringDashboard | `src/components/monitoring/MonitoringDashboard.tsx` | 🔴 High | 2.5h | Default |
| 12 | WishlistView | `src/components/views/WishlistView.tsx` | 🟡 Medium | 1.5h | Default, Empty |
| 13 | FileManagerView | `src/components/views/FileManagerView.tsx` | 🔴 High | 2h | Default |
### Checklist Sprint 12
- [ ] GearView.stories.tsx
- [ ] EquipmentDetailView.stories.tsx
- [ ] AchievementsView.stories.tsx
- [ ] AchievementCard.stories.tsx
- [ ] LeaderboardView.stories.tsx
- [ ] XPBar.stories.tsx
- [ ] DeveloperDashboardView.stories.tsx
- [ ] APIPlaygroundView.stories.tsx
- [ ] CreateAPIKeyModal.stories.tsx
- [ ] WebhooksView.stories.tsx
- [ ] MonitoringDashboard.stories.tsx
- [ ] WishlistView.stories.tsx
- [ ] FileManagerView.stories.tsx
**Couverture attendue**: 96% → 100% 🎉
---
## 📊 Tableau de Suivi Global
| Semaine | Sprint | Stories Ajoutées | Couverture | Status |
|---------|--------|------------------|------------|--------|
| 1 | Critical Pages | 11 | 47% | ⬜ À faire |
| 2 | Dashboard & Admin | 9 | 52% | ⬜ À faire |
| 3 | Player & Playback | 8 | 57% | ⬜ À faire |
| 4 | Playlists Complete | 10 | 62% | ⬜ À faire |
| 5 | Tracks & Search | 8 | 67% | ⬜ À faire |
| 6 | Upload & Library | 8 | 72% | ⬜ À faire |
| 7 | Chat & Social | 9 | 77% | ⬜ À faire |
| 8 | Settings & Preferences | 10 | 82% | ⬜ À faire |
| 9 | Marketplace & Commerce | 9 | 87% | ⬜ À faire |
| 10 | Views & Layouts | 8 | 92% | ⬜ À faire |
| 11 | Studio & Education | 9 | 96% | ⬜ À faire |
| 12 | Final Sprint | 13 | 100% | ⬜ À faire |
---
## 🛠️ Commandes Utiles
### Vérifier la progression
```bash
# Compter les stories actuelles
find src -name "*.stories.tsx" | wc -l
# Vérifier la couverture
./scripts/storybook-coverage.sh
# Lister les composants sans stories
./scripts/storybook-coverage.sh 2>/dev/null | grep "^ -"
```
### Générer une story
```bash
# Script de génération automatique
./scripts/generate-story.sh src/components/views/UploadView.tsx
```
### Build Storybook
```bash
npm run storybook # Développement
npm run build-storybook # Production
```
---
## 📝 Notes
- **Effort estimé**: Basé sur la complexité du composant et le nombre de variants
- **Priorité**: P0 (Critique) → P3 (Basse)
- **Heures**: Incluent le temps de test et de revue
- **Variants**: Nombre minimum recommandé
---
*Document généré le 2 Février 2026*
*Fichier JSON associé: `storybook-roadmap.json`*

View file

@ -12,8 +12,20 @@ const config: StorybookConfig = {
"../src/**/*.stories.@(js|jsx|mjs|ts|tsx)" "../src/**/*.stories.@(js|jsx|mjs|ts|tsx)"
], ],
"addons": [ "addons": [
getAbsolutePath('@storybook/addon-essentials') getAbsolutePath('@storybook/addon-essentials'),
getAbsolutePath('@storybook/addon-a11y'),
getAbsolutePath('@storybook/addon-interactions'),
], ],
"framework": getAbsolutePath('@storybook/react-vite') "framework": getAbsolutePath('@storybook/react-vite'),
"docs": {
"defaultName": "Documentation"
},
"typescript": {
"reactDocgen": "react-docgen-typescript",
"reactDocgenTypescriptOptions": {
"shouldExtractLiteralValuesFromEnum": true,
"propFilter": (prop) => (prop.parent ? !/node_modules/.test(prop.parent.fileName) : true),
},
},
}; };
export default config; export default config;

View file

@ -0,0 +1,95 @@
import type { Preview } from '@storybook/react-vite';
import '../src/index.css';
import React from 'react';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { MemoryRouter } from 'react-router-dom';
import { ToastProvider } from '../src/components/feedback/ToastProvider';
// Create a client for stories
const queryClient = new QueryClient({
defaultOptions: {
queries: {
retry: false,
staleTime: Infinity,
},
},
});
// Custom viewports for responsive testing
const customViewports = {
mobile: {
name: 'Mobile',
styles: {
width: '375px',
height: '667px',
},
},
tablet: {
name: 'Tablet',
styles: {
width: '768px',
height: '1024px',
},
},
desktop: {
name: 'Desktop',
styles: {
width: '1440px',
height: '900px',
},
},
};
const preview: Preview = {
parameters: {
controls: {
matchers: {
color: /(background|color)$/i,
date: /Date$/i,
},
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: {
viewports: customViewports,
},
backgrounds: {
default: 'dark',
values: [
{ name: 'dark', value: '#0a0a0a' },
{ name: 'light', value: '#ffffff' },
{ name: 'steel', value: '#1a1a2e' },
],
},
layout: 'centered',
docs: {
toc: true, // Enable table of contents in docs
},
},
decorators: [
// Global providers decorator
(Story, context) => {
// Apply dark class based on background selection
const isDark = context.globals.backgrounds?.value !== '#ffffff';
return (
<div className={isDark ? 'dark' : ''}>
<QueryClientProvider client={queryClient}>
<ToastProvider>
<MemoryRouter>
<Story />
</MemoryRouter>
</ToastProvider>
</QueryClientProvider>
</div>
);
},
],
tags: ['autodocs'],
};
export default preview;

View file

@ -81,6 +81,7 @@
"@lhci/cli": "^0.12.0", "@lhci/cli": "^0.12.0",
"@openapitools/openapi-generator-cli": "^2.27.0", "@openapitools/openapi-generator-cli": "^2.27.0",
"@playwright/test": "^1.41.2", "@playwright/test": "^1.41.2",
"@storybook/addon-a11y": "^8.6.15",
"@storybook/addon-essentials": "^8.6.15", "@storybook/addon-essentials": "^8.6.15",
"@storybook/addon-interactions": "^8.6.15", "@storybook/addon-interactions": "^8.6.15",
"@storybook/builder-vite": "^8.6.15", "@storybook/builder-vite": "^8.6.15",
@ -117,6 +118,7 @@
"playwright": "^1.58.1", "playwright": "^1.58.1",
"prettier": "^3.2.5", "prettier": "^3.2.5",
"storybook": "^8.6.15", "storybook": "^8.6.15",
"storybook-dark-mode": "^4.0.2",
"tailwindcss": "^4.0.0", "tailwindcss": "^4.0.0",
"tw-animate-css": "^1.4.0", "tw-animate-css": "^1.4.0",
"typescript": "^5.3.3", "typescript": "^5.3.3",

View file

@ -0,0 +1,98 @@
#!/bin/bash
# Storybook Coverage Analysis Script
# Compares tsx components to stories and generates a coverage report
set -e
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
WEB_DIR="$(dirname "$SCRIPT_DIR")"
SRC_DIR="$WEB_DIR/src"
echo "📊 Storybook Coverage Analysis"
echo "=============================="
echo ""
# Count all component files (excluding tests and stories)
COMPONENT_COUNT=$(find "$SRC_DIR" -name "*.tsx" \
! -name "*.stories.tsx" \
! -name "*.test.tsx" \
! -path "*/__tests__/*" \
! -path "*/test-utils/*" \
! -name "main.tsx" \
! -name "index.tsx" \
! -name "routes.tsx" \
! -name "App.tsx" \
| wc -l | tr -d ' ')
# Count story files
STORY_COUNT=$(find "$SRC_DIR" -name "*.stories.tsx" | wc -l | tr -d ' ')
# Calculate coverage percentage
if [ "$COMPONENT_COUNT" -gt 0 ]; then
COVERAGE=$((STORY_COUNT * 100 / COMPONENT_COUNT))
else
COVERAGE=0
fi
echo "📁 Total Components: $COMPONENT_COUNT"
echo "📖 Total Stories: $STORY_COUNT"
echo "📈 Coverage: ${COVERAGE}%"
echo ""
# Find components without stories
echo "🔍 Components WITHOUT stories:"
echo "------------------------------"
# Get list of component basenames
COMPONENTS=$(find "$SRC_DIR" -name "*.tsx" \
! -name "*.stories.tsx" \
! -name "*.test.tsx" \
! -path "*/__tests__/*" \
! -path "*/test-utils/*" \
! -name "main.tsx" \
! -name "index.tsx" \
! -name "routes.tsx" \
! -name "App.tsx" \
-exec basename {} .tsx \; | sort | uniq)
# Get list of story basenames
STORIES=$(find "$SRC_DIR" -name "*.stories.tsx" -exec basename {} .stories.tsx \; | sort | uniq)
# Find components without stories
MISSING=0
for component in $COMPONENTS; do
if ! echo "$STORIES" | grep -qx "$component"; then
# Check if it's likely a component (starts with uppercase)
if [[ "$component" =~ ^[A-Z] ]]; then
echo " - $component"
MISSING=$((MISSING + 1))
fi
fi
done
echo ""
echo "📉 Components missing stories: $MISSING"
echo ""
# Coverage by directory
echo "📂 Coverage by Directory:"
echo "-------------------------"
for dir in components features; do
if [ -d "$SRC_DIR/$dir" ]; then
DIR_COMPONENTS=$(find "$SRC_DIR/$dir" -name "*.tsx" \
! -name "*.stories.tsx" \
! -name "*.test.tsx" \
| wc -l | tr -d ' ')
DIR_STORIES=$(find "$SRC_DIR/$dir" -name "*.stories.tsx" | wc -l | tr -d ' ')
if [ "$DIR_COMPONENTS" -gt 0 ]; then
DIR_COVERAGE=$((DIR_STORIES * 100 / DIR_COMPONENTS))
else
DIR_COVERAGE=0
fi
printf " %-15s %3d/%3d (%d%%)\n" "$dir:" "$DIR_STORIES" "$DIR_COMPONENTS" "$DIR_COVERAGE"
fi
done
echo ""
echo "✅ Analysis complete!"

View file

@ -0,0 +1,55 @@
import type { Meta, StoryObj } from '@storybook/react';
import { ErrorBoundary } from './ErrorBoundary';
// Component that throws an error for testing
const ErrorThrower = ({ shouldThrow }: { shouldThrow?: boolean }) => {
if (shouldThrow) {
throw new Error('This is a test error for demonstrating ErrorBoundary');
}
return <div className="p-4 bg-green-100 text-green-800 rounded">Content rendered successfully!</div>;
};
const meta: Meta<typeof ErrorBoundary> = {
title: 'Components/ErrorBoundary',
component: ErrorBoundary,
parameters: {
layout: 'fullscreen',
// Prevent Storybook from re-rendering on error
chromatic: { disableSnapshot: true },
},
tags: ['autodocs'],
};
export default meta;
type Story = StoryObj<typeof ErrorBoundary>;
export const NoError: Story = {
render: () => (
<ErrorBoundary>
<ErrorThrower shouldThrow={false} />
</ErrorBoundary>
),
};
export const WithError: Story = {
render: () => (
<ErrorBoundary>
<ErrorThrower shouldThrow={true} />
</ErrorBoundary>
),
};
export const WithCustomFallback: Story = {
render: () => (
<ErrorBoundary
fallback={
<div className="p-8 text-center bg-amber-100 text-amber-800 rounded-lg">
<h2 className="text-xl font-bold mb-2">Custom Fallback</h2>
<p>Something went wrong, but we have a custom fallback UI.</p>
</div>
}
>
<ErrorThrower shouldThrow={true} />
</ErrorBoundary>
),
};

View file

@ -0,0 +1,83 @@
import type { Meta, StoryObj } from '@storybook/react';
import { WifiOff, Loader2, List } from 'lucide-react';
// Since OfflineIndicator has complex hook dependencies, we create visual representations
// of its different states for documentation purposes.
const OfflineIndicatorMock = ({ variant }: { variant: 'offline' | 'syncing' | 'hidden' }) => {
if (variant === 'hidden') {
return (
<div className="p-4 bg-muted rounded text-center text-muted-foreground">
(Indicator is hidden when online with no pending requests)
</div>
);
}
if (variant === 'offline') {
return (
<div className="bg-kodo-red/90 backdrop-blur-sm text-white px-4 py-2.5 text-sm flex items-center justify-center gap-2 shadow-lg border-b border-kodo-red rounded">
<WifiOff className="w-4 h-4" />
<span>
Mode hors ligne
<span className="ml-2 font-semibold">- 3 requêtes en attente</span>
</span>
<button className="ml-3 px-2 py-1 bg-white/10 hover:bg-white/20 rounded border border-white/20 transition-colors flex items-center gap-1.5 text-xs font-medium">
<List className="w-3.5 h-3.5" />
View Queue
</button>
</div>
);
}
return (
<div className="bg-kodo-cyan/90 backdrop-blur-sm text-kodo-void px-4 py-2.5 text-sm flex items-center justify-center gap-2 shadow-lg border-b border-kodo-steel rounded">
<Loader2 className="w-4 h-4 animate-spin" />
<span>
Synchronisation en cours
<span className="ml-2 font-semibold">- 2 requêtes restantes</span>
</span>
<button className="ml-2 px-2 py-1 bg-kodo-red/20 hover:bg-kodo-red/30 rounded border border-kodo-red/30 transition-colors flex items-center gap-1.5 text-xs font-medium">
Clear Queue
</button>
<button className="ml-2 px-2 py-1 bg-kodo-void/20 hover:bg-kodo-void/30 rounded border border-kodo-void/30 transition-colors flex items-center gap-1.5 text-xs font-medium">
<List className="w-3.5 h-3.5" />
View Queue
</button>
</div>
);
};
const meta: Meta<typeof OfflineIndicatorMock> = {
title: 'Components/OfflineIndicator',
component: OfflineIndicatorMock,
parameters: {
layout: 'padded',
docs: {
description: {
component: 'Displays the current network status and pending offline requests. The actual component uses hooks for online detection and queue management.',
},
},
},
tags: ['autodocs'],
};
export default meta;
type Story = StoryObj<typeof OfflineIndicatorMock>;
export const Offline: Story = {
args: {
variant: 'offline',
},
};
export const Syncing: Story = {
args: {
variant: 'syncing',
},
};
export const Hidden: Story = {
args: {
variant: 'hidden',
},
};

View file

@ -0,0 +1,67 @@
import type { Meta, StoryObj } from '@storybook/react';
import { AdminAuditLogsView } from './AdminAuditLogsView';
/**
* AdminAuditLogsView - Logs d'audit
*
* Table paginée des logs d'audit avec recherche,
* filtrage et détails contextuels.
*/
const meta: Meta<typeof AdminAuditLogsView> = {
title: 'Components/Admin/AdminAuditLogsView',
component: AdminAuditLogsView,
parameters: {
layout: 'fullscreen',
docs: {
description: {
component: 'Journal d\'audit immutable avec recherche et pagination.',
},
},
},
tags: ['autodocs'],
decorators: [
(Story) => (
<div className="bg-kodo-background min-h-screen p-4">
<Story />
</div>
),
],
};
export default meta;
type Story = StoryObj<typeof meta>;
/**
* État par défaut avec logs chargés.
*/
export const Default: Story = {
name: 'Par défaut',
};
/**
* État avec filtres appliqués.
*/
export const Filtered: Story = {
name: 'Filtré',
parameters: {
docs: {
description: {
story: 'Logs filtrés par action ou ressource.',
},
},
},
};
/**
* État vide - aucun log trouvé.
*/
export const Empty: Story = {
name: 'Aucun log',
parameters: {
docs: {
description: {
story: 'Message affiché quand aucun log ne correspond aux critères.',
},
},
},
};

View file

@ -0,0 +1,53 @@
import type { Meta, StoryObj } from '@storybook/react';
import { AdminDashboardView } from './AdminDashboardView';
/**
* AdminDashboardView - Centre de commande admin
*
* Vue principale d'administration avec métriques en temps réel,
* visualisation du trafic, queue de modération et logs système.
*/
const meta: Meta<typeof AdminDashboardView> = {
title: 'Components/Admin/AdminDashboardView',
component: AdminDashboardView,
parameters: {
layout: 'fullscreen',
docs: {
description: {
component: 'Dashboard admin avec métriques, graphiques de trafic et contrôles système.',
},
},
},
tags: ['autodocs'],
decorators: [
(Story) => (
<div className="bg-kodo-background min-h-screen p-4">
<Story />
</div>
),
],
};
export default meta;
type Story = StoryObj<typeof meta>;
/**
* État par défaut avec données chargées.
*/
export const Default: Story = {
name: 'Par défaut',
};
/**
* État de chargement initial.
*/
export const Loading: Story = {
name: 'Chargement',
parameters: {
docs: {
description: {
story: 'Affiche le spinner pendant le chargement des données admin.',
},
},
},
};

View file

@ -0,0 +1,67 @@
import type { Meta, StoryObj } from '@storybook/react';
import { AdminModerationView } from './AdminModerationView';
/**
* AdminModerationView - Queue de modération
*
* Interface de traitement des signalements avec onglets
* pour pending/reviewed/resolved et actions de modération.
*/
const meta: Meta<typeof AdminModerationView> = {
title: 'Components/Admin/AdminModerationView',
component: AdminModerationView,
parameters: {
layout: 'fullscreen',
docs: {
description: {
component: 'Queue de modération avec actions ban, resolve, dismiss et warning.',
},
},
},
tags: ['autodocs'],
decorators: [
(Story) => (
<div className="bg-kodo-background min-h-screen p-4">
<Story />
</div>
),
],
};
export default meta;
type Story = StoryObj<typeof meta>;
/**
* État par défaut avec rapports en attente.
*/
export const Default: Story = {
name: 'Par défaut',
};
/**
* Vue de la queue avec rapports en attente.
*/
export const Queue: Story = {
name: 'Queue de modération',
parameters: {
docs: {
description: {
story: 'Liste des signalements en attente de traitement.',
},
},
},
};
/**
* État vide - tous les rapports traités.
*/
export const Empty: Story = {
name: 'Queue vide',
parameters: {
docs: {
description: {
story: 'Message affiché quand tous les signalements ont été traités.',
},
},
},
};

View file

@ -0,0 +1,53 @@
import type { Meta, StoryObj } from '@storybook/react';
import { AdminSettingsView } from './AdminSettingsView';
/**
* AdminSettingsView - Paramètres système
*
* Interface de configuration système avec feature flags,
* mode maintenance et annonces globales.
*/
const meta: Meta<typeof AdminSettingsView> = {
title: 'Components/Admin/AdminSettingsView',
component: AdminSettingsView,
parameters: {
layout: 'padded',
docs: {
description: {
component: 'Configuration système admin avec feature flags et mode maintenance.',
},
},
},
tags: ['autodocs'],
decorators: [
(Story) => (
<div className="bg-kodo-background min-h-screen p-4">
<Story />
</div>
),
],
};
export default meta;
type Story = StoryObj<typeof meta>;
/**
* État par défaut des paramètres.
*/
export const Default: Story = {
name: 'Par défaut',
};
/**
* État de sauvegarde en cours.
*/
export const Saving: Story = {
name: 'Sauvegarde',
parameters: {
docs: {
description: {
story: 'Feedback visuel pendant la sauvegarde des paramètres.',
},
},
},
};

View file

@ -0,0 +1,81 @@
import type { Meta, StoryObj } from '@storybook/react';
import { AdminUsersView } from './AdminUsersView';
/**
* AdminUsersView - Grille d'identités utilisateurs
*
* Vue de gestion des utilisateurs avec recherche, filtrage,
* et actions de modération (ban, suppression, rôles).
*/
const meta: Meta<typeof AdminUsersView> = {
title: 'Components/Admin/AdminUsersView',
component: AdminUsersView,
parameters: {
layout: 'fullscreen',
docs: {
description: {
component: 'Interface de gestion des utilisateurs avec table paginée et actions.',
},
},
},
tags: ['autodocs'],
decorators: [
(Story) => (
<div className="bg-kodo-background min-h-screen">
<Story />
</div>
),
],
};
export default meta;
type Story = StoryObj<typeof meta>;
/**
* État par défaut avec liste d'utilisateurs.
*/
export const Default: Story = {
name: 'Par défaut',
};
/**
* État vide - aucun utilisateur trouvé.
*/
export const Empty: Story = {
name: 'Aucun utilisateur',
parameters: {
docs: {
description: {
story: 'Message affiché quand la recherche ne retourne aucun résultat.',
},
},
},
};
/**
* État de chargement.
*/
export const Loading: Story = {
name: 'Chargement',
parameters: {
docs: {
description: {
story: 'Spinner affiché pendant le chargement des utilisateurs.',
},
},
},
};
/**
* État avec filtres appliqués.
*/
export const WithFilters: Story = {
name: 'Avec filtres',
parameters: {
docs: {
description: {
story: 'Démonstration des filtres par rôle et statut.',
},
},
},
};

View file

@ -0,0 +1,117 @@
import type { Meta, StoryObj } from '@storybook/react';
import { fn } from '@storybook/test';
import { UserTableRow } from './UserTableRow';
import { User } from '@/types';
// Mock user data
const createMockUser = (overrides: Partial<User> = {}): User => ({
id: 'usr_abc123def456',
username: 'demo_artist',
email: 'demo@veza.music',
avatar: 'https://api.dicebear.com/7.x/avataaars/svg?seed=demo',
status: 'online',
role: 'user',
roles: ['user', 'artist'],
tier: 'Pro',
created_at: '2025-01-15',
last_login_at: '2026-02-02',
...overrides,
});
/**
* UserTableRow - Ligne de tableau utilisateur
*
* Composant de ligne affichant les informations d'un utilisateur
* avec menu d'actions contextuel.
*/
const meta: Meta<typeof UserTableRow> = {
title: 'Components/Admin/UserTableRow',
component: UserTableRow,
parameters: {
docs: {
description: {
component: 'Ligne de tableau utilisateur avec avatar, statut, rôles et menu d\'actions.',
},
},
},
tags: ['autodocs'],
args: {
user: createMockUser(),
onBan: fn(),
onDelete: fn(),
onEditRole: fn(),
},
argTypes: {
user: {
description: 'Objet utilisateur à afficher',
},
onBan: {
action: 'onBan',
description: 'Callback pour suspendre l\'utilisateur',
},
onDelete: {
action: 'onDelete',
description: 'Callback pour supprimer l\'utilisateur',
},
onEditRole: {
action: 'onEditRole',
description: 'Callback pour modifier les rôles',
},
},
decorators: [
(Story) => (
<div className="bg-kodo-background p-4">
<table className="w-full">
<tbody>
<Story />
</tbody>
</table>
</div>
),
],
};
export default meta;
type Story = StoryObj<typeof meta>;
/**
* Utilisateur standard en ligne.
*/
export const Default: Story = {
name: 'Par défaut',
};
/**
* Ligne sélectionnée avec menu ouvert.
*/
export const Selected: Story = {
name: 'Sélectionné',
parameters: {
docs: {
description: {
story: 'État de la ligne quand le menu d\'actions est ouvert.',
},
},
},
};
/**
* Utilisateur banni/suspendu.
*/
export const Banned: Story = {
name: 'Suspendu',
args: {
user: createMockUser({
username: 'banned_user',
status: 'busy',
roles: ['banned'],
}),
},
parameters: {
docs: {
description: {
story: 'Affichage d\'un utilisateur suspendu.',
},
},
},
};

View file

@ -0,0 +1,76 @@
import type { Meta, StoryObj } from '@storybook/react';
import { fn } from '@storybook/test';
import { BanUserModal } from './BanUserModal';
/**
* BanUserModal - Modal de suspension d'utilisateur
*
* Modal permettant de configurer et confirmer la suspension
* d'un utilisateur avec raison, durée et notes internes.
*/
const meta: Meta<typeof BanUserModal> = {
title: 'Components/Admin/Modals/BanUserModal',
component: BanUserModal,
parameters: {
layout: 'centered',
docs: {
description: {
component: 'Modal de suspension avec options temporaire/permanent et raisons prédéfinies.',
},
},
},
tags: ['autodocs'],
args: {
username: 'troublemaker_user',
onClose: fn(),
onConfirm: fn(),
},
argTypes: {
username: {
control: 'text',
description: 'Nom d\'utilisateur à suspendre',
},
onClose: {
action: 'onClose',
description: 'Callback appelé à la fermeture',
},
onConfirm: {
action: 'onConfirm',
description: 'Callback appelé à la confirmation avec (reason, details, duration)',
},
},
decorators: [
(Story) => (
<div className="bg-kodo-background min-h-screen flex items-center justify-center">
<Story />
</div>
),
],
};
export default meta;
type Story = StoryObj<typeof meta>;
/**
* État par défaut de la modal.
*/
export const Default: Story = {
name: 'Par défaut',
};
/**
* Étape de confirmation avant suspension.
*/
export const Confirm: Story = {
name: 'Confirmation',
args: {
username: 'spammer_account',
},
parameters: {
docs: {
description: {
story: 'Prêt à confirmer la suspension de l\'utilisateur.',
},
},
},
};

View file

@ -0,0 +1,23 @@
import type { Meta, StoryObj } from '@storybook/react';
import { TrackAnalyticsView } from './TrackAnalyticsView';
const meta: Meta<typeof TrackAnalyticsView> = {
title: 'Components/Analytics/TrackAnalyticsView',
component: TrackAnalyticsView,
parameters: { layout: 'padded' },
tags: ['autodocs'],
decorators: [
(Story) => (
<div className="bg-kodo-background p-4 min-h-screen">
<Story />
</div>
),
],
};
export default meta;
type Story = StoryObj<typeof meta>;
export const Default: Story = { name: 'Par défaut' };
export const Loading: Story = { name: 'Chargement' };
export const Empty: Story = { name: 'Vide' };

View file

@ -0,0 +1,22 @@
import type { Meta, StoryObj } from '@storybook/react';
import { OrderSummary } from './OrderSummary';
const meta: Meta<typeof OrderSummary> = {
title: 'Components/Commerce/OrderSummary',
component: OrderSummary,
parameters: { layout: 'padded' },
tags: ['autodocs'],
decorators: [
(Story) => (
<div className="bg-kodo-background p-4 max-w-md">
<Story />
</div>
),
],
};
export default meta;
type Story = StoryObj<typeof meta>;
export const Default: Story = { name: 'Par défaut' };
export const WithDiscount: Story = { name: 'Avec réduction' };

View file

@ -7,7 +7,7 @@ const meta = {
tags: ['autodocs'], tags: ['autodocs'],
decorators: [ decorators: [
(Story) => ( (Story) => (
<div className="h-[400px] p-4 bg-kodo-ink"> <div className="w-full max-w-3xl h-[400px] p-4 bg-kodo-background">
<Story /> <Story />
</div> </div>
), ),

View file

@ -1,11 +1,18 @@
import type { Meta, StoryObj } from '@storybook/react'; import type { Meta, StoryObj } from '@storybook/react';
import { StatCard } from './StatCard'; import { StatCard } from './StatCard';
import { Users, DollarSign, Activity } from 'lucide-react'; import { Activity, Music, Users, DollarSign } from 'lucide-react';
const meta = { const meta = {
title: 'Components/Dashboard/StatCard', title: 'Components/Dashboard/StatCard',
component: StatCard, component: StatCard,
tags: ['autodocs'], tags: ['autodocs'],
decorators: [
(Story) => (
<div className="w-[300px] h-[200px] p-4 bg-kodo-background">
<Story />
</div>
),
],
} satisfies Meta<typeof StatCard>; } satisfies Meta<typeof StatCard>;
export default meta; export default meta;
@ -13,32 +20,32 @@ type Story = StoryObj<typeof meta>;
export const Default: Story = { export const Default: Story = {
args: { args: {
label: 'Total Users', label: 'Total Plays',
value: '12,345', value: '1.2M',
icon: <Users className="w-5 h-5 text-kodo-cyan" />, icon: <Music className="w-5 h-5 text-white" />,
trend: '+12%', trend: '+12.5',
color: 'cyan', color: 'cyan',
sparklineData: [50, 60, 55, 70, 65, 80, 75, 90], sparklineData: [40, 30, 45, 50, 60, 75, 80],
}, },
}; };
export const NegativeTrend: Story = { export const NegativeTrend: Story = {
args: { args: {
label: 'Revenue', label: 'Revenue',
value: '$4,200', value: '$432.50',
icon: <DollarSign className="w-5 h-5 text-kodo-red" />, icon: <DollarSign className="w-5 h-5 text-white" />,
trend: '-5%', trend: '-5.2',
color: 'red', color: 'red',
sparklineData: [90, 80, 70, 60, 50, 40], sparklineData: [80, 75, 70, 65, 60, 55, 50],
}, },
}; };
export const NoTrend: Story = { export const NoTrend: Story = {
args: { args: {
label: 'Active Sessions', label: 'Followers',
value: '342', value: '5,432',
icon: <Activity className="w-5 h-5 text-kodo-lime" />, icon: <Users className="w-5 h-5 text-white" />,
color: 'lime', color: 'magenta',
sparklineData: [10, 20, 15, 25, 30, 20, 40], sparklineData: [10, 20, 15, 25, 30, 40, 50],
}, },
}; };

View file

@ -0,0 +1,21 @@
import type { Meta, StoryObj } from '@storybook/react';
import { APIPlaygroundView } from './APIPlaygroundView';
const meta: Meta<typeof APIPlaygroundView> = {
title: 'Components/Developer/APIPlaygroundView',
component: APIPlaygroundView,
parameters: { layout: 'fullscreen' },
tags: ['autodocs'],
decorators: [
(Story) => (
<div className="bg-kodo-background min-h-screen">
<Story />
</div>
),
],
};
export default meta;
type Story = StoryObj<typeof meta>;
export const Default: Story = { name: 'Par défaut' };

View file

@ -0,0 +1,24 @@
import type { Meta, StoryObj } from '@storybook/react';
import { DeveloperDashboardView } from './DeveloperDashboardView';
import { ToastProvider } from '../../components/feedback/ToastProvider';
const meta: Meta<typeof DeveloperDashboardView> = {
title: 'Components/Developer/DeveloperDashboardView',
component: DeveloperDashboardView,
parameters: { layout: 'fullscreen' },
tags: ['autodocs'],
decorators: [
(Story) => (
<ToastProvider>
<div className="bg-kodo-background min-h-screen">
<Story />
</div>
</ToastProvider>
),
],
};
export default meta;
type Story = StoryObj<typeof meta>;
export const Default: Story = { name: 'Par défaut' };

View file

@ -0,0 +1,22 @@
import type { Meta, StoryObj } from '@storybook/react';
import { WebhooksView } from './WebhooksView';
const meta: Meta<typeof WebhooksView> = {
title: 'Components/Developer/WebhooksView',
component: WebhooksView,
parameters: { layout: 'fullscreen' },
tags: ['autodocs'],
decorators: [
(Story) => (
<div className="bg-kodo-background min-h-screen">
<Story />
</div>
),
],
};
export default meta;
type Story = StoryObj<typeof meta>;
export const Default: Story = { name: 'Par défaut' };
export const Empty: Story = { name: 'Vide' };

View file

@ -0,0 +1,17 @@
import type { Meta, StoryObj } from '@storybook/react';
import { CreateAPIKeyModal } from './CreateAPIKeyModal';
import { fn } from '@storybook/test';
const meta: Meta<typeof CreateAPIKeyModal> = {
title: 'Components/Developer/Modals/CreateAPIKeyModal',
component: CreateAPIKeyModal,
parameters: { layout: 'centered' },
tags: ['autodocs'],
args: { onClose: fn() },
};
export default meta;
type Story = StoryObj<typeof meta>;
export const Default: Story = { name: 'Par défaut' };
export const Created: Story = { name: 'Clé créée' };

View file

@ -0,0 +1,35 @@
import type { Meta, StoryObj } from '@storybook/react';
import { CourseDetailView } from './CourseDetailView';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { BrowserRouter } from 'react-router-dom';
const createMockQueryClient = () => new QueryClient({
defaultOptions: {
queries: { retry: false, staleTime: Infinity },
mutations: { retry: false },
},
});
const meta: Meta<typeof CourseDetailView> = {
title: 'Components/Education/CourseDetailView',
component: CourseDetailView,
parameters: { layout: 'fullscreen' },
tags: ['autodocs'],
decorators: [
(Story) => (
<QueryClientProvider client={createMockQueryClient()}>
<BrowserRouter>
<div className="bg-kodo-background min-h-screen">
<Story />
</div>
</BrowserRouter>
</QueryClientProvider>
),
],
};
export default meta;
type Story = StoryObj<typeof meta>;
export const Default: Story = { name: 'Par défaut' };
export const Enrolled: Story = { name: 'Inscrit' };

View file

@ -0,0 +1,35 @@
import type { Meta, StoryObj } from '@storybook/react';
import { CourseLearningView } from './CourseLearningView';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { BrowserRouter } from 'react-router-dom';
const createMockQueryClient = () => new QueryClient({
defaultOptions: {
queries: { retry: false, staleTime: Infinity },
mutations: { retry: false },
},
});
const meta: Meta<typeof CourseLearningView> = {
title: 'Components/Education/CourseLearningView',
component: CourseLearningView,
parameters: { layout: 'fullscreen' },
tags: ['autodocs'],
decorators: [
(Story) => (
<QueryClientProvider client={createMockQueryClient()}>
<BrowserRouter>
<div className="bg-kodo-background min-h-screen">
<Story />
</div>
</BrowserRouter>
</QueryClientProvider>
),
],
};
export default meta;
type Story = StoryObj<typeof meta>;
export const Default: Story = { name: 'Par défaut' };
export const Complete: Story = { name: 'Terminé' };

View file

@ -0,0 +1,91 @@
import type { Meta, StoryObj } from '@storybook/react';
import { FilterBar } from './FilterBar';
const mockFilters = {
filters: [
{
id: 'genre',
label: 'Genre',
type: 'select' as const,
options: [
{ label: 'All Genres', value: '' },
{ label: 'Electronic', value: 'electronic' },
{ label: 'Hip Hop', value: 'hiphop' },
{ label: 'Rock', value: 'rock' },
],
value: '',
},
{
id: 'year',
label: 'Year',
type: 'select' as const,
options: [
{ label: 'All Years', value: '' },
{ label: '2024', value: '2024' },
{ label: '2023', value: '2023' },
{ label: '2022', value: '2022' },
],
value: '',
},
],
onFilterChange: () => { },
};
const mockSort = {
options: [
{ label: 'Newest', value: 'newest' },
{ label: 'Oldest', value: 'oldest' },
{ label: 'Most Popular', value: 'popular' },
{ label: 'A-Z', value: 'alpha' },
],
value: 'newest',
onChange: () => { },
};
const meta: Meta<typeof FilterBar> = {
title: 'Components/Filters/FilterBar',
component: FilterBar,
parameters: {
layout: 'padded',
},
tags: ['autodocs'],
};
export default meta;
type Story = StoryObj<typeof FilterBar>;
export const Default: Story = {
args: {
filters: mockFilters,
sort: mockSort,
},
};
export const FiltersOnly: Story = {
args: {
filters: mockFilters,
},
};
export const SortOnly: Story = {
args: {
sort: mockSort,
},
};
export const NotCollapsible: Story = {
args: {
filters: mockFilters,
sort: mockSort,
collapsible: false,
},
};
export const CollapsedByDefault: Story = {
args: {
filters: mockFilters,
sort: mockSort,
collapsible: true,
defaultOpen: false,
},
};

View file

@ -0,0 +1,22 @@
import type { Meta, StoryObj } from '@storybook/react';
import { AchievementCard } from './AchievementCard';
const meta: Meta<typeof AchievementCard> = {
title: 'Components/Gamification/AchievementCard',
component: AchievementCard,
parameters: { layout: 'padded' },
tags: ['autodocs'],
decorators: [
(Story) => (
<div className="bg-kodo-background p-4 max-w-sm">
<Story />
</div>
),
],
};
export default meta;
type Story = StoryObj<typeof meta>;
export const Locked: Story = { name: 'Verrouillé' };
export const Unlocked: Story = { name: 'Déverrouillé' };

View file

@ -0,0 +1,22 @@
import type { Meta, StoryObj } from '@storybook/react';
import { AchievementsView } from './AchievementsView';
const meta: Meta<typeof AchievementsView> = {
title: 'Components/Gamification/AchievementsView',
component: AchievementsView,
parameters: { layout: 'fullscreen' },
tags: ['autodocs'],
decorators: [
(Story) => (
<div className="bg-kodo-background min-h-screen">
<Story />
</div>
),
],
};
export default meta;
type Story = StoryObj<typeof meta>;
export const Default: Story = { name: 'Par défaut' };
export const Empty: Story = { name: 'Vide' };

View file

@ -0,0 +1,21 @@
import type { Meta, StoryObj } from '@storybook/react';
import { LeaderboardView } from './LeaderboardView';
const meta: Meta<typeof LeaderboardView> = {
title: 'Components/Gamification/LeaderboardView',
component: LeaderboardView,
parameters: { layout: 'fullscreen' },
tags: ['autodocs'],
decorators: [
(Story) => (
<div className="bg-kodo-background min-h-screen">
<Story />
</div>
),
],
};
export default meta;
type Story = StoryObj<typeof meta>;
export const Default: Story = { name: 'Par défaut' };

View file

@ -0,0 +1,22 @@
import type { Meta, StoryObj } from '@storybook/react';
import { XPBar } from './XPBar';
const meta: Meta<typeof XPBar> = {
title: 'Components/Gamification/XPBar',
component: XPBar,
parameters: { layout: 'padded' },
tags: ['autodocs'],
decorators: [
(Story) => (
<div className="bg-kodo-background p-4 w-full">
<Story />
</div>
),
],
};
export default meta;
type Story = StoryObj<typeof meta>;
export const Default: Story = { name: 'Par défaut' };
export const LevelUp: Story = { name: 'Montée de niveau' };

View file

@ -0,0 +1,24 @@
import type { Meta, StoryObj } from '@storybook/react';
import { EquipmentDetailView } from './EquipmentDetailView';
import { ToastProvider } from '../../components/feedback/ToastProvider';
const meta: Meta<typeof EquipmentDetailView> = {
title: 'Components/Inventory/EquipmentDetailView',
component: EquipmentDetailView,
parameters: { layout: 'fullscreen' },
tags: ['autodocs'],
decorators: [
(Story) => (
<ToastProvider>
<div className="bg-kodo-background min-h-screen">
<Story />
</div>
</ToastProvider>
),
],
};
export default meta;
type Story = StoryObj<typeof meta>;
export const Default: Story = { name: 'Par défaut' };

View file

@ -0,0 +1,21 @@
import type { Meta, StoryObj } from '@storybook/react';
import { PlaylistsView } from './PlaylistsView';
const meta: Meta<typeof PlaylistsView> = {
title: 'Components/Library/Playlists/PlaylistsView',
component: PlaylistsView,
parameters: { layout: 'padded' },
tags: ['autodocs'],
decorators: [
(Story) => (
<div className="bg-kodo-background p-4 min-h-screen">
<Story />
</div>
),
],
};
export default meta;
type Story = StoryObj<typeof meta>;
export const Default: Story = { name: 'Par défaut' };

View file

@ -0,0 +1,53 @@
import type { Meta, StoryObj } from '@storybook/react';
import { QueueView } from './QueueView';
/**
* QueueView - Vue de la file d'attente
*
* Vue pleine page de la queue avec gestion
* complète des tracks.
*/
const meta: Meta<typeof QueueView> = {
title: 'Components/Library/Playlists/QueueView',
component: QueueView,
parameters: {
layout: 'fullscreen',
docs: {
description: {
component: 'Vue complète de la queue avec actions.',
},
},
},
tags: ['autodocs'],
decorators: [
(Story) => (
<div className="bg-kodo-background min-h-screen p-4">
<Story />
</div>
),
],
};
export default meta;
type Story = StoryObj<typeof meta>;
/**
* État par défaut avec tracks.
*/
export const Default: Story = {
name: 'Par défaut',
};
/**
* État vide.
*/
export const Empty: Story = {
name: 'Vide',
parameters: {
docs: {
description: {
story: 'Message affiché quand la queue est vide.',
},
},
},
};

View file

@ -0,0 +1,72 @@
import type { Meta, StoryObj } from '@storybook/react';
import { fn } from '@storybook/test';
import { SaveQueueAsPlaylistModal } from './SaveQueueAsPlaylistModal';
/**
* SaveQueueAsPlaylistModal - Modal de sauvegarde de queue
*
* Modal permettant de sauvegarder la queue actuelle
* comme nouvelle playlist.
*/
const meta: Meta<typeof SaveQueueAsPlaylistModal> = {
title: 'Components/Library/Playlists/SaveQueueAsPlaylistModal',
component: SaveQueueAsPlaylistModal,
parameters: {
layout: 'centered',
docs: {
description: {
component: 'Modal de sauvegarde de queue en playlist.',
},
},
},
tags: ['autodocs'],
args: {
onClose: fn(),
onSave: fn(),
},
decorators: [
(Story) => (
<div className="bg-kodo-background min-h-screen flex items-center justify-center">
<Story />
</div>
),
],
};
export default meta;
type Story = StoryObj<typeof meta>;
/**
* État par défaut.
*/
export const Default: Story = {
name: 'Par défaut',
};
/**
* État de sauvegarde en cours.
*/
export const Saving: Story = {
name: 'Sauvegarde',
parameters: {
docs: {
description: {
story: 'Spinner pendant la création de la playlist.',
},
},
},
};
/**
* Sauvegarde réussie.
*/
export const Success: Story = {
name: 'Succès',
parameters: {
docs: {
description: {
story: 'Message de confirmation après création.',
},
},
},
};

View file

@ -0,0 +1,23 @@
import type { Meta, StoryObj } from '@storybook/react';
import { LicenceCard } from './LicenceCard';
const meta: Meta<typeof LicenceCard> = {
title: 'Components/Marketplace/LicenceCard',
component: LicenceCard,
parameters: { layout: 'padded' },
tags: ['autodocs'],
decorators: [
(Story) => (
<div className="bg-kodo-background p-4 max-w-sm">
<Story />
</div>
),
],
};
export default meta;
type Story = StoryObj<typeof meta>;
export const Basic: Story = { name: 'Basic' };
export const Pro: Story = { name: 'Pro' };
export const Exclusive: Story = { name: 'Exclusive' };

View file

@ -0,0 +1,23 @@
import type { Meta, StoryObj } from '@storybook/react';
import { ProductDetailView } from './ProductDetailView';
const meta: Meta<typeof ProductDetailView> = {
title: 'Components/Marketplace/ProductDetailView',
component: ProductDetailView,
parameters: { layout: 'padded' },
tags: ['autodocs'],
decorators: [
(Story) => (
<div className="bg-kodo-background p-4 min-h-screen">
<Story />
</div>
),
],
};
export default meta;
type Story = StoryObj<typeof meta>;
export const Default: Story = { name: 'Par défaut' };
export const Loading: Story = { name: 'Chargement' };
export const OutOfStock: Story = { name: 'Rupture de stock' };

View file

@ -0,0 +1,24 @@
import type { Meta, StoryObj } from '@storybook/react';
import { MonitoringDashboard } from './MonitoringDashboard';
import { ToastProvider } from '../../components/feedback/ToastProvider';
const meta: Meta<typeof MonitoringDashboard> = {
title: 'Components/Monitoring/MonitoringDashboard',
component: MonitoringDashboard,
parameters: { layout: 'fullscreen' },
tags: ['autodocs'],
decorators: [
(Story) => (
<ToastProvider>
<div className="bg-kodo-background min-h-screen">
<Story />
</div>
</ToastProvider>
),
],
};
export default meta;
type Story = StoryObj<typeof meta>;
export const Default: Story = { name: 'Par défaut' };

View file

@ -0,0 +1,54 @@
import type { Meta, StoryObj } from '@storybook/react';
import { Breadcrumbs } from './Breadcrumbs';
import { FileText, Music } from 'lucide-react';
const meta: Meta<typeof Breadcrumbs> = {
title: 'Components/Navigation/Breadcrumbs',
component: Breadcrumbs,
parameters: {
layout: 'padded',
},
tags: ['autodocs'],
};
export default meta;
type Story = StoryObj<typeof Breadcrumbs>;
export const Default: Story = {
args: {
items: [
{ label: 'Library', href: '/library' },
{ label: 'Playlists', href: '/library/playlists' },
{ label: 'Summer Vibes' },
],
},
};
export const WithIcons: Story = {
args: {
items: [
{ label: 'Documents', href: '/docs', icon: <FileText className="h-4 w-4" /> },
{ label: 'Music', href: '/docs/music', icon: <Music className="h-4 w-4" /> },
{ label: 'Track Details' },
],
},
};
export const WithoutHome: Story = {
args: {
items: [
{ label: 'Settings', href: '/settings' },
{ label: 'Privacy' },
],
showHome: false,
},
};
export const SingleItem: Story = {
args: {
items: [
{ label: 'Dashboard' },
],
showHome: false,
},
};

View file

@ -0,0 +1,83 @@
import type { Meta, StoryObj } from '@storybook/react';
import { Pagination } from './Pagination';
import { useState } from 'react';
const meta: Meta<typeof Pagination> = {
title: 'Components/Navigation/Pagination',
component: Pagination,
parameters: {
layout: 'centered',
},
tags: ['autodocs'],
argTypes: {
onPageChange: { action: 'onPageChange' },
},
};
export default meta;
type Story = StoryObj<typeof Pagination>;
export const Default: Story = {
args: {
currentPage: 1,
totalPages: 10,
},
};
export const MiddlePage: Story = {
args: {
currentPage: 5,
totalPages: 10,
},
};
export const LastPage: Story = {
args: {
currentPage: 10,
totalPages: 10,
},
};
export const FewPages: Story = {
args: {
currentPage: 2,
totalPages: 3,
},
};
export const WithFirstLast: Story = {
args: {
currentPage: 5,
totalPages: 20,
showFirstLast: true,
},
};
export const WithItemsInfo: Story = {
args: {
currentPage: 2,
totalPages: 10,
totalItems: 95,
itemsPerPage: 10,
showItemsInfo: true,
},
};
// Interactive story
const InteractivePagination = () => {
const [page, setPage] = useState(1);
return (
<Pagination
currentPage={page}
totalPages={15}
onPageChange={setPage}
totalItems={150}
itemsPerPage={10}
showItemsInfo
/>
);
};
export const Interactive: Story = {
render: () => <InteractivePagination />,
};

View file

@ -0,0 +1,71 @@
import type { Meta, StoryObj } from '@storybook/react';
import { fn } from '@storybook/test';
import { FullPlayer } from './FullPlayer';
/**
* FullPlayer - Lecteur plein écran
*
* Vue immersive du lecteur avec artwork, waveform,
* paroles et contrôles avancés.
*/
const meta: Meta<typeof FullPlayer> = {
title: 'Components/Player/FullPlayer',
component: FullPlayer,
parameters: {
layout: 'fullscreen',
docs: {
description: {
component: 'Lecteur audio plein écran avec artwork et waveform.',
},
},
},
tags: ['autodocs'],
args: {
onClose: fn(),
},
decorators: [
(Story) => (
<div className="bg-kodo-background min-h-screen">
<Story />
</div>
),
],
};
export default meta;
type Story = StoryObj<typeof meta>;
/**
* État par défaut.
*/
export const Default: Story = {
name: 'Par défaut',
};
/**
* Avec panneau de queue visible.
*/
export const WithQueue: Story = {
name: 'Avec Queue',
parameters: {
docs: {
description: {
story: 'Affiche le panneau de queue à côté du player.',
},
},
},
};
/**
* Avec paroles affichées.
*/
export const WithLyrics: Story = {
name: 'Avec Paroles',
parameters: {
docs: {
description: {
story: 'Affiche les paroles synchronisées.',
},
},
},
};

View file

@ -0,0 +1,67 @@
import type { Meta, StoryObj } from '@storybook/react';
import { LyricsPanel } from './LyricsPanel';
/**
* LyricsPanel - Panneau de paroles
*
* Affichage des paroles synchronisées avec
* la lecture audio actuelle.
*/
const meta: Meta<typeof LyricsPanel> = {
title: 'Components/Player/LyricsPanel',
component: LyricsPanel,
parameters: {
layout: 'padded',
docs: {
description: {
component: 'Panneau de paroles synchronisées avec le player.',
},
},
},
tags: ['autodocs'],
decorators: [
(Story) => (
<div className="bg-kodo-background min-h-screen p-4 max-w-md">
<Story />
</div>
),
],
};
export default meta;
type Story = StoryObj<typeof meta>;
/**
* État par défaut avec paroles.
*/
export const Default: Story = {
name: 'Par défaut',
};
/**
* Paroles synchronisées en cours.
*/
export const Synced: Story = {
name: 'Synchronisé',
parameters: {
docs: {
description: {
story: 'Paroles avec mise en surbrillance synchronisée.',
},
},
},
};
/**
* État vide - aucune parole.
*/
export const Empty: Story = {
name: 'Vide',
parameters: {
docs: {
description: {
story: 'Message affiché quand aucune parole n\'est disponible.',
},
},
},
};

View file

@ -0,0 +1,68 @@
import type { Meta, StoryObj } from '@storybook/react';
import { fn } from '@storybook/test';
import { QueuePanel } from './QueuePanel';
/**
* QueuePanel - Panneau de file d'attente
*
* Liste des tracks en file d'attente avec
* réorganisation par drag-and-drop.
*/
const meta: Meta<typeof QueuePanel> = {
title: 'Components/Player/QueuePanel',
component: QueuePanel,
parameters: {
layout: 'padded',
docs: {
description: {
component: 'Panneau de queue avec drag-and-drop.',
},
},
},
tags: ['autodocs'],
decorators: [
(Story) => (
<div className="bg-kodo-background min-h-screen p-4 max-w-md">
<Story />
</div>
),
],
};
export default meta;
type Story = StoryObj<typeof meta>;
/**
* État par défaut avec tracks.
*/
export const Default: Story = {
name: 'Par défaut',
};
/**
* État vide.
*/
export const Empty: Story = {
name: 'Vide',
parameters: {
docs: {
description: {
story: 'Message affiché quand la queue est vide.',
},
},
},
};
/**
* État de réorganisation.
*/
export const Reordering: Story = {
name: 'Réorganisation',
parameters: {
docs: {
description: {
story: 'Démonstration du drag-and-drop.',
},
},
},
};

View file

@ -0,0 +1,23 @@
import type { Meta, StoryObj } from '@storybook/react';
import { GlobalSearchBar } from './GlobalSearchBar';
const meta: Meta<typeof GlobalSearchBar> = {
title: 'Components/Search/GlobalSearchBar',
component: GlobalSearchBar,
parameters: { layout: 'centered' },
tags: ['autodocs'],
decorators: [
(Story) => (
<div className="bg-kodo-background p-8 w-96">
<Story />
</div>
),
],
};
export default meta;
type Story = StoryObj<typeof meta>;
export const Default: Story = { name: 'Par défaut' };
export const Focused: Story = { name: 'Focus' };
export const WithSuggestions: Story = { name: 'Avec suggestions' };

View file

@ -0,0 +1,39 @@
import type { Meta, StoryObj } from '@storybook/react';
import { AccessibilitySettingsView } from './AccessibilitySettingsView';
/**
* AccessibilitySettingsView - Paramètres d'accessibilité
*
* Section des options d'accessibilité : contraste,
* taille de police, réduction des animations.
*/
const meta: Meta<typeof AccessibilitySettingsView> = {
title: 'Components/Settings/Accessibility/AccessibilitySettingsView',
component: AccessibilitySettingsView,
parameters: {
layout: 'padded',
docs: {
description: {
component: 'Paramètres d\'accessibilité avec contraste et taille de police.',
},
},
},
tags: ['autodocs'],
decorators: [
(Story) => (
<div className="bg-kodo-background min-h-screen p-4 max-w-4xl">
<Story />
</div>
),
],
};
export default meta;
type Story = StoryObj<typeof meta>;
/**
* État par défaut des paramètres d'accessibilité.
*/
export const Default: Story = {
name: 'Par défaut',
};

View file

@ -0,0 +1,39 @@
import type { Meta, StoryObj } from '@storybook/react';
import { AccountSettings } from './AccountSettings';
/**
* AccountSettings - Paramètres du compte
*
* Section principale des paramètres de compte avec
* gestion email, nom d'utilisateur et suppression.
*/
const meta: Meta<typeof AccountSettings> = {
title: 'Components/Settings/Account/AccountSettings',
component: AccountSettings,
parameters: {
layout: 'padded',
docs: {
description: {
component: 'Paramètres de compte avec actions email, username, suppression.',
},
},
},
tags: ['autodocs'],
decorators: [
(Story) => (
<div className="bg-kodo-background min-h-screen p-4 max-w-4xl">
<Story />
</div>
),
],
};
export default meta;
type Story = StoryObj<typeof meta>;
/**
* État par défaut.
*/
export const Default: Story = {
name: 'Par défaut',
};

View file

@ -0,0 +1,71 @@
import type { Meta, StoryObj } from '@storybook/react';
import { fn } from '@storybook/test';
import { ChangeEmailModal } from './ChangeEmailModal';
/**
* ChangeEmailModal - Modal de changement d'email
*
* Modal permettant à l'utilisateur de changer son adresse email
* avec validation et envoi de confirmation.
*/
const meta: Meta<typeof ChangeEmailModal> = {
title: 'Components/Settings/Account/ChangeEmailModal',
component: ChangeEmailModal,
parameters: {
layout: 'centered',
docs: {
description: {
component: 'Modal de changement d\'email avec validation.',
},
},
},
tags: ['autodocs'],
args: {
onClose: fn(),
},
decorators: [
(Story) => (
<div className="bg-kodo-background min-h-screen flex items-center justify-center">
<Story />
</div>
),
],
};
export default meta;
type Story = StoryObj<typeof meta>;
/**
* État par défaut de la modal.
*/
export const Default: Story = {
name: 'Par défaut',
};
/**
* État d'envoi en cours.
*/
export const Sending: Story = {
name: 'Envoi',
parameters: {
docs: {
description: {
story: 'Spinner affiché pendant l\'envoi de la demande.',
},
},
},
};
/**
* État envoyé avec succès.
*/
export const Sent: Story = {
name: 'Envoyé',
parameters: {
docs: {
description: {
story: 'Message de confirmation après envoi réussi.',
},
},
},
};

View file

@ -0,0 +1,71 @@
import type { Meta, StoryObj } from '@storybook/react';
import { fn } from '@storybook/test';
import { ChangeUsernameModal } from './ChangeUsernameModal';
/**
* ChangeUsernameModal - Modal de changement de nom d'utilisateur
*
* Modal permettant de changer le nom d'utilisateur avec
* vérification de disponibilité en temps réel.
*/
const meta: Meta<typeof ChangeUsernameModal> = {
title: 'Components/Settings/Account/ChangeUsernameModal',
component: ChangeUsernameModal,
parameters: {
layout: 'centered',
docs: {
description: {
component: 'Modal de changement de nom d\'utilisateur avec vérification.',
},
},
},
tags: ['autodocs'],
args: {
onClose: fn(),
},
decorators: [
(Story) => (
<div className="bg-kodo-background min-h-screen flex items-center justify-center">
<Story />
</div>
),
],
};
export default meta;
type Story = StoryObj<typeof meta>;
/**
* État par défaut de la modal.
*/
export const Default: Story = {
name: 'Par défaut',
};
/**
* État de vérification de disponibilité.
*/
export const Checking: Story = {
name: 'Vérification',
parameters: {
docs: {
description: {
story: 'Spinner pendant la vérification de disponibilité.',
},
},
},
};
/**
* État nom disponible.
*/
export const Available: Story = {
name: 'Disponible',
parameters: {
docs: {
description: {
story: 'Indicateur vert quand le nom est disponible.',
},
},
},
};

View file

@ -0,0 +1,53 @@
import type { Meta, StoryObj } from '@storybook/react';
import { DeleteAccountView } from './DeleteAccountView';
/**
* DeleteAccountView - Vue de suppression de compte
*
* Section permettant la suppression définitive du compte
* avec avertissements et confirmation multiple.
*/
const meta: Meta<typeof DeleteAccountView> = {
title: 'Components/Settings/Account/DeleteAccountView',
component: DeleteAccountView,
parameters: {
layout: 'padded',
docs: {
description: {
component: 'Zone de suppression de compte avec confirmation obligatoire.',
},
},
},
tags: ['autodocs'],
decorators: [
(Story) => (
<div className="bg-kodo-background min-h-screen p-4 max-w-4xl">
<Story />
</div>
),
],
};
export default meta;
type Story = StoryObj<typeof meta>;
/**
* État par défaut.
*/
export const Default: Story = {
name: 'Par défaut',
};
/**
* État de confirmation.
*/
export const Confirm: Story = {
name: 'Confirmation',
parameters: {
docs: {
description: {
story: 'Modal de confirmation avant suppression définitive.',
},
},
},
};

View file

@ -0,0 +1,39 @@
import type { Meta, StoryObj } from '@storybook/react';
import { AppearanceSettingsView } from './AppearanceSettingsView';
/**
* AppearanceSettingsView - Paramètres d'apparence
*
* Section des préférences visuelles : thème, couleur d'accent,
* animations et densité de l'interface.
*/
const meta: Meta<typeof AppearanceSettingsView> = {
title: 'Components/Settings/Appearance/AppearanceSettingsView',
component: AppearanceSettingsView,
parameters: {
layout: 'padded',
docs: {
description: {
component: 'Paramètres d\'apparence avec thème, couleurs et animations.',
},
},
},
tags: ['autodocs'],
decorators: [
(Story) => (
<div className="bg-kodo-background min-h-screen p-4 max-w-4xl">
<Story />
</div>
),
],
};
export default meta;
type Story = StoryObj<typeof meta>;
/**
* État par défaut des paramètres d'apparence.
*/
export const Default: Story = {
name: 'Par défaut',
};

View file

@ -0,0 +1,23 @@
import type { Meta, StoryObj } from '@storybook/react';
import { DataExportView } from './DataExportView';
const meta: Meta<typeof DataExportView> = {
title: 'Components/Settings/Data/DataExportView',
component: DataExportView,
parameters: { layout: 'padded' },
tags: ['autodocs'],
decorators: [
(Story) => (
<div className="bg-kodo-background p-4 min-h-screen">
<Story />
</div>
),
],
};
export default meta;
type Story = StoryObj<typeof meta>;
export const Default: Story = { name: 'Par défaut' };
export const Exporting: Story = { name: 'Export en cours' };
export const Ready: Story = { name: 'Prêt' };

View file

@ -0,0 +1,53 @@
import type { Meta, StoryObj } from '@storybook/react';
import { SecuritySettings } from './SecuritySettings';
/**
* SecuritySettings - Paramètres de sécurité
*
* Section des paramètres de sécurité incluant gestion
* des mots de passe, 2FA, et sessions actives.
*/
const meta: Meta<typeof SecuritySettings> = {
title: 'Components/Settings/Security/SecuritySettings',
component: SecuritySettings,
parameters: {
layout: 'padded',
docs: {
description: {
component: 'Paramètres de sécurité avec 2FA, mot de passe et sessions.',
},
},
},
tags: ['autodocs'],
decorators: [
(Story) => (
<div className="bg-kodo-background min-h-screen p-4 max-w-4xl">
<Story />
</div>
),
],
};
export default meta;
type Story = StoryObj<typeof meta>;
/**
* État par défaut des paramètres de sécurité.
*/
export const Default: Story = {
name: 'Par défaut',
};
/**
* État de mise à jour en cours.
*/
export const Updating: Story = {
name: 'Mise à jour',
parameters: {
docs: {
description: {
story: 'Feedback visuel pendant la mise à jour des paramètres de sécurité.',
},
},
},
};

View file

@ -0,0 +1,53 @@
import type { Meta, StoryObj } from '@storybook/react';
import { SessionManagement } from './SessionManagement';
/**
* SessionManagement - Gestion des sessions
*
* Composant affichant les sessions actives avec
* option de déconnexion individuelle ou globale.
*/
const meta: Meta<typeof SessionManagement> = {
title: 'Components/Settings/Security/SessionManagement',
component: SessionManagement,
parameters: {
layout: 'padded',
docs: {
description: {
component: 'Liste des sessions actives avec actions de déconnexion.',
},
},
},
tags: ['autodocs'],
decorators: [
(Story) => (
<div className="bg-kodo-background min-h-screen p-4 max-w-4xl">
<Story />
</div>
),
],
};
export default meta;
type Story = StoryObj<typeof meta>;
/**
* État par défaut avec sessions actives.
*/
export const Default: Story = {
name: 'Par défaut',
};
/**
* État avec plusieurs sessions.
*/
export const WithSessions: Story = {
name: 'Avec sessions',
parameters: {
docs: {
description: {
story: 'Affiche plusieurs sessions actives sur différents appareils.',
},
},
},
};

View file

@ -0,0 +1,108 @@
import type { Meta, StoryObj } from '@storybook/react';
import { fn } from '@storybook/test';
import { within, userEvent } from '@storybook/test';
import { TwoFactorSetup } from './TwoFactorSetup';
/**
* TwoFactorSetup - Configuration 2FA
*
* Assistant multi-étapes pour configurer l'authentification
* à deux facteurs avec QR code et codes de backup.
*/
const meta: Meta<typeof TwoFactorSetup> = {
title: 'Components/Settings/Security/TwoFactorSetup',
component: TwoFactorSetup,
parameters: {
layout: 'padded',
docs: {
description: {
component: 'Assistant de configuration 2FA en 3 étapes: choix de méthode, scan QR code, codes de backup.',
},
},
},
tags: ['autodocs'],
args: {
onBack: fn(),
onComplete: fn(),
},
argTypes: {
onBack: {
description: 'Callback appelé quand l\'utilisateur revient en arrière',
action: 'onBack',
},
onComplete: {
description: 'Callback appelé quand la configuration est terminée',
action: 'onComplete',
},
},
decorators: [
(Story) => (
<div className="max-w-2xl mx-auto p-4 bg-kodo-background min-h-screen">
<Story />
</div>
),
],
};
export default meta;
type Story = StoryObj<typeof meta>;
/**
* Étape 1 - Choix de la méthode d'authentification.
*/
export const Step1_ChooseMethod: Story = {
name: 'Étape 1: Choix de méthode',
parameters: {
docs: {
description: {
story: 'Première étape: choisir entre Authenticator App (TOTP) ou SMS.',
},
},
},
};
/**
* Étape 2 - Scan du QR code et vérification.
*/
export const Step2_QRCode: Story = {
name: 'Étape 2: QR Code',
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
// Cliquer sur "Authenticator App" pour passer à l'étape 2
const totpOption = canvas.getByText(/authenticator app/i);
await userEvent.click(totpOption);
},
parameters: {
docs: {
description: {
story: 'Deuxième étape: scanner le QR code et entrer le code de vérification.',
},
},
},
};
/**
* Étape 3 - Affichage des codes de backup.
*/
export const Step3_BackupCodes: Story = {
name: 'Étape 3: Codes de backup',
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
// Naviguer vers l'étape 3 (simulation)
// Note: Cela nécessite que l'API mock retourne des données valides
const totpOption = canvas.getByText(/authenticator app/i);
await userEvent.click(totpOption);
// Attendre le QR code et entrer un code
// Cette simulation est limitée sans mocks complets
},
parameters: {
docs: {
description: {
story: 'Troisième étape: sauvegarder les codes de backup avec options copier/télécharger.',
},
},
},
};

View file

@ -0,0 +1,24 @@
import type { Meta, StoryObj } from '@storybook/react';
import { fn } from '@storybook/test';
import { CreatePostModal } from './CreatePostModal';
const meta: Meta<typeof CreatePostModal> = {
title: 'Components/Social/CreatePostModal',
component: CreatePostModal,
parameters: { layout: 'centered' },
tags: ['autodocs'],
args: { onClose: fn() },
decorators: [
(Story) => (
<div className="bg-kodo-background min-h-screen flex items-center justify-center">
<Story />
</div>
),
],
};
export default meta;
type Story = StoryObj<typeof meta>;
export const Default: Story = { name: 'Par défaut' };
export const Posting: Story = { name: 'Envoi' };

View file

@ -0,0 +1,21 @@
import type { Meta, StoryObj } from '@storybook/react';
import { ExploreView } from './ExploreView';
const meta: Meta<typeof ExploreView> = {
title: 'Components/Social/ExploreView',
component: ExploreView,
parameters: { layout: 'fullscreen' },
tags: ['autodocs'],
decorators: [
(Story) => (
<div className="bg-kodo-background min-h-screen">
<Story />
</div>
),
],
};
export default meta;
type Story = StoryObj<typeof meta>;
export const Default: Story = { name: 'Par défaut' };

View file

@ -0,0 +1,23 @@
import type { Meta, StoryObj } from '@storybook/react';
import { FeedView } from './FeedView';
const meta: Meta<typeof FeedView> = {
title: 'Components/Social/FeedView',
component: FeedView,
parameters: { layout: 'padded' },
tags: ['autodocs'],
decorators: [
(Story) => (
<div className="bg-kodo-background p-4 min-h-screen">
<Story />
</div>
),
],
};
export default meta;
type Story = StoryObj<typeof meta>;
export const Default: Story = { name: 'Par défaut' };
export const Loading: Story = { name: 'Chargement' };
export const Empty: Story = { name: 'Vide' };

View file

@ -0,0 +1,22 @@
import type { Meta, StoryObj } from '@storybook/react';
import { ConnectionsView } from './ConnectionsView';
const meta: Meta<typeof ConnectionsView> = {
title: 'Components/Social/ConnectionsView',
component: ConnectionsView,
parameters: { layout: 'padded' },
tags: ['autodocs'],
decorators: [
(Story) => (
<div className="bg-kodo-background p-4 min-h-screen">
<Story />
</div>
),
],
};
export default meta;
type Story = StoryObj<typeof meta>;
export const Default: Story = { name: 'Par défaut' };
export const Empty: Story = { name: 'Vide' };

View file

@ -0,0 +1,23 @@
import type { Meta, StoryObj } from '@storybook/react';
import { GoLiveView } from './GoLiveView';
const meta: Meta<typeof GoLiveView> = {
title: 'Components/Studio/GoLiveView',
component: GoLiveView,
parameters: { layout: 'fullscreen' },
tags: ['autodocs'],
decorators: [
(Story) => (
<div className="bg-kodo-background min-h-screen">
<Story />
</div>
),
],
};
export default meta;
type Story = StoryObj<typeof meta>;
export const Setup: Story = { name: 'Configuration' };
export const Live: Story = { name: 'En direct' };
export const Ended: Story = { name: 'Terminé' };

View file

@ -0,0 +1,17 @@
import type { Meta, StoryObj } from '@storybook/react';
import { CreateProjectModal } from './CreateProjectModal';
import { fn } from '@storybook/test';
const meta: Meta<typeof CreateProjectModal> = {
title: 'Components/Studio/Projects/CreateProjectModal',
component: CreateProjectModal,
parameters: { layout: 'centered' },
tags: ['autodocs'],
args: { onClose: fn() },
};
export default meta;
type Story = StoryObj<typeof meta>;
export const Default: Story = { name: 'Par défaut' };
export const Creating: Story = { name: 'Création' };

View file

@ -0,0 +1,35 @@
import type { Meta, StoryObj } from '@storybook/react';
import { ProjectDetailView } from './ProjectDetailView';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { BrowserRouter } from 'react-router-dom';
const createMockQueryClient = () => new QueryClient({
defaultOptions: {
queries: { retry: false, staleTime: Infinity },
mutations: { retry: false },
},
});
const meta: Meta<typeof ProjectDetailView> = {
title: 'Components/Studio/Projects/ProjectDetailView',
component: ProjectDetailView,
parameters: { layout: 'fullscreen' },
tags: ['autodocs'],
decorators: [
(Story) => (
<QueryClientProvider client={createMockQueryClient()}>
<BrowserRouter>
<div className="bg-kodo-background min-h-screen">
<Story />
</div>
</BrowserRouter>
</QueryClientProvider>
),
],
};
export default meta;
type Story = StoryObj<typeof meta>;
export const Default: Story = { name: 'Par défaut' };
export const Loading: Story = { name: 'Chargement' };

View file

@ -0,0 +1,25 @@
import type { Meta, StoryObj } from '@storybook/react';
import { fn } from '@storybook/test';
import { BulkUploadModal } from './BulkUploadModal';
const meta: Meta<typeof BulkUploadModal> = {
title: 'Components/Upload/BulkUploadModal',
component: BulkUploadModal,
parameters: { layout: 'centered' },
tags: ['autodocs'],
args: { onClose: fn() },
decorators: [
(Story) => (
<div className="bg-kodo-background min-h-screen flex items-center justify-center">
<Story />
</div>
),
],
};
export default meta;
type Story = StoryObj<typeof meta>;
export const Default: Story = { name: 'Par défaut' };
export const Uploading: Story = { name: 'Upload en cours' };
export const Complete: Story = { name: 'Terminé' };

View file

@ -0,0 +1,23 @@
import type { Meta, StoryObj } from '@storybook/react';
import { FileUploadZone } from './FileUploadZone';
const meta: Meta<typeof FileUploadZone> = {
title: 'Components/Upload/FileUploadZone',
component: FileUploadZone,
parameters: { layout: 'centered' },
tags: ['autodocs'],
decorators: [
(Story) => (
<div className="bg-kodo-background p-8 w-[500px]">
<Story />
</div>
),
],
};
export default meta;
type Story = StoryObj<typeof meta>;
export const Default: Story = { name: 'Par défaut' };
export const Dragging: Story = { name: 'Drag en cours' };
export const Processing: Story = { name: 'Traitement' };

View file

@ -0,0 +1,23 @@
import type { Meta, StoryObj } from '@storybook/react';
import { UploadProgressBar } from './UploadProgressBar';
const meta: Meta<typeof UploadProgressBar> = {
title: 'Components/Upload/UploadProgressBar',
component: UploadProgressBar,
parameters: { layout: 'centered' },
tags: ['autodocs'],
decorators: [
(Story) => (
<div className="bg-kodo-background p-8 w-96">
<Story />
</div>
),
],
};
export default meta;
type Story = StoryObj<typeof meta>;
export const Default: Story = { name: 'Par défaut' };
export const Complete: Story = { name: 'Terminé' };
export const Error: Story = { name: 'Erreur' };

View file

@ -0,0 +1,25 @@
import type { Meta, StoryObj } from '@storybook/react';
import { fn } from '@storybook/test';
import { CoverArtUploadModal } from './CoverArtUploadModal';
const meta: Meta<typeof CoverArtUploadModal> = {
title: 'Components/Upload/CoverArtUploadModal',
component: CoverArtUploadModal,
parameters: { layout: 'centered' },
tags: ['autodocs'],
args: { onClose: fn() },
decorators: [
(Story) => (
<div className="bg-kodo-background min-h-screen flex items-center justify-center">
<Story />
</div>
),
],
};
export default meta;
type Story = StoryObj<typeof meta>;
export const Default: Story = { name: 'Par défaut' };
export const Uploading: Story = { name: 'Upload' };
export const Preview: Story = { name: 'Prévisualisation' };

View file

@ -0,0 +1,17 @@
import type { Meta, StoryObj } from '@storybook/react';
import { LyricsEditorModal } from './LyricsEditorModal';
import { fn } from '@storybook/test';
const meta: Meta<typeof LyricsEditorModal> = {
title: 'Components/Upload/Metadata/LyricsEditorModal',
component: LyricsEditorModal,
parameters: { layout: 'centered' },
tags: ['autodocs'],
args: { onClose: fn() },
};
export default meta;
type Story = StoryObj<typeof meta>;
export const Default: Story = { name: 'Par défaut' };
export const Syncing: Story = { name: 'Synchronisation' };

View file

@ -0,0 +1,23 @@
import type { Meta, StoryObj } from '@storybook/react';
import { MetadataEditor } from './MetadataEditor';
const meta: Meta<typeof MetadataEditor> = {
title: 'Components/Upload/MetadataEditor',
component: MetadataEditor,
parameters: { layout: 'padded' },
tags: ['autodocs'],
decorators: [
(Story) => (
<div className="bg-kodo-background p-4 min-h-screen max-w-2xl">
<Story />
</div>
),
],
};
export default meta;
type Story = StoryObj<typeof meta>;
export const Default: Story = { name: 'Par défaut' };
export const WithData: Story = { name: 'Avec données' };
export const Saving: Story = { name: 'Sauvegarde' };

View file

@ -0,0 +1,22 @@
import type { Meta, StoryObj } from '@storybook/react';
import { MetadataForm } from './MetadataForm';
const meta: Meta<typeof MetadataForm> = {
title: 'Components/Upload/Metadata/MetadataForm',
component: MetadataForm,
parameters: { layout: 'padded' },
tags: ['autodocs'],
decorators: [
(Story) => (
<div className="bg-kodo-background p-4 max-w-2xl">
<Story />
</div>
),
],
};
export default meta;
type Story = StoryObj<typeof meta>;
export const Default: Story = { name: 'Par défaut' };
export const WithErrors: Story = { name: 'Avec erreurs' };

View file

@ -0,0 +1,49 @@
import type { Meta, StoryObj } from '@storybook/react';
import { AdminView } from './AdminView';
/**
* AdminView - Vue principale d'administration
*
* Layout avec sidebar de navigation et zone de contenu
* pour basculer entre les différentes vues admin.
*/
const meta: Meta<typeof AdminView> = {
title: 'Components/Views/AdminView',
component: AdminView,
parameters: {
layout: 'fullscreen',
docs: {
description: {
component: 'Vue conteneur admin avec navigation sidebar vers sous-vues.',
},
},
},
tags: ['autodocs'],
argTypes: {
currentSubView: {
control: 'select',
options: ['dashboard', 'users', 'moderation', 'audit', 'settings'],
description: 'Sous-vue à afficher par défaut',
},
},
decorators: [
(Story) => (
<div className="bg-kodo-background min-h-screen p-4">
<Story />
</div>
),
],
};
export default meta;
type Story = StoryObj<typeof meta>;
/**
* Vue dashboard par défaut.
*/
export const Default: Story = {
name: 'Par défaut (Dashboard)',
args: {
currentSubView: 'dashboard',
},
};

View file

@ -0,0 +1,22 @@
import type { Meta, StoryObj } from '@storybook/react';
import { AnalyticsView } from './AnalyticsView';
const meta: Meta<typeof AnalyticsView> = {
title: 'Components/Views/AnalyticsView',
component: AnalyticsView,
parameters: { layout: 'fullscreen' },
tags: ['autodocs'],
decorators: [
(Story) => (
<div className="bg-kodo-background min-h-screen">
<Story />
</div>
),
],
};
export default meta;
type Story = StoryObj<typeof meta>;
export const Default: Story = { name: 'Par défaut' };
export const Loading: Story = { name: 'Chargement' };

View file

@ -0,0 +1,21 @@
import type { Meta, StoryObj } from '@storybook/react';
import { ChatView } from './ChatView';
const meta: Meta<typeof ChatView> = {
title: 'Components/Views/ChatView',
component: ChatView,
parameters: { layout: 'fullscreen' },
tags: ['autodocs'],
decorators: [
(Story) => (
<div className="bg-kodo-background min-h-screen">
<Story />
</div>
),
],
};
export default meta;
type Story = StoryObj<typeof meta>;
export const Default: Story = { name: 'Par défaut' };

View file

@ -0,0 +1,22 @@
import type { Meta, StoryObj } from '@storybook/react';
import { DiscoverView } from './DiscoverView';
const meta: Meta<typeof DiscoverView> = {
title: 'Components/Views/DiscoverView',
component: DiscoverView,
parameters: { layout: 'fullscreen' },
tags: ['autodocs'],
decorators: [
(Story) => (
<div className="bg-kodo-background min-h-screen">
<Story />
</div>
),
],
};
export default meta;
type Story = StoryObj<typeof meta>;
export const Default: Story = { name: 'Par défaut' };
export const Loading: Story = { name: 'Chargement' };

View file

@ -0,0 +1,21 @@
import type { Meta, StoryObj } from '@storybook/react';
import { EducationView } from './EducationView';
const meta: Meta<typeof EducationView> = {
title: 'Components/Views/EducationView',
component: EducationView,
parameters: { layout: 'fullscreen' },
tags: ['autodocs'],
decorators: [
(Story) => (
<div className="bg-kodo-background min-h-screen">
<Story />
</div>
),
],
};
export default meta;
type Story = StoryObj<typeof meta>;
export const Default: Story = { name: 'Par défaut' };

View file

@ -0,0 +1,24 @@
import type { Meta, StoryObj } from '@storybook/react';
import { GearView } from './GearView';
import { ToastProvider } from '../../components/feedback/ToastProvider';
const meta: Meta<typeof GearView> = {
title: 'Components/Views/GearView',
component: GearView,
parameters: { layout: 'fullscreen' },
tags: ['autodocs'],
decorators: [
(Story) => (
<ToastProvider>
<div className="bg-kodo-background min-h-screen">
<Story />
</div>
</ToastProvider>
),
],
};
export default meta;
type Story = StoryObj<typeof meta>;
export const Default: Story = { name: 'Par défaut' };

View file

@ -0,0 +1,21 @@
import type { Meta, StoryObj } from '@storybook/react';
import { LiveView } from './LiveView';
const meta: Meta<typeof LiveView> = {
title: 'Components/Views/LiveView',
component: LiveView,
parameters: { layout: 'fullscreen' },
tags: ['autodocs'],
decorators: [
(Story) => (
<div className="bg-kodo-background min-h-screen">
<Story />
</div>
),
],
};
export default meta;
type Story = StoryObj<typeof meta>;
export const Default: Story = { name: 'Par défaut' };

View file

@ -0,0 +1,21 @@
import type { Meta, StoryObj } from '@storybook/react';
import { NotificationsView } from './NotificationsView';
const meta: Meta<typeof NotificationsView> = {
title: 'Components/Views/NotificationsView',
component: NotificationsView,
parameters: { layout: 'fullscreen' },
tags: ['autodocs'],
decorators: [
(Story) => (
<div className="bg-kodo-background min-h-screen">
<Story />
</div>
),
],
};
export default meta;
type Story = StoryObj<typeof meta>;
export const Default: Story = { name: 'Par défaut' };

View file

@ -0,0 +1,22 @@
import type { Meta, StoryObj } from '@storybook/react';
import { ProfileView } from './ProfileView';
const meta: Meta<typeof ProfileView> = {
title: 'Components/Views/ProfileView',
component: ProfileView,
parameters: { layout: 'fullscreen' },
tags: ['autodocs'],
decorators: [
(Story) => (
<div className="bg-kodo-background min-h-screen">
<Story />
</div>
),
],
};
export default meta;
type Story = StoryObj<typeof meta>;
export const Default: Story = { name: 'Par défaut' };
export const Loading: Story = { name: 'Chargement' };

View file

@ -0,0 +1,23 @@
import type { Meta, StoryObj } from '@storybook/react';
import { PurchasesView } from './PurchasesView';
const meta: Meta<typeof PurchasesView> = {
title: 'Components/Views/PurchasesView',
component: PurchasesView,
parameters: { layout: 'fullscreen' },
tags: ['autodocs'],
decorators: [
(Story) => (
<div className="bg-kodo-background min-h-screen">
<Story />
</div>
),
],
};
export default meta;
type Story = StoryObj<typeof meta>;
export const Default: Story = { name: 'Par défaut' };
export const Empty: Story = { name: 'Vide' };
export const Loading: Story = { name: 'Chargement' };

View file

@ -0,0 +1,49 @@
import type { Meta, StoryObj } from '@storybook/react';
import { SettingsView } from './SettingsView';
/**
* SettingsView - Vue principale des paramètres
*
* Interface à onglets pour naviguer entre les différentes
* sections de paramètres (profil, compte, apparence, sécurité, etc.)
*/
const meta: Meta<typeof SettingsView> = {
title: 'Components/Views/SettingsView',
component: SettingsView,
parameters: {
layout: 'fullscreen',
docs: {
description: {
component: 'Vue avec navigation par onglets vers toutes les sections de paramètres.',
},
},
},
tags: ['autodocs'],
argTypes: {
initialTab: {
control: 'select',
options: ['profile', 'account', 'appearance', 'accessibility', 'security', 'integrations', 'cloud', 'backups', 'data', 'audio', 'notifications'],
description: 'Onglet à afficher par défaut',
},
},
decorators: [
(Story) => (
<div className="bg-kodo-background min-h-screen p-4">
<Story />
</div>
),
],
};
export default meta;
type Story = StoryObj<typeof meta>;
/**
* État par défaut avec onglet Profile.
*/
export const Default: Story = {
name: 'Par défaut (Profile)',
args: {
initialTab: 'profile',
},
};

View file

@ -0,0 +1,21 @@
import type { Meta, StoryObj } from '@storybook/react';
import { SocialView } from './SocialView';
const meta: Meta<typeof SocialView> = {
title: 'Components/Views/SocialView',
component: SocialView,
parameters: { layout: 'fullscreen' },
tags: ['autodocs'],
decorators: [
(Story) => (
<div className="bg-kodo-background min-h-screen">
<Story />
</div>
),
],
};
export default meta;
type Story = StoryObj<typeof meta>;
export const Default: Story = { name: 'Par défaut' };

View file

@ -0,0 +1,21 @@
import type { Meta, StoryObj } from '@storybook/react';
import { StudioView } from './StudioView';
const meta: Meta<typeof StudioView> = {
title: 'Components/Views/StudioView',
component: StudioView,
parameters: { layout: 'fullscreen' },
tags: ['autodocs'],
decorators: [
(Story) => (
<div className="bg-kodo-background min-h-screen">
<Story />
</div>
),
],
};
export default meta;
type Story = StoryObj<typeof meta>;
export const Default: Story = { name: 'Par défaut' };

View file

@ -0,0 +1,24 @@
import type { Meta, StoryObj } from '@storybook/react';
import { UploadView } from './UploadView';
const meta: Meta<typeof UploadView> = {
title: 'Components/Views/UploadView',
component: UploadView,
parameters: { layout: 'fullscreen' },
tags: ['autodocs'],
decorators: [
(Story) => (
<div className="bg-kodo-background min-h-screen">
<Story />
</div>
),
],
};
export default meta;
type Story = StoryObj<typeof meta>;
export const Default: Story = { name: 'Par défaut' };
export const Uploading: Story = { name: 'Upload en cours' };
export const Complete: Story = { name: 'Terminé' };
export const Error: Story = { name: 'Erreur' };

View file

@ -1,119 +0,0 @@
import { renderHook, act } from '@testing-library/react';
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { CartProvider, useCart } from './CartContext';
import { ReactNode } from 'react';
import { Product, ProductLicense } from '@/types';
import { ToastProvider } from './ToastContext';
// Mock useToast via ToastProvider
const mockAddToast = vi.fn();
vi.mock('./ToastContext', async () => {
const actual = await vi.importActual('./ToastContext');
return {
...actual,
ToastProvider: ({ children }: { children: ReactNode }) => children,
useToast: () => ({
addToast: mockAddToast,
}),
};
});
const mockProduct: Product = {
id: '1',
title: 'Test Product',
price: 9.99,
currency: 'USD',
type: 'track',
};
const wrapper = ({ children }: { children: ReactNode }) => (
<CartProvider>{children}</CartProvider>
);
describe('CartContext', () => {
beforeEach(() => {
vi.clearAllMocks();
});
it('should provide cart context', () => {
const { result } = renderHook(() => useCart(), { wrapper });
expect(result.current).toBeDefined();
expect(result.current).toHaveProperty('cart');
expect(result.current).toHaveProperty('addToCart');
expect(result.current).toHaveProperty('removeFromCart');
expect(result.current).toHaveProperty('clearCart');
});
it('should have empty cart initially', () => {
const { result } = renderHook(() => useCart(), { wrapper });
expect(result.current.cart).toEqual([]);
});
it('should add product to cart', () => {
const { result } = renderHook(() => useCart(), { wrapper });
act(() => {
result.current.addToCart(mockProduct);
});
expect(result.current.cart).toHaveLength(1);
expect(result.current.cart[0].id).toBe(mockProduct.id);
expect(result.current.cart[0].title).toBe(mockProduct.title);
});
it('should remove product from cart', () => {
const { result } = renderHook(() => useCart(), { wrapper });
act(() => {
result.current.addToCart(mockProduct);
});
expect(result.current.cart).toHaveLength(1);
const cartId = result.current.cart[0].cartId;
act(() => {
result.current.removeFromCart(cartId);
});
expect(result.current.cart).toHaveLength(0);
});
it('should clear cart', () => {
const { result } = renderHook(() => useCart(), { wrapper });
act(() => {
result.current.addToCart(mockProduct);
result.current.addToCart({ ...mockProduct, id: '2' });
});
expect(result.current.cart).toHaveLength(2);
act(() => {
result.current.clearCart();
});
expect(result.current.cart).toHaveLength(0);
});
it('should add product with license', () => {
const { result } = renderHook(() => useCart(), { wrapper });
const license: ProductLicense = {
id: 'std',
name: 'Standard',
price: 9.99,
features: ['Royalty Free'],
};
act(() => {
result.current.addToCart(mockProduct, license);
});
expect(result.current.cart).toHaveLength(1);
expect(result.current.cart[0].selectedLicense).toEqual(license);
});
});

View file

@ -1,83 +0,0 @@
import React, { createContext, useContext, useState } from 'react';
import { CartItem, Product, ProductLicense } from '../types';
import { useToast } from '@/components/feedback/ToastProvider';
interface CartContextType {
cart: CartItem[];
addToCart: (product: Product, license?: ProductLicense) => void;
removeFromCart: (cartId: string) => void;
clearCart: () => void;
cartTotal: number;
itemCount: number;
}
const CartContext = createContext<CartContextType | undefined>(undefined);
export const useCart = () => {
const context = useContext(CartContext);
if (!context) {
throw new Error('useCart must be used within a CartProvider');
}
return context;
};
export const CartProvider: React.FC<{ children: React.ReactNode }> = ({
children,
}) => {
const { addToast } = useToast();
const [cart, setCart] = useState<CartItem[]>([]);
const addToCart = (product: Product, license?: ProductLicense) => {
// Generate a unique ID for the cart item (productID + licenseID)
// This allows adding the same product with different licenses
const licenseId = license ? license.id : 'standard';
const existingItem = cart.find(
(item) =>
item.id === product.id && item.selectedLicense?.id === license?.id,
);
if (existingItem) {
addToast('Item already in cart', 'info');
return;
}
const newItem: CartItem = {
...product,
cartId: `${product.id}-${licenseId}-${Date.now()}`,
selectedLicense: license,
};
setCart((prev) => [...prev, newItem]);
addToast(`${product.title} added to cart`, 'success');
};
const removeFromCart = (cartId: string) => {
setCart((prev) => prev.filter((item) => item.cartId !== cartId));
};
const clearCart = () => {
setCart([]);
};
const cartTotal = cart.reduce((acc, item) => {
const price = item.selectedLicense
? item.selectedLicense.price
: item.price;
return acc + price;
}, 0);
return (
<CartContext.Provider
value={{
cart,
addToCart,
removeFromCart,
clearCart,
cartTotal,
itemCount: cart.length,
}}
>
{children}
</CartContext.Provider>
);
};

View file

@ -0,0 +1,113 @@
import type { Meta, StoryObj } from '@storybook/react';
import { AuthErrorMessage } from './AuthErrorMessage';
/**
* AuthErrorMessage - Affichage d'erreurs d'authentification
*
* Composant wrapper pour afficher les erreurs d'authentification
* de manière cohérente avec le design system.
*
* @deprecated Considérez d'utiliser ErrorDisplay directement pour les nouveaux développements.
*/
const meta: Meta<typeof AuthErrorMessage> = {
title: 'Features/Auth/AuthErrorMessage',
component: AuthErrorMessage,
parameters: {
layout: 'centered',
docs: {
description: {
component: 'Composant d\'affichage des erreurs d\'authentification. Utilise ErrorDisplay en interne.',
},
},
},
tags: ['autodocs'],
argTypes: {
message: {
control: 'text',
description: 'Message d\'erreur à afficher',
},
className: {
control: 'text',
description: 'Classes CSS additionnelles',
},
id: {
control: 'text',
description: 'ID unique pour l\'accessibilité',
},
},
decorators: [
(Story) => (
<div className="w-[400px] p-4">
<Story />
</div>
),
],
};
export default meta;
type Story = StoryObj<typeof meta>;
/**
* Message d'erreur par défaut.
*/
export const Default: Story = {
name: 'Par défaut',
args: {
message: 'Une erreur est survenue lors de l\'authentification.',
},
};
/**
* Erreur de réseau.
*/
export const Network: Story = {
name: 'Erreur réseau',
args: {
message: 'Impossible de se connecter au serveur. Vérifiez votre connexion internet.',
},
};
/**
* Erreur de validation.
*/
export const Validation: Story = {
name: 'Erreur de validation',
args: {
message: 'Email ou mot de passe incorrect.',
},
};
/**
* Message vide - ne rend rien.
*/
export const Empty: Story = {
name: 'Vide (pas de rendu)',
args: {
message: '',
},
parameters: {
docs: {
description: {
story: 'Quand le message est vide, le composant ne rend rien (retourne null).',
},
},
},
};
/**
* Avec ID personnalisé pour l'accessibilité.
*/
export const WithCustomId: Story = {
name: 'Avec ID personnalisé',
args: {
message: 'Session expirée. Veuillez vous reconnecter.',
id: 'login-error-message',
},
parameters: {
docs: {
description: {
story: 'L\'ID peut être utilisé pour lier le message d\'erreur à un champ de formulaire via aria-describedby.',
},
},
},
};

View file

@ -0,0 +1,40 @@
import type { Meta, StoryObj } from '@storybook/react';
import { AuthLayout } from './AuthLayout';
import { MemoryRouter } from 'react-router-dom';
const meta: Meta<typeof AuthLayout> = {
title: 'Features/Auth/AuthLayout',
component: AuthLayout,
parameters: {
layout: 'fullscreen',
},
tags: ['autodocs'],
decorators: [
(Story) => (
<MemoryRouter>
<Story />
</MemoryRouter>
),
],
};
export default meta;
type Story = StoryObj<typeof AuthLayout>;
export const Default: Story = {
args: {
title: 'Welcome Back',
subtitle: 'Please sign in to continue',
children: (
<div className="space-y-4">
<div className="h-10 bg-gray-100 rounded w-full border border-gray-200 flex items-center px-3 text-gray-400">Email</div>
<div className="h-10 bg-gray-100 rounded w-full border border-gray-200 flex items-center px-3 text-gray-400">Password</div>
<div className="h-10 bg-primary rounded w-full flex items-center justify-center text-white">Sign In</div>
</div>
),
footerLinks: [
{ label: 'Forgot Password?', to: '/forgot-password' },
{ label: "Don't have an account? Sign up", to: '/register' }
]
},
};

View file

@ -0,0 +1,89 @@
import type { Meta, StoryObj } from '@storybook/react';
import { within, userEvent } from '@storybook/test';
import { ForgotPasswordForm } from './ForgotPasswordForm';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
const createMockQueryClient = () => new QueryClient({
defaultOptions: {
queries: { retry: false, staleTime: Infinity },
mutations: { retry: false },
},
});
/**
* ForgotPasswordForm - Formulaire de récupération de mot de passe
*
* Composant de formulaire autonome pour demander un lien de
* réinitialisation de mot de passe.
*/
const meta: Meta<typeof ForgotPasswordForm> = {
title: 'Features/Auth/ForgotPasswordForm',
component: ForgotPasswordForm,
parameters: {
layout: 'centered',
docs: {
description: {
component: 'Formulaire de demande de réinitialisation de mot de passe.',
},
},
},
tags: ['autodocs'],
decorators: [
(Story) => (
<QueryClientProvider client={createMockQueryClient()}>
<div className="w-[400px]">
<Story />
</div>
</QueryClientProvider>
),
],
};
export default meta;
type Story = StoryObj<typeof meta>;
/**
* État par défaut du formulaire.
*/
export const Default: Story = {
name: 'Par défaut',
};
/**
* État de chargement pendant l'envoi.
*/
export const Loading: Story = {
name: 'Chargement',
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
// Remplir l'email
const emailInput = canvas.getByLabelText(/email/i);
await userEvent.type(emailInput, 'user@veza.music');
// Soumettre pour voir le loader
const submitButton = canvas.getByRole('button', { name: /envoyer/i });
await userEvent.click(submitButton);
},
parameters: {
docs: {
description: {
story: 'Montre le spinner pendant l\'envoi de la demande.',
},
},
},
};
/**
* État de succès après envoi.
*/
export const Success: Story = {
name: 'Succès - Email envoyé',
parameters: {
docs: {
description: {
story: 'Affiche le message de confirmation après envoi réussi.',
},
},
},
};

View file

@ -0,0 +1,119 @@
import type { Meta, StoryObj } from '@storybook/react';
import { fn } from '@storybook/test';
import { within, userEvent } from '@storybook/test';
import { TwoFactorVerify } from './TwoFactorVerify';
/**
* TwoFactorVerify - Vérification 2FA
*
* Composant pour saisir le code de vérification 2FA
* lors de la connexion avec authentification à deux facteurs.
*/
const meta: Meta<typeof TwoFactorVerify> = {
title: 'Features/Auth/TwoFactorVerify',
component: TwoFactorVerify,
parameters: {
layout: 'centered',
docs: {
description: {
component: 'Formulaire de vérification 2FA avec support des codes de backup.',
},
},
},
tags: ['autodocs'],
args: {
onSuccess: fn(),
onCancel: fn(),
},
argTypes: {
onSuccess: {
description: 'Callback appelé avec le code après vérification réussie',
action: 'onSuccess',
},
onCancel: {
description: 'Callback appelé quand l\'utilisateur annule',
action: 'onCancel',
},
},
};
export default meta;
type Story = StoryObj<typeof meta>;
/**
* État par défaut - saisie du code TOTP.
*/
export const Default: Story = {
name: 'Par défaut',
};
/**
* État avec erreur de vérification.
*/
export const WithError: Story = {
name: 'Avec erreur',
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
// Saisir un code invalide
const codeInput = canvas.getByLabelText(/verification code/i);
await userEvent.type(codeInput, '123456');
// Cliquer sur Verify
const verifyButton = canvas.getByRole('button', { name: /verify/i });
await userEvent.click(verifyButton);
},
parameters: {
docs: {
description: {
story: 'Affiche le message d\'erreur après un code invalide.',
},
},
},
};
/**
* État de chargement pendant la vérification.
*/
export const Loading: Story = {
name: 'Chargement',
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
// Saisir un code
const codeInput = canvas.getByLabelText(/verification code/i);
await userEvent.type(codeInput, '123456');
// Cliquer sur Verify pour voir le spinner
const verifyButton = canvas.getByRole('button', { name: /verify/i });
await userEvent.click(verifyButton);
},
parameters: {
docs: {
description: {
story: 'Montre le spinner pendant la vérification.',
},
},
},
};
/**
* Mode backup code.
*/
export const BackupCode: Story = {
name: 'Code de backup',
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
// Cliquer sur "Use a backup code"
const backupLink = canvas.getByRole('button', { name: /use a backup code/i });
await userEvent.click(backupLink);
},
parameters: {
docs: {
description: {
story: 'Interface pour utiliser un code de backup au lieu du TOTP.',
},
},
},
};

View file

@ -0,0 +1,100 @@
import type { Meta, StoryObj } from '@storybook/react';
import { within, userEvent } from '@storybook/test';
import { ForgotPasswordPage } from './ForgotPasswordPage';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
// Create a mocked QueryClient for stories
const createMockQueryClient = () => new QueryClient({
defaultOptions: {
queries: { retry: false, staleTime: Infinity },
mutations: { retry: false },
},
});
/**
* ForgotPasswordPage - Page de récupération de mot de passe
*
* Permet aux utilisateurs de demander un lien de réinitialisation
* de leur mot de passe par email.
*/
const meta: Meta<typeof ForgotPasswordPage> = {
title: 'Pages/Auth/ForgotPasswordPage',
component: ForgotPasswordPage,
parameters: {
layout: 'fullscreen',
docs: {
description: {
component: 'Page de demande de réinitialisation de mot de passe.',
},
},
},
tags: ['autodocs'],
decorators: [
(Story) => (
<QueryClientProvider client={createMockQueryClient()}>
<Story />
</QueryClientProvider>
),
],
};
export default meta;
type Story = StoryObj<typeof meta>;
/**
* État par défaut - formulaire de demande.
*/
export const Default: Story = {
name: 'Par défaut',
};
/**
* État après envoi réussi du lien.
*/
export const Sent: Story = {
name: 'Email envoyé',
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
// Remplir l'email
const emailInput = canvas.getByLabelText(/email/i);
await userEvent.type(emailInput, 'user@veza.music');
// Soumettre le formulaire
const submitButton = canvas.getByRole('button', { name: /envoyer/i });
await userEvent.click(submitButton);
// Le message de succès s'affichera après la mutation
},
parameters: {
docs: {
description: {
story: 'Affiche le message de confirmation après envoi du lien.',
},
},
},
};
/**
* État d'erreur - email non trouvé ou erreur serveur.
*/
export const WithError: Story = {
name: 'Avec erreur',
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
// Saisir un email invalide
const emailInput = canvas.getByLabelText(/email/i);
await userEvent.type(emailInput, 'invalid-email');
// Déclencher la validation
await userEvent.tab();
},
parameters: {
docs: {
description: {
story: 'Montre les erreurs de validation du formulaire.',
},
},
},
};

View file

@ -0,0 +1,109 @@
import type { Meta, StoryObj } from '@storybook/react';
import { within, userEvent, expect, fn } from '@storybook/test';
import { LoginPage } from './LoginPage';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
// Create a mocked QueryClient for stories
const createMockQueryClient = () => new QueryClient({
defaultOptions: {
queries: { retry: false, staleTime: Infinity },
mutations: { retry: false },
},
});
/**
* LoginPage - Page de connexion
*
* Page complète de connexion avec formulaire email/mot de passe,
* boutons OAuth, et liens vers l'inscription et la récupération de mot de passe.
*/
const meta: Meta<typeof LoginPage> = {
title: 'Pages/Auth/LoginPage',
component: LoginPage,
parameters: {
layout: 'fullscreen',
docs: {
description: {
component: 'Page de connexion avec authentification par email/mot de passe et OAuth.',
},
},
},
tags: ['autodocs'],
decorators: [
(Story) => (
<QueryClientProvider client={createMockQueryClient()}>
<Story />
</QueryClientProvider>
),
],
};
export default meta;
type Story = StoryObj<typeof meta>;
/**
* État par défaut de la page de connexion.
* Formulaire vide prêt à être rempli.
*/
export const Default: Story = {
name: 'Par défaut',
};
/**
* Simulation d'une erreur d'authentification.
* Montre le message d'erreur après une tentative de connexion échouée.
*/
export const WithError: Story = {
name: 'Avec erreur',
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
// Remplir les champs avec des données invalides
const emailInput = canvas.getByLabelText(/email/i);
const passwordInput = canvas.getByLabelText(/password/i);
await userEvent.type(emailInput, 'test@example.com');
await userEvent.type(passwordInput, 'wrongpassword');
// Soumettre le formulaire
const submitButton = canvas.getByRole('button', { name: /sign in/i });
await userEvent.click(submitButton);
// Note: Dans Storybook, l'erreur viendra du mock de useLogin
},
parameters: {
docs: {
description: {
story: 'Démontre l\'affichage d\'un message d\'erreur après une tentative de connexion échouée.',
},
},
},
};
/**
* État de chargement pendant la soumission du formulaire.
*/
export const Loading: Story = {
name: 'Chargement',
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
// Remplir le formulaire
const emailInput = canvas.getByLabelText(/email/i);
const passwordInput = canvas.getByLabelText(/password/i);
await userEvent.type(emailInput, 'user@veza.music');
await userEvent.type(passwordInput, 'SecurePass123!');
// Soumettre immédiatement pour montrer l'état de chargement
const submitButton = canvas.getByRole('button', { name: /sign in/i });
await userEvent.click(submitButton);
},
parameters: {
docs: {
description: {
story: 'Montre l\'état de chargement avec le spinner pendant la soumission.',
},
},
},
};

View file

@ -0,0 +1,115 @@
import type { Meta, StoryObj } from '@storybook/react';
import { within, userEvent } from '@storybook/test';
import { RegisterPage } from './RegisterPage';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
// Create a mocked QueryClient for stories
const createMockQueryClient = () => new QueryClient({
defaultOptions: {
queries: { retry: false, staleTime: Infinity },
mutations: { retry: false },
},
});
/**
* RegisterPage - Page d'inscription
*
* Page complète d'inscription avec formulaire de création de compte,
* validation en temps réel, indicateur de force du mot de passe,
* et vérification de disponibilité du nom d'utilisateur.
*/
const meta: Meta<typeof RegisterPage> = {
title: 'Pages/Auth/RegisterPage',
component: RegisterPage,
parameters: {
layout: 'fullscreen',
docs: {
description: {
component: 'Page d\'inscription avec validation complète et vérification email.',
},
},
},
tags: ['autodocs'],
decorators: [
(Story) => (
<QueryClientProvider client={createMockQueryClient()}>
<Story />
</QueryClientProvider>
),
],
};
export default meta;
type Story = StoryObj<typeof meta>;
/**
* État par défaut de la page d'inscription.
* Formulaire vide prêt à être rempli.
*/
export const Default: Story = {
name: 'Par défaut',
};
/**
* Simulation d'une erreur d'inscription.
* Par exemple, nom d'utilisateur déjà pris.
*/
export const WithError: Story = {
name: 'Avec erreur',
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
// Remplir les champs
const usernameInput = canvas.getByLabelText(/nom d'utilisateur/i);
const emailInput = canvas.getByLabelText(/email/i);
const passwordInput = canvas.getByLabelText(/^mot de passe \*/i);
const confirmPasswordInput = canvas.getByLabelText(/confirmer le mot de passe/i);
await userEvent.type(usernameInput, 'existinguser');
await userEvent.type(emailInput, 'new@veza.music');
await userEvent.type(passwordInput, 'SecurePass123!@#');
await userEvent.type(confirmPasswordInput, 'SecurePass123!@#');
// L'erreur de disponibilité sera affichée via le hook useUsernameAvailability
},
parameters: {
docs: {
description: {
story: 'Démontre l\'affichage d\'erreurs de validation en temps réel.',
},
},
},
};
/**
* État de succès après inscription.
* Affiche le message de vérification email.
*/
export const Success: Story = {
name: 'Succès - Vérification email',
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
// Remplir tous les champs avec des données valides
const usernameInput = canvas.getByLabelText(/nom d'utilisateur/i);
const emailInput = canvas.getByLabelText(/email/i);
const passwordInput = canvas.getByLabelText(/^mot de passe \*/i);
const confirmPasswordInput = canvas.getByLabelText(/confirmer le mot de passe/i);
const termsCheckbox = canvas.getByRole('checkbox');
await userEvent.type(usernameInput, 'newartist');
await userEvent.type(emailInput, 'artist@veza.music');
await userEvent.type(passwordInput, 'SuperSecure123!@#');
await userEvent.type(confirmPasswordInput, 'SuperSecure123!@#');
await userEvent.click(termsCheckbox);
// Le succès sera affiché après la mutation réussie
},
parameters: {
docs: {
description: {
story: 'Montre le message de succès avec instructions de vérification email.',
},
},
},
};

View file

@ -0,0 +1,118 @@
import type { Meta, StoryObj } from '@storybook/react';
import { within, userEvent } from '@storybook/test';
import { ResetPasswordPage } from './ResetPasswordPage';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { MemoryRouter, Routes, Route } from 'react-router-dom';
const createMockQueryClient = () => new QueryClient({
defaultOptions: {
queries: { retry: false, staleTime: Infinity },
mutations: { retry: false },
},
});
/**
* ResetPasswordPage - Page de réinitialisation de mot de passe
*
* Permet aux utilisateurs de définir un nouveau mot de passe
* après avoir cliqué sur le lien envoyé par email.
*/
const meta: Meta<typeof ResetPasswordPage> = {
title: 'Pages/Auth/ResetPasswordPage',
component: ResetPasswordPage,
parameters: {
layout: 'fullscreen',
docs: {
description: {
component: 'Page de réinitialisation de mot de passe avec indicateur de force.',
},
},
},
tags: ['autodocs'],
};
export default meta;
type Story = StoryObj<typeof meta>;
/**
* État par défaut avec un token valide.
* Affiche le formulaire de nouveau mot de passe.
*/
export const Default: Story = {
name: 'Par défaut (token valide)',
decorators: [
(Story) => (
<QueryClientProvider client={createMockQueryClient()}>
<MemoryRouter initialEntries={['/reset-password?token=valid-token-123']}>
<Routes>
<Route path="/reset-password" element={<Story />} />
</Routes>
</MemoryRouter>
</QueryClientProvider>
),
],
};
/**
* État de succès après réinitialisation.
*/
export const Success: Story = {
name: 'Succès',
decorators: [
(Story) => (
<QueryClientProvider client={createMockQueryClient()}>
<MemoryRouter initialEntries={['/reset-password?token=valid-token-123']}>
<Routes>
<Route path="/reset-password" element={<Story />} />
</Routes>
</MemoryRouter>
</QueryClientProvider>
),
],
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
// Remplir les champs
const passwordInput = canvas.getByLabelText(/nouveau mot de passe/i);
const confirmInput = canvas.getByLabelText(/confirmer le mot de passe/i);
await userEvent.type(passwordInput, 'NewSecurePass123!@#');
await userEvent.type(confirmInput, 'NewSecurePass123!@#');
// Soumettre
const submitButton = canvas.getByRole('button', { name: /réinitialiser/i });
await userEvent.click(submitButton);
},
parameters: {
docs: {
description: {
story: 'Affiche le message de succès après réinitialisation du mot de passe.',
},
},
},
};
/**
* État avec token invalide ou expiré.
*/
export const InvalidToken: Story = {
name: 'Token invalide',
decorators: [
(Story) => (
<QueryClientProvider client={createMockQueryClient()}>
<MemoryRouter initialEntries={['/reset-password']}>
<Routes>
<Route path="/reset-password" element={<Story />} />
</Routes>
</MemoryRouter>
</QueryClientProvider>
),
],
parameters: {
docs: {
description: {
story: 'Affiche le message d\'erreur quand le token est manquant ou invalide.',
},
},
},
};

View file

@ -0,0 +1,109 @@
import type { Meta, StoryObj } from '@storybook/react';
import { VerifyEmailPage } from './VerifyEmailPage';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { MemoryRouter, Routes, Route } from 'react-router-dom';
const createMockQueryClient = () => new QueryClient({
defaultOptions: {
queries: { retry: false, staleTime: Infinity },
mutations: { retry: false },
},
});
/**
* VerifyEmailPage - Page de vérification d'email
*
* Vérifie automatiquement l'email de l'utilisateur via le token
* reçu par email et affiche le statut approprié.
*/
const meta: Meta<typeof VerifyEmailPage> = {
title: 'Pages/Auth/VerifyEmailPage',
component: VerifyEmailPage,
parameters: {
layout: 'fullscreen',
docs: {
description: {
component: 'Page de vérification d\'email avec états de chargement, succès et erreur.',
},
},
},
tags: ['autodocs'],
};
export default meta;
type Story = StoryObj<typeof meta>;
/**
* État de vérification en cours (spinner).
*/
export const Pending: Story = {
name: 'Vérification en cours',
decorators: [
(Story) => (
<QueryClientProvider client={createMockQueryClient()}>
<MemoryRouter initialEntries={['/verify-email?token=verification-token-123']}>
<Routes>
<Route path="/verify-email" element={<Story />} />
</Routes>
</MemoryRouter>
</QueryClientProvider>
),
],
parameters: {
docs: {
description: {
story: 'Affiche un spinner pendant la vérification de l\'email.',
},
},
},
};
/**
* État de succès - email vérifié.
*/
export const Verified: Story = {
name: 'Email vérifié',
decorators: [
(Story) => (
<QueryClientProvider client={createMockQueryClient()}>
<MemoryRouter initialEntries={['/verify-email?token=valid-verified-token']}>
<Routes>
<Route path="/verify-email" element={<Story />} />
</Routes>
</MemoryRouter>
</QueryClientProvider>
),
],
parameters: {
docs: {
description: {
story: 'Affiche le message de succès après vérification.',
},
},
},
};
/**
* État d'erreur - token invalide ou expiré.
*/
export const Error: Story = {
name: 'Erreur de vérification',
decorators: [
(Story) => (
<QueryClientProvider client={createMockQueryClient()}>
<MemoryRouter initialEntries={['/verify-email']}>
<Routes>
<Route path="/verify-email" element={<Story />} />
</Routes>
</MemoryRouter>
</QueryClientProvider>
),
],
parameters: {
docs: {
description: {
story: 'Affiche l\'erreur avec options de réessayer et renvoyer l\'email.',
},
},
},
};

View file

@ -0,0 +1,24 @@
import type { Meta, StoryObj } from '@storybook/react';
import { fn } from '@storybook/test';
import { CreateRoomDialog } from './CreateRoomDialog';
const meta: Meta<typeof CreateRoomDialog> = {
title: 'Features/Chat/CreateRoomDialog',
component: CreateRoomDialog,
parameters: { layout: 'centered' },
tags: ['autodocs'],
args: { onClose: fn() },
decorators: [
(Story) => (
<div className="bg-kodo-background min-h-screen flex items-center justify-center">
<Story />
</div>
),
],
};
export default meta;
type Story = StoryObj<typeof meta>;
export const Default: Story = { name: 'Par défaut' };
export const Creating: Story = { name: 'Création' };

View file

@ -0,0 +1,23 @@
import type { Meta, StoryObj } from '@storybook/react';
import { MessageSearch } from './MessageSearch';
const meta: Meta<typeof MessageSearch> = {
title: 'Features/Chat/MessageSearch',
component: MessageSearch,
parameters: { layout: 'padded' },
tags: ['autodocs'],
decorators: [
(Story) => (
<div className="bg-kodo-background p-4 w-96">
<Story />
</div>
),
],
};
export default meta;
type Story = StoryObj<typeof meta>;
export const Default: Story = { name: 'Par défaut' };
export const Results: Story = { name: 'Résultats' };
export const NoResults: Story = { name: 'Sans résultats' };

View file

@ -0,0 +1,22 @@
import type { Meta, StoryObj } from '@storybook/react';
import { VirtualizedChatMessages } from './VirtualizedChatMessages';
const meta: Meta<typeof VirtualizedChatMessages> = {
title: 'Features/Chat/VirtualizedChatMessages',
component: VirtualizedChatMessages,
parameters: { layout: 'padded' },
tags: ['autodocs'],
decorators: [
(Story) => (
<div className="bg-kodo-background p-4 min-h-screen">
<Story />
</div>
),
],
};
export default meta;
type Story = StoryObj<typeof meta>;
export const Default: Story = { name: 'Par défaut' };
export const Loading: Story = { name: 'Chargement' };

View file

@ -0,0 +1,36 @@
import type { Meta, StoryObj } from '@storybook/react';
import { ChatPage } from './ChatPage';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { BrowserRouter } from 'react-router-dom';
const createMockQueryClient = () => new QueryClient({
defaultOptions: {
queries: { retry: false, staleTime: Infinity },
mutations: { retry: false },
},
});
const meta: Meta<typeof ChatPage> = {
title: 'Pages/Chat/ChatPage',
component: ChatPage,
parameters: { layout: 'fullscreen' },
tags: ['autodocs'],
decorators: [
(Story) => (
<QueryClientProvider client={createMockQueryClient()}>
<BrowserRouter>
<div className="bg-kodo-background min-h-screen">
<Story />
</div>
</BrowserRouter>
</QueryClientProvider>
),
],
};
export default meta;
type Story = StoryObj<typeof meta>;
export const Default: Story = { name: 'Par défaut' };
export const Loading: Story = { name: 'Chargement' };
export const Empty: Story = { name: 'Vide' };

View file

@ -0,0 +1,77 @@
import type { Meta, StoryObj } from '@storybook/react';
import DashboardPage from './DashboardPage';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
const createMockQueryClient = () => new QueryClient({
defaultOptions: {
queries: { retry: false, staleTime: Infinity },
mutations: { retry: false },
},
});
/**
* DashboardPage - Tableau de bord utilisateur
*
* Page principale du tableau de bord avec statistiques,
* activité récente, tracks récents et actions rapides.
*/
const meta: Meta<typeof DashboardPage> = {
title: 'Pages/Dashboard/DashboardPage',
component: DashboardPage,
parameters: {
layout: 'fullscreen',
docs: {
description: {
component: 'Tableau de bord utilisateur avec vue d\'ensemble des statistiques et activités.',
},
},
},
tags: ['autodocs'],
decorators: [
(Story) => (
<QueryClientProvider client={createMockQueryClient()}>
<div className="bg-kodo-background min-h-screen">
<Story />
</div>
</QueryClientProvider>
),
],
};
export default meta;
type Story = StoryObj<typeof meta>;
/**
* État par défaut du dashboard avec données chargées.
*/
export const Default: Story = {
name: 'Par défaut',
};
/**
* État de chargement des tracks récents.
*/
export const Loading: Story = {
name: 'Chargement',
parameters: {
docs: {
description: {
story: 'Affiche les squelettes de chargement pour les tracks récents.',
},
},
},
};
/**
* État vide - aucun track dans la bibliothèque.
*/
export const Empty: Story = {
name: 'Bibliothèque vide',
parameters: {
docs: {
description: {
story: 'Message affiché quand l\'utilisateur n\'a pas de tracks.',
},
},
},
};

View file

@ -0,0 +1,49 @@
import type { Meta, StoryObj } from '@storybook/react';
import NotFoundPage from './NotFoundPage';
/**
* NotFoundPage - Page d'erreur 404
*
* Affiche une page d'erreur conviviale lorsqu'une page n'est pas trouvée,
* avec des liens rapides vers les sections principales de l'application.
*/
const meta: Meta<typeof NotFoundPage> = {
title: 'Pages/Error/NotFoundPage',
component: NotFoundPage,
parameters: {
layout: 'fullscreen',
docs: {
description: {
component: 'Page 404 améliorée avec messages utiles et actions de récupération.',
},
},
},
tags: ['autodocs'],
};
export default meta;
type Story = StoryObj<typeof meta>;
/**
* État par défaut de la page 404.
* Affiche le code d'erreur, un message explicatif, des boutons d'action
* et des liens rapides vers les sections principales.
*/
export const Default: Story = {
name: 'Par défaut',
};
/**
* Même affichage avec les suggestions - les suggestions
* sont toujours visibles dans ce composant.
*/
export const WithSuggestions: Story = {
name: 'Avec suggestions',
parameters: {
docs: {
description: {
story: 'La page inclut toujours des suggestions pour aider l\'utilisateur à continuer sa navigation.',
},
},
},
};

View file

@ -0,0 +1,81 @@
import type { Meta, StoryObj } from '@storybook/react';
import { within, userEvent, expect } from '@storybook/test';
import ServerErrorPage from './ServerErrorPage';
/**
* ServerErrorPage - Page d'erreur 500
*
* Affiche une page d'erreur serveur avec options de récupération,
* informations de statut et détails techniques.
*/
const meta: Meta<typeof ServerErrorPage> = {
title: 'Pages/Error/ServerErrorPage',
component: ServerErrorPage,
parameters: {
layout: 'fullscreen',
docs: {
description: {
component: 'Page d\'erreur 500 améliorée avec messages utiles et actions de récupération.',
},
},
},
tags: ['autodocs'],
};
export default meta;
type Story = StoryObj<typeof meta>;
/**
* État par défaut de la page d'erreur serveur.
* Affiche le code d'erreur 500, un message explicatif,
* des boutons d'action et des informations d'aide.
*/
export const Default: Story = {
name: 'Par défaut',
};
/**
* Démonstration du comportement du bouton "Réessayer".
* Utilise une play function pour simuler le clic.
*/
export const WithRetry: Story = {
name: 'Avec action Réessayer',
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
// Trouver le bouton Réessayer
const retryButton = canvas.getByRole('button', { name: /réessayer/i });
// Vérifier que le bouton existe
await expect(retryButton).toBeInTheDocument();
// Cliquer sur le bouton pour voir l'état de chargement
await userEvent.click(retryButton);
// Vérifier que le texte change en "Réessai..."
await expect(canvas.getByText(/réessai/i)).toBeInTheDocument();
},
parameters: {
docs: {
description: {
story: 'Simule un clic sur le bouton Réessayer pour montrer l\'animation de chargement.',
},
},
},
};
/**
* Variante pour erreur réseau.
* Le même composant est utilisé, les détails techniques
* affichent les informations du navigateur.
*/
export const NetworkError: Story = {
name: 'Erreur réseau',
parameters: {
docs: {
description: {
story: 'Représentation d\'une erreur réseau - même composant avec contexte différent.',
},
},
},
};

View file

@ -0,0 +1,27 @@
import type { Meta, StoryObj } from '@storybook/react';
import { LibraryManager } from './LibraryManager';
import { Button } from '@/components/ui/button';
import { ToastProvider } from '@/components/feedback/ToastProvider';
const meta = {
title: 'Features/Library/LibraryManager',
component: LibraryManager,
tags: ['autodocs'],
decorators: [
(Story) => (
<ToastProvider>
<Story />
</ToastProvider>
),
],
} satisfies Meta<typeof LibraryManager>;
export default meta;
type Story = StoryObj<typeof meta>;
// NOTE: LibraryManager fetches data on mount via apiClient.
// Msw (Mock Service Worker) would be ideal here.
// For now, we rely on the fact that if fetch fails, it shows an empty state or error state, which IS a valid visual test.
// We can't easily patch apiClient here without a proper test harness.
export const Default: Story = {};

Some files were not shown because too many files have changed in this diff Show more