refactor: arch audit Phase 3 -- frontend TypeScript strict mode fixes

- Enable strict mode in frontend tsconfig.json
- Prefix unused parameters with _ across hooks and feature components
- Fix noUnusedLocals violations in useQueries, useTVNavigation, and
  various feature components (AlbumHero, ArtistHero, ArtistActionBar, etc.)

Addresses audit finding: TYPE-001 (frontend)
This commit is contained in:
Your Name
2026-03-20 21:42:57 -05:00
parent 0f9becfc79
commit 28872a20aa
21 changed files with 2036 additions and 29 deletions
+983
View File
@@ -0,0 +1,983 @@
# Component Catalog for SvelteKit Migration
**Generated:** 2026-03-20
**Total Components:** ~142 files across global and feature directories
---
## Global Components
### Layout Components
#### Sidebar
**Path:** `/components/layout/Sidebar.tsx` (512 lines)
**Props:** None (self-contained)
**Internal State:**
- `useState`: `isMobileMenuOpen`, `isSyncing`, `showCreatePlaylist`, `newPlaylistName`, `isCreating`
- `refs`: `syncTimeoutRef`, `createPopoverRef`
**Effects:**
- `useEffect`: Cleanup sync timeout on unmount
- `useEffect`: Close mobile menu on route change (depends: `pathname`)
- `useEffect`: Handle escape key for mobile menu (depends: `isMobileMenuOpen`)
- `useEffect`: Listen for `toggle-mobile-menu` custom event
- `useEffect`: Close create playlist popover on click outside (depends: `showCreatePlaylist`)
**Consumed Hooks/Contexts:**
- `usePathname`, `useRouter`: Next.js navigation
- `useAuth`: `isAuthenticated`
- `useToast`: `toast.error()`
- `useAudioState`: `currentTrack`, `currentAudiobook`, `currentPodcast`, `playbackType`
- `useIsMobile`, `useIsTablet`: Device detection
- `useQuery`: Fetch playlists from API
**API Calls:**
- `api.getPlaylists()`: Fetch user playlists
- `api.scanLibrary()`: Trigger library sync
- `api.createPlaylist(name)`: Create new playlist
**Child Components:**
- `MobileSidebar`: Props - `isOpen`, `onClose`
**Event Handlers:**
- `handleSync`: Triggers library scan, shows error toast on failure
- `handleCreatePlaylist`: Creates playlist, invalidates query, navigates to new playlist
**Next.js APIs:**
- `Link`: Navigation
- `usePathname`: Route detection
- `useRouter`: Programmatic navigation
- `Image`: Optimized image loading
---
#### TopBar
**Path:** `/components/layout/TopBar.tsx` (399 lines)
**Props:** None (self-contained)
**Internal State:**
- `useState`: `searchQuery`, `scanJobId`, `lastScanTime`
- `refs`: `searchTimeoutRef`, `searchInputRef`
**Effects:**
- `useEffect`: Handle scan completion/failure (depends: `scanStatus`, `scanJobId`)
- `useEffect`: Auto-search with debounce (depends: `searchQuery`, `router`, `pathname`)
- `useEffect`: Sync search query with URL on page change (depends: `pathname`)
- `useEffect`: Global "/" keyboard shortcut to focus search
**Consumed Hooks/Contexts:**
- `usePathname`, `useRouter`: Next.js navigation
- `useAuth`: `logout`
- `useToast`: `toast.success()`, `toast.error()`
- `useDownloadContext`: `pendingDownloads`, `downloadStatus`
- `useIsMobile`, `useIsTablet`: Device detection
- `useQuery`: Fetch scan status
- `useQueryClient`: Invalidate queries
**API Calls:**
- `api.scanLibrary()`: Trigger library scan
**Child Components:**
- `ActivityPanelToggle`: Standalone button component
**Event Handlers:**
- `handleSync`: Trigger library scan with 5s cooldown
- `handleLogout`: Logout user with toast feedback
- `handleSearch`: Navigate to search page
**Next.js APIs:**
- `Link`: Navigation
- `usePathname`: Route detection
- `useRouter`: Programmatic navigation
- `Image`: Logo display
---
#### BottomNavigation
**Path:** `/components/layout/BottomNavigation.tsx` (99 lines)
**Props:** None
**Internal State:** None (derived from props/hooks)
**Effects:** None
**Consumed Hooks/Contexts:**
- `usePathname`: Route matching
- `useIsMobile`, `useIsTablet`: Conditional rendering
**API Calls:** None
**Child Components:** None (renders `Link` directly)
**Event Handlers:** None (navigation via `Link`)
**Next.js APIs:**
- `Link`: Navigation with prefetch
---
#### MobileSidebar
**Path:** `/components/layout/MobileSidebar.tsx` (229 lines)
**Props:**
```typescript
interface MobileSidebarProps {
isOpen: boolean;
onClose: () => void;
}
```
**Internal State:**
- `useState`: `isSyncing`
- `refs`: `isFirstRender`, `syncTimeoutRef`
**Effects:**
- `useEffect`: Close on route change (skip initial mount)
- `useEffect`: Cleanup sync timeout
**Consumed Hooks/Contexts:**
- `usePathname`: Route detection
- `useAuth`: `logout`
- `useToast`: Toast feedback
- `useQueryClient`: Invalidate notifications
**API Calls:**
- `api.scanLibrary()`: Trigger library sync
**Event Handlers:**
- `handleSync`: Sync library, close menu on success
- `handleLogout`: Logout, close menu
---
#### AuthenticatedLayout
**Path:** `/components/layout/AuthenticatedLayout.tsx` (198 lines)
**Props:**
```typescript
interface AuthenticatedLayoutProps {
children: ReactNode;
}
```
**Internal State:** None (derived)
**Effects:**
- `useEffect`: Listen for activity panel events (toggle/open/close/set-tab)
**Consumed Hooks/Contexts:**
- `useAuth`: `isAuthenticated`, `isLoading`
- `usePathname`: Route detection for public paths
- `useIsMobile`, `useIsTablet`: Device detection
- `useIsTV`: TV detection
- `useActivityPanel`: Panel state management
- `useImportToasts`: Import notification toasts
**API Calls:** None (delegates to child components)
**Child Components:**
- `Sidebar`: Conditional rendering
- `TopBar`: Always rendered
- `TVLayout`: When `isTV`
- `BottomNavigation`: When mobile/tablet
- `ActivityPanel`: Mobile overlay
- `UnifiedPanel`: Desktop side panel
- `UniversalPlayer`: Player wrapper
- `MediaControlsHandler`: Media session controls
- `PlayerModeWrapper`: Player mode state
- `GalaxyBackground`: Background gradient
**Layout Logic:**
- Public pages (`/login`, `/register`, `/onboarding`, `/sync`, `/share/*`): Render children only
- TV: `TVLayout` with full keyboard navigation
- Mobile/Tablet: `TopBar` + `Sidebar` (mobile drawer) + `BottomNavigation` + `ActivityPanel` overlay
- Desktop: `TopBar` + `Sidebar` + `UnifiedPanel` side panel
---
#### TVLayout
**Path:** `/components/layout/TVLayout.tsx` (328 lines)
**Props:**
```typescript
interface TVLayoutProps {
children: React.ReactNode;
}
```
**Internal State:**
- `useState`: `focusedTabIndex`, `isNavFocused`, `isSyncing`
- `refs`: `navRef`, `currentTimeRef`, `durationRef`
**Effects:**
- `useEffect`: Add/remove `tv-mode` class on mount/unmount
- `useEffect`: Sync `currentTime` to ref
- `useEffect`: Sync `duration` to ref
- `useEffect`: Listen for keyboard events (global)
- `useEffect`: Focus correct nav tab when `isNavFocused` changes
- `useEffect`: Set correct focused tab on pathname change
**Consumed Hooks/Contexts:**
- `usePathname`, `useRouter`: Navigation
- `useAudio`: Full audio control (`currentTrack`, `isPlaying`, `pause`, `resumeWithGesture`, `next`, `previous`, `seek`, etc.)
- `useTVNavigation`: Content navigation hook
**API Calls:**
- `api.scanLibrary()`: Trigger sync
- `api.getCoverArtUrl()`: Generate cover art URLs
**Event Handlers:**
- `handleKeyDown`: Global keyboard handler (DPAD, media keys)
- `handleSync`: Library sync
**TV-Specific Features:**
- DPAD navigation for menu tabs
- Media key support (play/pause, next/previous, fast forward/rewind)
- Focus management with `data-tv-tab` attributes
- Content navigation via `useTVNavigation` hook
---
#### ActivityPanel
**Path:** `/components/layout/ActivityPanel.tsx` (301 lines)
**Props:**
```typescript
interface ActivityPanelProps {
isOpen: boolean;
onToggle: () => void;
activeTab?: ActivityTab; // "notifications" | "active" | "imports" | "history" | "settings"
onTabChange?: (tab: ActivityTab) => void;
}
```
**Internal State:**
- `useState`: `internalActiveTab` (fallback if prop not provided)
**Effects:**
- `useEffect`: Auto-switch to notifications if settings tab has no content
**Consumed Hooks/Contexts:**
- `useNotifications`: `unreadCount`
- `useActiveDownloads`: `downloads`
- `useIsMobile`, `useIsTablet`: Device detection
- `useActivityPanelSettings`: `settingsContent`, `setSettingsContent`
**Child Components (Tabs):**
- `NotificationsTab`: Notification feed
- `ActiveDownloadsTab`: Download progress
- `ImportsTab`: Import history
- `HistoryTab`: Playback history
**Event Handlers:**
- `handleTabClick`: Switch tabs, clear settings content when leaving settings tab
**Layout:**
- Mobile/Tablet: Full-screen overlay with backdrop
- Desktop: Collapsible side panel (48px collapsed, 450px expanded)
---
#### UnifiedPanel
**Path:** `/components/layout/UnifiedPanel.tsx` (109 lines)
**Props:**
```typescript
interface UnifiedPanelProps {
isOpen: boolean;
onToggle: () => void;
}
```
**Internal State:**
- `useState`: `activeTab` (default: "now-playing"), `expandedActivity`
**Consumed Hooks/Contexts:**
- `useNotifications`, `useActiveDownloads`: Activity badges
- `ActivityIconBar`: Activity type selector
- `ActivityHeader`, `ActivityContent`: Expanded activity views
- `TabBar`, `TabContent`: Default tab system
**Child Components:**
- `ActivityIconBar`: Props - `expandedActivity`, `onToggleActivity`
- `ActivityHeader`: Props - `type`, `onClose`
- `TabBar`: Props - `activeTab`, `onTabClick`
**Event Handlers:**
- `handleToggleActivity`: Expand/collapse activity type
**Layout:** Desktop-only collapsible panel (380px expanded, 48px collapsed)
---
### Player Components
#### UniversalPlayer
**Path:** `/components/player/UniversalPlayer.tsx` (58 lines)
**Props:** None
**Internal State:**
- `useRef`: `lastMediaIdRef`, `hasAutoSwitchedRef`
**Effects:**
- `useEffect`: Auto-switch to overlay mode on mobile when media starts playing (fires once per mount)
**Consumed Hooks/Contexts:**
- `useAudio`: `playerMode`, `setPlayerMode`, `currentTrack`, `isPlaying`
- `useIsMobile`, `useIsTablet`: Device detection
**Child Components (Conditional):**
- `OverlayPlayer`: When `playerMode === "overlay"` and has media
- `MiniPlayer`: Mobile/tablet default
- `FullPlayer`: Desktop default
**Logic:**
- Auto-opens overlay on mobile when user initiates playback
- Prevents re-opening on auto-advances (skips, queue)
---
#### MiniPlayer
**Path:** `/components/player/MiniPlayer.tsx` (689 lines)
**Props:** None
**Internal State:**
- `useState`: `isMinimized`, `isDismissed`, `swipeOffset`, `lastMediaId`
- `refs`: `touchStartX`
**Effects:**
- Derived state: Reset dismissed/minimized on media change or resume
**Consumed Hooks/Contexts:**
- `useAudioState`: `currentTrack`, `playbackType`, `isShuffle`, `repeatMode`, `activeOperation`
- `useAudioPlayback`: `isPlaying`, `isBuffering`, `canSeek`, `downloadProgress`, `audioError`, `clearAudioError`
- `useAudioControls`: `pause`, `resumeWithGesture`, `next`, `previous`, `toggleShuffle`, `toggleRepeat`, `seek`, `skipForward`, `skipBackward`, `setPlayerMode`
- `usePlaybackProgress`: `duration`, `progress`
- `useMediaInfo`: `title`, `subtitle`, `coverUrl`, `mediaLink`, `hasMedia`
- `useFeatures`: `vibeEmbeddings`, `loading`
- `useVibeToggle`: `handleVibeToggle`, `isVibeLoading`
**Event Handlers:**
- `handleTouchStart`, `handleTouchMove`, `handleTouchEnd`: Swipe gestures (right=minimize, left=open overlay/dismiss)
**Child Components:**
- `KeyboardShortcutsTooltip`: Desktop hints
- `SeekSlider`: Progress bar with seek
- `SleepTimer`: Sleep timer popover
- `ChevronLeft`: Minimized tab indicator
**Mobile-Specific Features:**
- Swipe RIGHT → minimize to corner tab
- Swipe LEFT + playing → open overlay
- Swipe LEFT + not playing → dismiss completely
- Gradient border with animated background
- Touch gesture feedback with opacity/transform
**Desktop Features:**
- Full-width bottom player
- Volume slider
- Shuffle/repeat/vibe/lyrics/queue toggles
- 30s skip buttons
- Keyboard shortcuts tooltip
---
#### FullPlayer
**Path:** `/components/player/FullPlayer.tsx` (584 lines)
**Props:** None
**Internal State:**
- `useState`: `showPlaylistSelector`
- `useMemo`: `vibeMatchScore`
**Consumed Hooks/Contexts:**
- `useAudioState`: Full state access
- `useAudioPlayback`: Playback status
- `useAudioControls`: All controls
- `usePlaybackProgress`: Time display
- `useMediaInfo`: Track metadata
- `useVibeToggle`: Vibe match
- `useFeatures`: Embeddings availability
- `useToast`: Success/error toasts
- `useAddToPlaylistMutation`: Add to playlist
- `useLyricsToggle`: Lyrics visibility
- `router`, `pathname`: Navigation
**Child Components:**
- `SeekSlider`: Progress bar
- `SleepTimer`: Sleep timer
- `KeyboardShortcutsTooltip`: Hints
- `PlaylistSelector`: Modal (conditional)
**Event Handlers:**
- `handleVolumeChange`: Volume slider
**Features:**
- Full playback controls
- Volume slider with mute toggle
- Vibe match score display (when active)
- Lyrics toggle button
- Queue navigation
- Add to playlist modal
- 30s skip buttons
- Shuffle/repeat controls
---
#### OverlayPlayer
**Path:** `/components/player/OverlayPlayer.tsx` (467 lines)
**Props:** None
**Internal State:**
- `useState`: `swipeOffset`
- `refs`: `touchStartX`
**Consumed Hooks/Contexts:**
- `useAudioState`, `useAudioPlayback`, `useAudioControls`: Full audio control
- `usePlaybackProgress`: Time display
- `useMediaInfo`: Track metadata
- `useVibeToggle`: Vibe mode
- `useLyricsToggle`: Lyrics visibility (mobile only)
- `useIsMobile`, `useIsTablet`: Device detection
**Event Handlers:**
- `handleTouchStart`, `handleTouchMove`, `handleTouchEnd`: Swipe for track skip (left=next, right=prev)
**Child Components:**
- `SeekSlider`: Progress bar
- `SleepTimer`: Sleep timer
- `MobileLyricsView`: Lyrics display (mobile, when active)
**Mobile-Specific Features:**
- Full-screen overlay (z-index 9999)
- Swipe LEFT/RIGHT on artwork to skip tracks
- Swipe feedback with transform/opacity
- Album art / lyrics swap (mobile only)
- Portrait vs landscape layout
- Safe area padding for iOS
---
#### SeekSlider
**Path:** `/components/player/SeekSlider.tsx` (273 lines)
**Props:**
```typescript
interface SeekSliderProps {
progress: number; // 0-100 percentage
duration: number; // seconds
onSeek: (time: number) => void;
canSeek: boolean;
hasMedia: boolean;
downloadProgress?: number | null;
className?: string;
showHandle?: boolean; // default: true
variant?: "default" | "minimal" | "overlay";
}
```
**Internal State:**
- `useState`: `isDragging`, `previewProgress`
- `refs`: `sliderRef`, `touchIdentifierRef`
**Effects:**
- `useEffect`: Add/remove global mouse listeners when dragging
**Event Handlers:**
- `handleTouchStart`, `handleTouchMove`, `handleTouchEnd`: Touch seeking with identifier tracking
- `handleMouseDown`, `handleMouseMove`, `handleMouseUp`: Mouse seeking
- `handleClick`: Click-to-seek
**Features:**
- Drag handle shows on hover/drag (configurable)
- Preview progress while dragging
- Tooltip text based on state (downloading, seeking disabled, etc.)
- Variant-specific styling (minimal for mini player, overlay for full screen)
- Prevents scroll/parent swipe during touch drag
---
#### SleepTimer
**Path:** `/components/player/SleepTimer.tsx` (151 lines)
**Props:**
```typescript
interface SleepTimerProps {
size?: "sm" | "md"; // default: "md"
}
```
**Internal State:**
- `useState`: `isOpen`, `customMinutes`
- `refs`: `popoverRef`, `buttonRef`
**Effects:**
- `useEffect`: Close popover on outside click or Escape key
**Consumed Hooks/Contexts:**
- `useSleepTimer`: `isActive`, `remainingSeconds`, `displayRemaining`, `setTimer`, `clearTimer`
**Event Handlers:**
- `handlePreset`: Set timer from preset (15, 30, 45, 60, 90, 120 mins)
- `handleCustom`: Set custom timer (1-480 mins)
**Features:**
- Preset buttons for common durations
- Custom input field
- Cancel button when active
- Popover closes on outside click or Escape
---
#### PlayerModeWrapper
**Path:** `/components/player/PlayerModeWrapper.tsx` (11 lines)
**Props:**
```typescript
interface PlayerModeWrapperProps {
children: ReactNode;
}
```
**Purpose:** Wrapper component that initializes `usePlayerMode` hook (must be client component)
**Consumed Hooks/Contexts:**
- `usePlayerMode`: Manages player mode state (mini/full/overlay)
---
### UI Components
#### Button
**Path:** `/components/ui/Button.tsx` (59 lines)
**Props:**
```typescript
interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
variant?: "primary" | "secondary" | "ghost" | "danger" | "ai" | "icon";
isLoading?: boolean;
}
```
**Pattern:** `forwardRef` + `memo` with `displayName`
**Variants:**
- `primary`: Brand color (#fca200), black text, shadow
- `secondary`: Dark bg (#1a1a1a), white text, border
- `ghost`: Transparent, gray text, hover bg
- `danger`: Red text, red border
- `ai`: Dark bg, brand text, brand hover
- `icon`: Fixed size (32x32), icon button
**Features:**
- Loading state with `GradientSpinner`
- Disabled state handling
- Focus ring accessibility
---
#### CachedImage
**Path:** `/components/ui/CachedImage.tsx` (34 lines)
**Props:**
```typescript
interface CachedImageProps extends Omit<ImageProps, "src"> {
src: string | null | undefined;
fill?: boolean;
}
```
**Pattern:** `memo` wrapper around Next.js `Image`
**Features:**
- Null src handling (returns null)
- Lazy loading by default
- Unoptimized (covers served via API, not static)
- Service Worker caching for `/api/library/cover-art/*`
---
#### EmptyState
**Path:** `/components/ui/EmptyState.tsx` (47 lines)
**Props:**
```typescript
interface EmptyStateProps {
icon: ReactNode;
title: string;
description: string;
children?: ReactNode;
action?: {
label: string;
onClick: () => void;
variant?: "primary" | "secondary" | "ghost";
};
}
```
**Pattern:** `memo`
**Usage:** Generic empty state for lists, searches, etc.
---
#### HomeHero
**Path:** `/features/home/components/HomeHero.tsx` (40 lines)
**Props:** None
**Features:**
- Time-based greeting (morning/afternoon/evening)
- System status indicator
- Two-line title with brand color accent
- Subtitle with page description
---
#### AlbumHero
**Path:** `/features/album/components/AlbumHero.tsx` (181 lines)
**Props:**
```typescript
interface AlbumHeroProps {
album: Album;
source: AlbumSource; // "library" | "discover" | "browse"
coverUrl: string | null;
colors: ColorPalette | null; // From VibrantJS
onReload: () => void;
children?: ReactNode; // Action bar
}
```
**Consumed Hooks/Contexts:**
- `useAlbumDisplayData`: Title/artist/year with user overrides
- `lazy` + `Suspense`: MetadataEditor (library albums only)
**Features:**
- Dynamic background gradient from album art colors
- Album cover with fallback icon
- Edit badge for user-overridden metadata
- MetadataEditor modal (lazy-loaded, library albums only)
- Duration formatting (hours/minutes)
- Genre tag display
---
## Feature Components Summary
### Album Features
- **TrackList**: Renders album tracks with play/download actions
- **AlbumActionBar**: Play all, shuffle, download, add to playlist buttons
- **SimilarAlbums**: Carousel of similar albums based on genre/year
### Artist Features
- **AvailableAlbums**: Grid of artist's albums
- **SimilarArtists**: Carousel of similar artists
- **ArtistHero**: Artist info with cover art and bio
- **PopularTracks**: Top tracks list
- **Discography**: Album grid by year
- **ArtistBio**: Artist biography (if available)
- **ArtistActionBar**: Follow, download, shuffle actions
### Audiobook Features
- **ChapterList**: Chapter list with progress tracking
- **AudiobookActionBar**: Play, download, add to library actions
- **AudiobookHero**: Book info with cover and author
### Discover Features
- **TrackList**: Discover tracks with preview player
- **HowItWorks**: Feature explanation modal
- **DiscoverActionBar**: Search, filter actions
- **UnavailableAlbums**: Shows albums not yet in library
- **DiscoverHero**: Discover page header
### Home Features
- **SectionHeader**: Reusable section title with "See all" link
- **HomeHero**: Greeting and status
- **PopularArtistsGrid**: Artist cards grid
- **FeaturedPlaylistsGrid**: Playlist cards
- **PodcastsGrid**: Podcast cards
- **AudiobooksGrid**: Audiobook cards
- **ContinueListening**: Recently played items
- **LibraryRadioStations**: Genre-based radio stations
- **MixesGrid**: AI-generated mix cards
- **ArtistsGrid**: Generic artist grid component
- **LibraryRadioStations**: Radio station generator
### Library Features
- **TracksList**: Library track list with delete/move actions
- **LibraryHeader**: Library stats and filters
- **LibraryToolbar**: View toggle (grid/list), sort dropdown
- **LibraryTabs**: Tab switcher (tracks/albums/artists)
- **AlbumsGrid**: Album grid with cover art
- **ArtistsGrid**: Artist grid with portraits
### Podcast Features
- **PodcastActionBar**: Subscribe, download actions
- **SimilarPodcasts**: Recommendations
- **PodcastHero**: Podcast info with cover
- **PreviewEpisodes**: Latest episodes (not in library)
- **ContinueListening**: In-progress episodes
- **EpisodeList**: Full episode list with progress
### Search Features
- **TopResult**: Featured result (album/artist/podcast)
- **UnifiedSongsList**: Combined search results
- **LibraryTracksList**: Library-only track results
- **LibraryAlbumsGrid**: Library album results
- **LibraryPodcastsGrid**: Library podcast results
- **LibraryAudiobooksGrid**: Library audiobook results
- **SoulseekBrowser**: External Soulseek search
- **SearchFilters**: Filter dropdown (type, source, quality)
- **AliasResolutionBanner**: Shows resolved artist aliases
- **SimilarArtistsGrid**: Artist recommendations
- **EmptyState**: No results message
- **TVSearchInput**: Search bar with TV navigation support
### Settings Features
**Sections:**
- **StoragePathsSection**: Library path configuration
- **SoulseekSection**: Soulseek client settings
- **SubsonicSection**: Subsonic API configuration
- **AccountSection**: User profile and password
- **CacheSection**: Cache size and cleanup
- **UserManagementSection**: Admin user management
- **CorruptTracksSection**: Track repair tools
- **PlaybackSection**: Audio playback settings
- **DownloadPreferencesSection**: Download quality/format
- **LidarrSection**: Lidarr integration
- **AIServicesSection**: CLAP/embedding service config
- **AudiobookshelfSection**: Audiobookshelf integration
**UI Components:**
- **SettingsLayout**: Main settings container with sidebar
- **SettingsSidebar**: Navigation sidebar
- **SettingsSection**: Section wrapper with title/description
- **SettingsRow**: Key-value row for settings
- **SettingsToggle**: Boolean toggle switch
- **SettingsSelect**: Dropdown selector
- **SettingsInput**: Text/number input
### Vibe Features
- **VibeMap**: Main 3D visualization component
- **VibeToolbar**: Control toolbar
- **VibePanelSheet**: Side panel with tabs
- **VibeSongPath**: Song path visualization
- **ActivityIconBar**: Activity type selector
- **GravityGridScene**: Three.js gravity simulation scene
- **LyricsTab**: Lyrics display in panel
- **NowPlayingTab**: Current track info
- **QueueTab**: Play queue in panel
- **panel-shared.tsx**: Shared panel components
---
## Third-Party Dependencies
### React Libraries
- **next**: Next.js 15 (App Router, Server Components)
- **react**: React 19 (hooks, memo, forwardRef, Suspense, lazy)
- **@tanstack/react-query**: Data fetching and caching
- **lucide-react**: Icon library
### UI/Styling
- **tailwindcss**: Utility-first CSS
- **clsx**: Conditional className merging
- **tailwind-merge**: Smart className merging
### Audio/Media
- **three**: 3D visualization (Vibe mode)
- **@react-three/fiber**: Three.js React renderer
- **@react-three/drei**: Three.js helpers
### Utilities
- **vibrant-js**: Image color extraction
- **howler**: Audio playback (via custom wrapper)
---
## Next.js-Specific APIs Used
### Routing
- `usePathname`: Active route detection
- `useRouter`: Programmatic navigation
- `Link`: Client-side navigation with prefetch
- File-based routing in `/app` directory
### Image Optimization
- `next/image`: Optimized image loading with lazy loading
- `unoptimized` prop used for API-served images
### Server Components
- Default: All components are Server Components unless marked `"use client"`
- Client components used for:
- Hooks (`useState`, `useEffect`, etc.)
- Browser APIs
- Event handlers
- Context providers/consumers
### API Routes
- `/app/api/events/route.ts`: SSE endpoint
- `/app/api/events/ticket/route.ts`: SSE ticket generation
---
## Component Patterns
### 1. forwardRef + memo Wrapper
All UI components use this pattern for performance and debugging:
```typescript
const Component = memo(forwardRef<HTMLDivElement, ComponentProps>(
({ className, ...props }, ref) => {
return <div ref={ref} className={cn(baseStyles, className)} {...props} />;
}
));
Component.displayName = "ComponentName";
```
### 2. cn() Utility
Class name merging using `clsx` + `tailwind-merge`:
```typescript
className={cn("base-styles", conditional && "conditional-styles", props.className)}
```
### 3. Split Context Pattern
Audio state split into 4 contexts to avoid re-renders:
- `AudioStateProvider`: Core state (current track, queue, shuffle, repeat)
- `AudioPlaybackProvider`: Playback status (playing, buffering, error)
- `AudioControlsProvider`: Control functions (play, pause, seek, skip)
- `AudioController`: Low-level audio element management
### 4. Query Key Factory
Type-safe query keys in `/hooks/useQueries.ts`:
```typescript
export const queryKeys = {
albums: (artistId?: string) => ["albums", artistId] as const,
tracks: (albumId?: string) => ["tracks", albumId] as const,
};
```
### 5. Lazy Loading
Modal components lazy-loaded to reduce initial bundle:
```typescript
const MetadataEditor = lazy(() => import("./MetadataEditor"));
// Usage with Suspense
<Suspense fallback={null}>
<MetadataEditor ... />
</Suspense>
```
### 6. Custom Event Bus
Cross-component communication via custom events:
```typescript
window.dispatchEvent(new CustomEvent("toggle-activity-panel"));
window.addEventListener("toggle-activity-panel", handler);
```
---
## Migration Considerations
### Next.js → SvelteKit Mapping
| Next.js | SvelteKit | Notes |
|---------|-----------|-------|
| `usePathname` | `$page.url.pathname` | Reactive in Svelte |
| `useRouter` | `goto()` from `$app/navigation` | Client-side navigation |
| `Link` | `a` tag with `data-sveltekit-preload-data` | Native SvelteKit prefetch |
| `next/image` | `svelte/image` or custom | Need caching strategy |
| Server Components | SSR + `{#if browser}` | Client-only code in `{@render}` or components |
| API Routes | Endpoint routes (`+server.ts`) | Similar pattern |
| `useEffect` | `onMount` | Lifecycle hook |
| `useState` | `let` with reactivity | Native Svelte reactivity |
| Context | Svelte `setContext`/`getContext` | Built-in |
| React Query | Svelte stores + custom hooks | Need to reimplement caching |
### Component Conversion Priority
**High Priority (Core UI):**
1. Button, EmptyState, CachedImage (UI primitives)
2. Sidebar, TopBar, BottomNavigation (layout)
3. MiniPlayer, FullPlayer, OverlayPlayer (player)
4. SeekSlider, SleepTimer (player controls)
**Medium Priority (Feature Pages):**
5. HomeHero, AlbumHero, ArtistHero (page headers)
6. Grid components (AlbumsGrid, ArtistsGrid, etc.)
7. List components (TrackList, EpisodeList, etc.)
**Low Priority (Advanced Features):**
8. VibeMap (Three.js integration - complex)
9. Settings sections (form-heavy)
10. Search features (Soulseek browser)
### State Management Migration
| React Pattern | Svelte Equivalent |
|---------------|-------------------|
| `useState` | `let` variable |
| `useReducer` | `writable` store |
| `useContext` | `setContext`/`getContext` |
| React Query | Custom stores + `derived` |
| `useMemo` | `derived` store or computed property |
| `useCallback` | Function in store or memoized with `derived` |
### Audio System Migration
The split audio context pattern should be preserved:
- Svelte stores for each concern (`audioState.svelte`, `audioPlayback.svelte`, etc.)
- Use `$state` runes in Svelte 5 for fine-grained reactivity
- Keep `AudioController` as a singleton service
---
## Summary Statistics
- **Total Components:** ~142 files
- **Layout Components:** 8 files
- **Player Components:** 9 files
- **UI Components:** 18 files
- **Feature Components:** ~90+ files across 10 features
- **Activity Tabs:** 5 files
- **Settings Sections:** 11 files
- **Settings UI:** 6 files
- **Vibe Components:** ~10 files (including scenes/tabs)
**Lines of Code (approximate):**
- Layout: ~2,000 lines
- Player: ~1,600 lines
- UI: ~500 lines
- Features: ~8,000+ lines
- **Total:** ~12,000+ lines of component code
**Most Complex Components:**
1. `MiniPlayer` (689 lines) - Gesture handling, multiple modes
2. `Sidebar` (512 lines) - Playlist management, responsive
3. `FullPlayer` (584 lines) - Full playback controls
4. `OverlayPlayer` (467 lines) - Full-screen mobile player
5. `TopBar` (399 lines) - Search, sync, notifications
6. `TVLayout` (328 lines) - Keyboard navigation
7. `ActivityPanel` (301 lines) - Multi-tab panel
8. `SeekSlider` (273 lines) - Touch/mouse seeking
**Components with Direct API Calls:**
- Sidebar, TopBar, MobileSidebar (sync, playlists)
- All feature action bar components
- Settings sections (save/load settings)
**Components Using Contexts:**
- All player components (audio contexts)
- Layout components (auth, toast, activity panel)
- Feature components (audio, features, query)
---
*End of Component Catalog*
@@ -0,0 +1,999 @@
# Feature Components Catalog for SvelteKit Migration
**Generated:** 2026-03-20
**Status:** Comprehensive inventory of all feature-specific components
---
## Album Features (`/features/album/components/`)
### TrackList
**Path:** `/features/album/components/TrackList.tsx` (297 lines)
**Props:**
```typescript
interface TrackListProps {
tracks: Track[];
album: Album;
source: AlbumSource; // "library" | "discover" | "browse"
currentTrackId: string | undefined;
colors: ColorPalette | null;
onPlayTrack: (track: Track, index: number) => void;
onAddToQueue: (track: Track) => void;
onAddToPlaylist: (trackId: string) => void;
previewTrack: string | null;
previewPlaying: boolean;
onPreview: (track: Track, e: React.MouseEvent) => void;
}
```
**Internal State:** None (pure presentational)
**Consumed Hooks/Contexts:** None (all logic passed via props)
**Child Components:** `TrackRow` (memoized with custom comparison)
**Event Handlers:**
- `handlePlayTrack`: Play local track
- `handleAddToQueue`: Add to playback queue
- `handleAddToPlaylist`: Open playlist selector
- `handlePreview`: Play preview for unowned tracks
- `handleRowClick`: Conditional play/preview based on ownership
**Features:**
- Dual-mode playback (local file vs preview)
- Missing track badges
- Preview-only badges for unowned albums
- Play count display (library only)
- Double-tap support via `useDoubleTap` hook
- TV navigation support (`data-tv-card`, `data-tv-card-index`)
- Custom memo comparison for performance
**Patterns:**
- `memo` with custom comparison function
- `useCallback` for all event handlers
- Conditional rendering based on `isOwned` prop
- Accessibility via `aria-label` and keyboard support
---
### AlbumActionBar
**Path:** `/features/album/components/AlbumActionBar.tsx` (103 lines)
**Props:**
```typescript
interface AlbumActionBarProps {
album: Album;
source: AlbumSource;
colors: ColorPalette | null;
onPlayAll: () => void;
onShuffle: () => void;
onDownloadAlbum: () => void;
onAddToPlaylist: () => void;
isPendingDownload: boolean;
isPlaying?: boolean;
isPlayingThisAlbum?: boolean;
onPause?: () => void;
}
```
**Internal State:** None (derived)
**Consumed Hooks/Contexts:** None
**Event Handlers:**
- `handlePlayPauseClick`: Conditional play/pause based on state
**Features:**
- Play all button (owned albums only)
- Shuffle button (owned albums only)
- Add to playlist button (owned albums only)
- Download button (unowned albums with MBID)
- Pause button when playing this album
- Cooldown logic for download (via parent)
**Patterns:**
- Conditional rendering based on `isOwned` and `showDownload`
- Disabled state for pending downloads
- Icon-only buttons for secondary actions
---
### SimilarAlbums
**Path:** `/features/album/components/SimilarAlbums.tsx` (39 lines)
**Props:**
```typescript
interface SimilarAlbumsProps {
similarAlbums: SimilarAlbum[];
colors: ColorPalette | null;
onNavigate: (albumId: string) => void;
}
```
**Internal State:** None
**Consumed Hooks/Contexts:** None
**Child Components:** `PlayableCard`, `SectionHeader`
**Features:**
- Grid layout (2-5 columns responsive)
- Navigation to album detail pages
- Owned badge for library albums
- TV navigation support
**Patterns:**
- Simple map/render pattern
- Reuses `SectionHeader` component
---
## Artist Features (`/features/artist/components/`)
### ArtistHero
**Path:** `/features/artist/components/ArtistHero.tsx` (177 lines)
**Props:**
```typescript
interface ArtistHeroProps {
artist: Artist;
source: ArtistSource;
albums: Album[];
heroImage: string | null;
backgroundImage?: string | null;
colors: ColorPalette | null;
onReload: () => void;
children?: ReactNode;
}
```
**Internal State:** None (derived)
**Consumed Hooks/Contexts:**
- `useArtistDisplayData`: Custom hook for metadata display with user overrides
**Child Components:**
- `MetadataEditor` (lazy-loaded via `Suspense`, library albums only)
**Features:**
- Dynamic background from hero image or gradient
- VibrantJS color extraction for overlays
- Circular artist image
- Edit badge for user-overridden metadata
- Listener count and album stats
- Conditional MetadataEditor modal (library only)
**Patterns:**
- Lazy loading with `Suspense`
- Conditional rendering based on source
- Background gradient from color palette
---
### ArtistActionBar
**Path:** `/features/artist/components/ArtistActionBar.tsx` (106 lines)
**Props:**
```typescript
interface ArtistActionBarProps {
artist: Artist;
albums: Album[];
source: ArtistSource;
colors: ColorPalette | null;
onPlayAll: () => void;
onShuffle: () => void;
onDownloadAll: () => void;
onStartRadio?: () => void;
isPendingDownload: boolean;
isPlaying?: boolean;
isPlayingThisArtist?: boolean;
onPause?: () => void;
}
```
**Internal State:** None (derived)
**Event Handlers:**
- `handlePlayPauseClick`: Conditional play/pause
**Features:**
- Play all button
- Shuffle button
- Radio button (library only)
- Download all button (discovery/unowned)
- Pause button when playing
**Patterns:**
- Similar to `AlbumActionBar`
- Conditional radio button for library artists
---
### PopularTracks
**Path:** `/features/artist/components/PopularTracks.tsx` (202 lines)
**Props:**
```typescript
interface PopularTracksProps {
tracks: Track[];
artist: Artist;
currentTrackId: string | undefined;
colors: ColorPalette | null;
onPlayTrack: (track: Track) => void;
previewTrack: string | null;
previewPlaying: boolean;
onPreview: (track: Track, e: React.MouseEvent) => void;
}
```
**Internal State:**
- `useRef`: `lastTapRef` for double-tap detection
**Consumed Hooks/Contexts:** None
**Features:**
- Top 10 tracks display
- Play count badges
- Preview mode for unowned tracks
- Double-tap support (touch and keyboard)
- Album art thumbnails
- TV navigation support
**Patterns:**
- Manual double-tap detection with `useRef`
- Inline event handlers with closure capture
- Conditional preview vs full playback
---
### AvailableAlbums
**Path:** `/features/artist/components/AvailableAlbums.tsx` (185 lines)
**Props:**
```typescript
interface AvailableAlbumsProps {
albums: Album[];
artistName: string;
source: ArtistSource;
colors: ColorPalette | null;
onDownloadAlbum: (album: Album, e: React.MouseEvent) => void;
isPendingDownload: (mbid: string) => boolean;
}
```
**Internal State:**
- `useState`: `coverArt` (lazy-loaded)
- `useState`: `fetchAttempted`
- `useEffect`: Lazy cover art fetch with staggered timing
**Child Components:** `LazyAlbumCard`, `AlbumGrid`, `SectionHeader`
**Features:**
- Lazy cover art loading for discovery albums
- Staggered fetch to avoid thundering herd
- Separation of studio albums vs EPs/singles
- Download tracking by MBID
- Year and type in subtitle
**Patterns:**
- Sub-component pattern (`LazyAlbumCard`, `AlbumGrid`)
- Effect-based lazy loading
- Staggered timing via `setTimeout` with index
---
### Discography
**Path:** `/features/artist/components/Discography.tsx` (84 lines)
**Props:**
```typescript
interface DiscographyProps {
albums: Album[];
colors: ColorPalette | null;
onPlayAlbum: (albumId: string, albumTitle: string) => Promise<void>;
sortBy: "year" | "dateAdded";
onSortChange: (sortBy: "year" | "dateAdded") => void;
}
```
**Internal State:** None (controlled component)
**Features:**
- Sort dropdown (year/date added)
- Play album action
- Track count in subtitle
- TV navigation support
**Patterns:**
- Controlled sort state via props
- Reuses `SectionHeader` with custom `rightAction`
---
### ArtistBio
**Path:** `/features/artist/components/ArtistBio.tsx` (23 lines)
**Props:**
```typescript
interface ArtistBioProps {
bio: string;
}
```
**Features:**
- HTML sanitization with `DOMPurify`
- Styled prose with Tailwind `prose` plugin
- Link styling overrides
**Patterns:**
- Minimal component, pure presentation
- `dangerouslySetInnerHTML` with sanitization
---
### SimilarArtists
**Path:** `/features/artist/components/SimilarArtists.tsx` (112 lines)
**Props:**
```typescript
interface SimilarArtistsProps {
similarArtists: SimilarArtist[];
onNavigate: (artistId: string) => void;
}
```
**Internal State:** None
**Features:**
- Circular artist cards
- Library indicator badge
- Match percentage display
- Owned album count
- Navigation to artist pages
- TV navigation support
**Patterns:**
- Manual keyboard handling (`onKeyDown`)
- Conditional library vs discovery navigation
- Image fallback with icon
---
## Home Features (`/features/home/components/`)
### SectionHeader
**Path:** `/features/home/components/SectionHeader.tsx` (56 lines)
**Props:**
```typescript
interface SectionHeaderProps {
title: string;
showAllHref?: string;
rightAction?: React.ReactNode;
badge?: string;
color?: "featured" | "tracks" | "albums" | "podcasts" | "audiobooks" | "artists" | "discover";
}
```
**Internal State:** None
**Features:**
- Color-coded gradient accent
- Optional "Show all" link
- Custom right action slot
- Badge support (e.g., "AI-generated")
**Patterns:**
- Reusable across all features
- Gradient color mapping via constant object
- `memo` wrapper
---
### PopularArtistsGrid
**Path:** `/features/home/components/PopularArtistsGrid.tsx` (81 lines)
**Props:**
```typescript
interface PopularArtistsGridProps {
artists: PopularArtist[];
}
```
**Internal State:** None
**Child Components:** `PopularArtistCard` (memoized), `HorizontalCarousel`
**Features:**
- Horizontal carousel layout
- Listener count display
- Search navigation on click
- Hover animations
- TV navigation support
**Patterns:**
- Sub-component pattern with memoization
- Reuses `HorizontalCarousel` component
---
### FeaturedPlaylistsGrid
**Path:** `/features/home/components/FeaturedPlaylistsGrid.tsx` (125 lines)
**Props:**
```typescript
interface FeaturedPlaylistsGridProps {
playlists: PlaylistPreview[];
}
```
**Internal State:** None
**Child Components:** `PlaylistCard` (memoized), `HorizontalCarousel`, `FeaturedPlaylistsSkeleton`
**Features:**
- Deezer playlist integration
- Custom Deezer icon
- Play button on hover
- Navigation to playlist pages
- Skeleton loading state
- Limit to 20 playlists
**Patterns:**
- Skeleton component for loading state
- Memoized card component
- `useCallback` for handler memoization
---
### ContinueListening
**Path:** `/features/home/components/ContinueListening.tsx` (155 lines)
**Props:**
```typescript
interface ContinueListeningProps {
items: ListenedItem[];
}
```
**Internal State:** None
**Child Components:** `ContinueListeningCard` (memoized), `HorizontalCarousel`
**Features:**
- Multi-type support (artist, podcast, audiobook)
- Progress bar for podcasts/audiobooks
- Type-specific icons and colors
- Navigation to respective detail pages
- Listener count in subtitle
**Patterns:**
- Type-based conditional rendering
- Color mapping by content type
- Reusable card pattern
---
### MixesGrid
**Path:** `/features/home/components/MixesGrid.tsx` (24 lines)
**Props:**
```typescript
interface MixesGridProps {
mixes: Mix[];
}
```
**Internal State:** None
**Child Components:** `MixCard`, `HorizontalCarousel`
**Features:**
- AI-generated mix display
- Reuses `MixCard` component
**Patterns:**
- Minimal wrapper component
- Delegates to specialized `MixCard`
---
## Library Features (`/features/library/components/`)
### LibraryHeader
**Path:** `/features/library/components/LibraryHeader.tsx` (40 lines)
**Props:**
```typescript
interface LibraryHeaderProps {
totalItems: number;
activeTab: string;
}
```
**Internal State:** None
**Features:**
- System status indicator
- Large title display
- Live item counter
- Tab-specific label
**Patterns:**
- Pure presentation component
- No Next.js-specific APIs
---
### LibraryToolbar
**Path:** `/features/library/components/LibraryToolbar.tsx` (93 lines)
**Props:**
```typescript
interface LibraryToolbarProps {
activeTab: Tab;
filter: LibraryFilter;
sortBy: SortOption;
itemsPerPage: number;
onFilterChange: (filter: LibraryFilter) => void;
onSortChange: (sort: SortOption) => void;
onItemsPerPageChange: (items: number) => void;
onShuffleLibrary: () => void;
}
```
**Internal State:** None (controlled)
**Features:**
- Filter pills (Owned/Discovery/All)
- Sort dropdown (tab-specific options)
- Items per page selector
- Shuffle library button
- Conditional filter display (artists/albums only)
**Patterns:**
- Controlled component pattern
- Filter pill constant array
- Tab-specific sort options
---
### TracksList
**Path:** `/features/library/components/TracksList.tsx` (255 lines)
**Props:**
```typescript
interface TracksListProps {
tracks: Track[];
onPlay: (tracks: Track[], startIndex?: number) => void;
onAddToQueue: (track: Track) => void;
onAddToPlaylist: (playlistId: string, trackId: string) => void;
onDelete: (trackId: string, trackTitle: string) => void;
isLoading?: boolean;
}
```
**Internal State:**
- `useState`: `showPlaylistSelector`
- `useState`: `selectedTrackId`
**Consumed Hooks/Contexts:**
- `useAudioState`: `currentTrack`
**Child Components:** `TrackRow` (memoized), `PlaylistSelector`
**Event Handlers:**
- `handleShowAddToPlaylist`: Open playlist selector
- `handleAddToPlaylist`: Add track to playlist
**Features:**
- Terminal-style header row
- Album column (desktop only)
- Action buttons on hover
- Double-tap support
- Loading state with spinner
- Empty state fallback
- Playlist selector modal
**Patterns:**
- Memoized row with custom comparison
- `useCallback` for handlers
- Conditional modal rendering
- `useDoubleTap` hook integration
---
## Search Features (`/features/search/components/`)
### TopResult
**Path:** `/features/search/components/TopResult.tsx` (97 lines)
**Props:**
```typescript
interface TopResultProps {
libraryArtist?: Artist;
discoveryArtist?: DiscoverResult;
}
```
**Internal State:** None
**Features:**
- Large artist card display
- Library vs discovery detection
- Background image with overlay
- Gradient border on hover
- External link icon
**Patterns:**
- Conditional rendering based on source
- Large hero-style card
- Hover animations
---
### SoulseekBrowser
**Path:** `/features/search/components/SoulseekBrowser.tsx` (532 lines)
**Props:**
```typescript
interface SoulseekBrowserProps {
results: SoulseekResult[];
isSearching: boolean;
isPolling: boolean;
isComplete: boolean;
uniqueUserCount: number;
downloadingFiles: Set<string>;
onDownload: (result: SoulseekResult) => void;
onBulkDownload: (results: SoulseekResult[]) => void;
}
```
**Internal State:**
- `useState`: `formatFilters` (Set)
- `useState`: `sortField`
- `useState`: `viewMode` (flat/grouped)
- `useState`: `selectedKeys` (Set)
- `useState`: `displayLimit` (infinite scroll)
- `useState`: `expandedGroups` (Set)
- `useState`: `groupsInitialized`
- `useRef`: `sentinelRef` (IntersectionObserver)
- `useMemo`: `filtered`, `sorted`, `grouped`, `selectedResults`
- `useEffect`: IntersectionObserver for infinite scroll
- `useCallback`: All event handlers
**Child Components:** `FlatView`, `GroupedView`, `ResultRow`
**Features:**
- Format filter pills (FLAC/320+/256+)
- Sort by quality/bitrate/size/filename
- Flat or grouped view (by username)
- Infinite scroll with IntersectionObserver
- Bulk selection and download
- Real-time search status
- File metadata parsing
- Quality badges
- Download progress tracking
**Patterns:**
- Complex state management with Sets
- Memoized filtered/sorted results
- Sub-component pattern (FlatView/GroupedView)
- Infinite scroll with sentinel
- Group expansion state
- Helper functions for filename parsing
**Third-party:**
- Custom helper functions from `soulseekHelpers.tsx`
---
## Podcast Features (`/features/podcast/components/`)
### PodcastHero
**Path:** `/features/podcast/components/PodcastHero.tsx` (176 lines)
**Props:**
```typescript
interface PodcastHeroProps {
title: string;
author: string;
description?: string;
genres?: string[];
heroImage: string | null;
colors: ColorPalette | null;
episodeCount: number;
inProgressCount: number;
children?: ReactNode;
}
```
**Internal State:** None
**Consumed Hooks/Contexts:**
- `useRouter`: Navigation
**Features:**
- Back navigation button
- System status indicator
- Cover art display
- Episode count and progress
- Genre tags
- Description truncation (HTML stripped)
**Patterns:**
- Similar to `AlbumHero`/`ArtistHero`
- HTML sanitization for description
- Conditional genre display
---
### PodcastActionBar
**Path:** `/features/podcast/components/PodcastActionBar.tsx` (135 lines)
**Props:**
```typescript
interface PodcastActionBarProps {
isSubscribed: boolean;
feedUrl?: string;
colors: ColorPalette | null;
isSubscribing: boolean;
showDeleteConfirm: boolean;
onSubscribe: () => void;
onRemove: () => void;
onShowDeleteConfirm: (show: boolean) => void;
onPlayLatest?: () => void;
isPlayingPodcast?: boolean;
onPause?: () => void;
onRefresh?: () => Promise<unknown>;
isRefreshing?: boolean;
}
```
**Internal State:** None (controlled)
**Features:**
- Subscribe/Unsubscribe toggle
- Play latest episode
- Refresh for new episodes
- RSS feed link
- Delete confirmation flow
- Loading states
**Patterns:**
- Confirmation dialog inline
- Spinner for async operations
- Conditional action buttons
---
### EpisodeList
**Path:** `/features/podcast/components/EpisodeList.tsx` (280 lines)
**Props:**
```typescript
interface EpisodeListProps {
podcast: Podcast;
episodes: Episode[];
sortOrder: "newest" | "oldest";
onSortOrderChange: (order: "newest" | "oldest") => void;
isEpisodePlaying: (episodeId: string) => boolean;
isPlaying: boolean;
onPlayPause: (episode: Episode) => void;
onPlay: (episode: Episode) => void;
onMarkComplete?: (episodeId: string, duration: number) => void;
}
```
**Internal State:**
- `useState`: `expanded` (per episode description)
- `useRef`: `lastTapRef` (double-tap)
**Consumed Hooks/Contexts:** None
**Child Components:** `EpisodeRow`
**Features:**
- Sort toggle (newest/oldest)
- Progress bar per episode
- Season/episode number display
- Description expand/collapse
- HTML sanitization
- Double-tap support
- Mark as complete button
- Finished badge
**Patterns:**
- Sub-component pattern
- Inline description expansion
- Progress tracking
- Conditional complete button
---
## Audiobook Features (`/features/audiobook/components/`)
### AudiobookHero
**Path:** `/features/audiobook/components/AudiobookHero.tsx` (183 lines)
**Props:**
```typescript
interface AudiobookHeroProps {
audiobook: Audiobook;
heroImage: string | null;
colors: ColorPalette | null;
metadata: {
narrator: string | null;
genre: string | null;
publishedYear: string | null;
description: string | null;
} | null;
formatTime: (seconds: number) => string;
children?: ReactNode;
}
```
**Internal State:** None (derived)
**Consumed Hooks/Contexts:**
- `useRouter`: Navigation
**Features:**
- Back navigation
- Cover art display
- Narrator and genre metadata
- Progress percentage
- Series information
- Duration formatting
- Description truncation
**Patterns:**
- Similar to `PodcastHero`/`AlbumHero`
- Type-specific color scheme (amber/orange)
---
### ChapterList
**Path:** `/features/audiobook/components/ChapterList.tsx` (54 lines)
**Props:**
```typescript
interface ChapterListProps {
chapters: AudiobookChapter[];
onSeekToChapter: (startTime: number) => void;
formatTime: (seconds: number) => string;
}
```
**Internal State:** None
**Features:**
- Chapter list display
- Click to seek
- Start time display
- Hidden if > 50 chapters
**Patterns:**
- Minimal component
- Reuses `SectionHeader` pattern
---
## Discover Features (`/features/discover/components/`)
### DiscoverHero
**Path:** `/features/discover/components/DiscoverHero.tsx` (84 lines)
**Props:**
```typescript
interface DiscoverHeroProps {
playlist: DiscoverPlaylist | null;
config: DiscoverConfig | null;
onOpenSettings: () => void;
}
```
**Internal State:** None
**Consumed Hooks/Contexts:**
- `date-fns`: Date formatting
**Features:**
- Settings button (top right)
- Week start date display
- Track count
- Total duration calculation
- Last generated timestamp
**Patterns:**
- Absolute positioning for settings button
- Duration formatting helper
- Date formatting with `date-fns`
---
### HowItWorks
**Path:** `/features/discover/components/HowItWorks.tsx` (56 lines)
**Props:** None
**Internal State:** None
**Features:**
- Feature explanation modal content
- Step-by-step guide
- Chevron icons for steps
**Patterns:**
- Pure presentation component
- Reuses `Card` component
---
## Key Observations
### Common Patterns Across Features
1. **Hero Components**: All features use similar hero layout:
- Background image/gradient
- System status indicator
- Title and metadata
- Action bar as children
- Consistent spacing and typography
2. **ActionBar Components**: Standard action patterns:
- Play/pause toggle
- Secondary actions (shuffle, download, etc.)
- Loading states
- Conditional rendering based on ownership
3. **List Components**: Track/episode/chapter lists:
- Memoized row components
- Double-tap support
- Progress indicators
- Action buttons on hover
- TV navigation support
4. **Grid Components**: Album/artist/podcast grids:
- Responsive column counts (2-5)
- Horizontal carousel for home
- PlayableCard reuse
- Badge system (owned, download, etc.)
5. **State Management**:
- Controlled components (state in parent)
- Local state for UI concerns (modals, expansion)
- Refs for gesture handling (double-tap)
- No component-level data fetching (via React Query in pages)
### Next.js APIs Used in Features
- `useRouter` / `usePathname`: Navigation
- `Link`: Client-side navigation
- `next/image`: Optimized images
- File-based routing for detail pages
### Third-Party Dependencies in Features
- `date-fns`: Date formatting
- `DOMPurify`: HTML sanitization
- `lucide-react`: Icons
- `clsx` / `tailwind-merge`: className merging
- VibrantJS (via hooks): Color extraction
### SvelteKit Migration Considerations
1. **Navigation**: Replace `useRouter` with `goto()` from `$app/navigation`
2. **Images**: Replace `next/image` with custom image component or SvelteKit's image optimization
3. **State**: Convert React hooks to Svelte stores or reactive declarations
4. **Events**: Replace `onClick` with Svelte's event handlers
5. **Conditional Rendering**: Replace `{condition && <Component />}` with `{#if condition}...{/if}`
6. **Lists**: Replace `.map()` with `{#each}` blocks
7. **Refs**: Replace `useRef` with Svelte bindings or `bind:this`
8. **Effects**: Replace `useEffect` with Svelte's reactive statements or `onMount`
9. **Memoization**: Replace `memo`/`useMemo` with Svelte's `$derived` or stores
---
*End of Feature Components Catalog*
+24
View File
@@ -0,0 +1,24 @@
# Task: Frontend Catalog for SvelteKit Migration
**Started:** 2026-03-20
**Goal:** Complete inventory of all pages, components, hooks, contexts, stores, API calls, and dependencies for migration to SvelteKit + Svelte 5
**Spec:** None (exploratory cataloging)
## Steps
- [x] Phase 1: Directory tree analysis (app/ and lib/)
- [ ] Phase 2: Pages catalog (routes, data fetching, params)
- [ ] Phase 3: Components catalog (props, state, effects, children)
- [ ] Phase 4: React Query hooks catalog (query keys, endpoints, cache config)
- [ ] Phase 5: Context providers catalog (state, consumers)
- [ ] Phase 6: Stores catalog (Zustand/other shape and actions)
- [ ] Phase 7: WebSocket/SSE connections catalog
- [ ] Phase 8: Third-party libraries catalog
## Agent Log
- 12:05 orchestrator: Task started, reviewed existing catalog files
## Notes
<!-- Key findings go here -->
## Blockers
<!-- None -->
+4 -4
View File
@@ -50,7 +50,7 @@ export default function AlbumPage({ params }: AlbumPageProps) {
const { previewTrack, previewPlaying, handlePreview } = useTrackPreview();
const combinedTracks = useMemo<AlbumTrack[]>(() => {
const ownedTracks = (album?.tracks || []).map((track) => ({
const ownedTracks = (album?.tracks || []).map((track: AlbumTrack) => ({
...track,
trackNumber: track.trackNumber ?? track.trackNo,
isMissing: false,
@@ -81,14 +81,14 @@ export default function AlbumPage({ params }: AlbumPageProps) {
});
const numberedMissing = missingTracks
.filter((track) => typeof track.trackNumber === "number")
.filter((track: AlbumTrack) => typeof track.trackNumber === "number")
.sort(
(a, b) =>
(a: AlbumTrack, b: AlbumTrack) =>
(a.trackNumber as number) - (b.trackNumber as number)
);
const unnumberedMissing = missingTracks.filter(
(track) => typeof track.trackNumber !== "number"
(track: AlbumTrack) => typeof track.trackNumber !== "number"
);
const merged: AlbumTrack[] = [];
+1 -1
View File
@@ -115,7 +115,7 @@ export default function ArtistPage() {
}));
const startIndex = formattedTracks.findIndex(
(t) => t.id === track.id,
(t: { id: string }) => t.id === track.id,
);
playTracks(formattedTracks, Math.max(0, startIndex));
}
+1 -1
View File
@@ -585,7 +585,7 @@ export default function PodcastsPage() {
className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 xl:grid-cols-6 gap-4"
data-tv-section={`genre-${genreId}`}
>
{genrePodcasts.map((podcast, index) => (
{genrePodcasts.map((podcast: { id: string; title: string; author: string; coverUrl?: string; episodeCount?: number }, index: number) => (
<PodcastCard
key={podcast.id}
podcast={podcast}
+2 -2
View File
@@ -386,8 +386,8 @@ export function MetadataEditor({
<div className="mt-2">
<Image
src={
formData.heroUrl ||
formData.coverUrl
(formData.heroUrl ||
formData.coverUrl)!
}
alt="Preview"
width={128}
@@ -122,10 +122,10 @@ export function AlbumHero({
id={album.id}
currentData={{
title: displayData.title,
year: displayData.year,
year: displayData.year ?? undefined,
genres: displayData.genres,
mbid: album.mbid,
coverUrl: displayData.coverUrl,
coverUrl: displayData.coverUrl ?? undefined,
// Pass originals for reset comparison
_originalTitle: album.title,
_originalYear: album.year,
@@ -8,7 +8,7 @@ import type { ColorPalette } from "@/hooks/useImageColor";
interface ArtistActionBarProps {
artist: Artist;
albums: Album[];
source: ArtistSource;
source: ArtistSource | null;
colors: ColorPalette | null;
onPlayAll: () => void;
onShuffle: () => void;
@@ -12,7 +12,7 @@ const MetadataEditor = lazy(() => import("@/components/MetadataEditor").then(mod
interface ArtistHeroProps {
artist: Artist;
source: ArtistSource;
source: ArtistSource | null;
albums: Album[];
heroImage: string | null;
backgroundImage?: string | null;
@@ -118,10 +118,10 @@ export function ArtistHero({
id={artist.id}
currentData={{
name: displayData.name,
bio: displayData.summary,
bio: displayData.summary ?? undefined,
genres: displayData.genres,
mbid: artist.mbid,
heroUrl: displayData.heroUrl,
heroUrl: displayData.heroUrl ?? undefined,
// Pass originals for reset comparison
_originalName: artist.name,
_originalBio:
@@ -11,7 +11,7 @@ import { SectionHeader } from "@/features/home/components/SectionHeader";
interface AvailableAlbumsProps {
albums: Album[];
artistName: string;
source: ArtistSource;
source: ArtistSource | null;
colors: ColorPalette | null;
onDownloadAlbum: (album: Album, e: React.MouseEvent) => void;
isPendingDownload: (mbid: string) => boolean;
@@ -27,7 +27,7 @@ function LazyAlbumCard({
index,
}: {
album: Album;
source: ArtistSource;
source: ArtistSource | null;
colors: ColorPalette | null;
onDownloadAlbum: (album: Album, e: React.MouseEvent) => void;
isPendingDownload: (mbid: string) => boolean;
@@ -57,7 +57,7 @@ export function useArtistActions() {
}));
// Sort tracks within album by track number
formattedTracks.sort((a, b) => a.trackNumber - b.trackNumber);
formattedTracks.sort((a: FormattedTrack, b: FormattedTrack) => a.trackNumber - b.trackNumber);
allTracks.push(...formattedTracks);
});
@@ -75,7 +75,7 @@ export function UnavailableAlbums({
key={album.id}
className={cn(
"flex items-center gap-4 px-4 py-3 hover:bg-[#1a1a1a] transition-colors group",
album.attemptNumber > 0 &&
(album.attemptNumber ?? 0) > 0 &&
"pl-12 bg-[#1a1a1a]/30"
)}
>
@@ -41,7 +41,7 @@ export function useDiscoverData() {
]);
setPlaylist(playlistData);
setConfig(configData);
setConfig(configData as DiscoverConfig | null);
} catch (error) {
console.error('Failed to load discover data:', error);
}
@@ -18,7 +18,7 @@ export function LibraryAlbumsGrid({ albums }: LibraryAlbumsGridProps) {
subtitle={album.artist?.name || ""}
imageUrl={
album.coverUrl || album.albumId
? api.getCoverArtUrl(album.coverUrl || album.albumId, 200)
? api.getCoverArtUrl((album.coverUrl || album.albumId)!, 200)
: null
}
fallbackIcon={Disc3}
@@ -38,7 +38,7 @@ export function LibraryTracksList({ tracks }: LibraryTracksListProps) {
album: {
id: t.album.id,
title: t.album.title,
coverArt: t.album.coverUrl,
coverArt: t.album.coverUrl ?? undefined,
},
}));
@@ -64,7 +64,7 @@ export function UnifiedSongsList({
album: {
id: t.album.id,
title: t.album.title,
coverArt: t.album.coverUrl,
coverArt: t.album.coverUrl ?? undefined,
},
}));
@@ -1165,7 +1165,7 @@ export function CacheSection({ settings, onUpdate }: CacheSectionProps) {
? "Cleaning..."
: "Cleanup Stale Jobs"}
</button>
{(enrichmentProgress?.audioAnalysis?.failed > 0 || enrichmentProgress?.audioAnalysis?.permanentlyFailed > 0) && (
{((enrichmentProgress?.audioAnalysis?.failed ?? 0) > 0 || (enrichmentProgress?.audioAnalysis?.permanentlyFailed ?? 0) > 0) && (
<button
onClick={handleRetryFailedAnalysis}
disabled={retryingFailed || isEnrichmentActive}
@@ -1173,7 +1173,7 @@ export function CacheSection({ settings, onUpdate }: CacheSectionProps) {
>
{retryingFailed
? "Retrying..."
: `Retry Failed Analysis (${(enrichmentProgress.audioAnalysis.failed || 0) + (enrichmentProgress.audioAnalysis.permanentlyFailed || 0)})`}
: `Retry Failed Analysis (${(enrichmentProgress?.audioAnalysis?.failed || 0) + (enrichmentProgress?.audioAnalysis?.permanentlyFailed || 0)})`}
</button>
)}
{retryResult && (
+4 -3
View File
@@ -621,10 +621,11 @@ export function usePodcastQuery(id: string | undefined) {
return await api.getPodcast(id);
} catch (error) {
// If podcast not found (404), return null to allow preview mode
const err = error as { status?: number; message?: string };
if (
error?.status === 404 ||
error?.message?.includes("not found") ||
error?.message?.includes("not subscribed")
err.status === 404 ||
err.message?.includes("not found") ||
err.message?.includes("not subscribed")
) {
return null;
}
+1 -1
View File
@@ -10,7 +10,7 @@ interface UseTVNavigationOptions {
}
interface UseTVNavigationResult {
containerRef: React.RefObject<HTMLElement>;
containerRef: React.RefObject<HTMLElement | null>;
focusedSectionIndex: number;
focusedCardIndex: number;
isContentFocused: boolean;
+1 -1
View File
@@ -4,7 +4,7 @@
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"strict": false,
"strict": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",