From cdfd3263de062976ef5a2c8fe306950dd1f3f473 Mon Sep 17 00:00:00 2001
From: vicrodh <103399283+vicrodh@users.noreply.github.com>
Date: Fri, 9 Jan 2026 07:16:46 -0600
Subject: [PATCH] feat: Convert Figma React UI to Svelte 5
- Add lucide-svelte for icons
- Create app.css with theme CSS variables (Dark, Light, OLED, Warm)
- Convert all React components to Svelte 5 using runes ($state, $derived, $effect, $props)
- Implement core components: Sidebar, NowPlayingBar, NavigationItem, UserCard,
AlbumCard, HorizontalScrollRow, HeroSection, TrackRow, Toggle, Dropdown,
Tooltip, VolumeSlider
- Implement view components: HomeView, SettingsView, AlbumDetailView
- Implement overlay components: QueuePanel, FullScreenNowPlaying, FocusMode, Toast
- Create main +page.svelte with routing, state management and keyboard shortcuts
- All components use Svelte 5 snippets for component children patterns
---
package-lock.json | 10 +
package.json | 5 +-
src/app.css | 129 +++++
src/lib/components/AlbumCard.svelte | 132 +++++
src/lib/components/Dropdown.svelte | 116 ++++
src/lib/components/FocusMode.svelte | 346 ++++++++++++
.../components/FullScreenNowPlaying.svelte | 501 +++++++++++++++++
src/lib/components/HeroSection.svelte | 127 +++++
src/lib/components/HorizontalScrollRow.svelte | 103 ++++
src/lib/components/NavigationItem.svelte | 62 +++
src/lib/components/NowPlayingBar.svelte | 525 ++++++++++++++++++
src/lib/components/QueuePanel.svelte | 362 ++++++++++++
src/lib/components/Sidebar.svelte | 285 ++++++++++
src/lib/components/Toast.svelte | 101 ++++
src/lib/components/Toggle.svelte | 50 ++
src/lib/components/Tooltip.svelte | 55 ++
src/lib/components/TrackRow.svelte | 219 ++++++++
src/lib/components/UserCard.svelte | 117 ++++
src/lib/components/VolumeSlider.svelte | 112 ++++
.../components/views/AlbumDetailView.svelte | 312 +++++++++++
src/lib/components/views/HomeView.svelte | 118 ++++
src/lib/components/views/SettingsView.svelte | 351 ++++++++++++
src/routes/+page.svelte | 515 ++++++++++++-----
23 files changed, 4512 insertions(+), 141 deletions(-)
create mode 100644 src/app.css
create mode 100644 src/lib/components/AlbumCard.svelte
create mode 100644 src/lib/components/Dropdown.svelte
create mode 100644 src/lib/components/FocusMode.svelte
create mode 100644 src/lib/components/FullScreenNowPlaying.svelte
create mode 100644 src/lib/components/HeroSection.svelte
create mode 100644 src/lib/components/HorizontalScrollRow.svelte
create mode 100644 src/lib/components/NavigationItem.svelte
create mode 100644 src/lib/components/NowPlayingBar.svelte
create mode 100644 src/lib/components/QueuePanel.svelte
create mode 100644 src/lib/components/Sidebar.svelte
create mode 100644 src/lib/components/Toast.svelte
create mode 100644 src/lib/components/Toggle.svelte
create mode 100644 src/lib/components/Tooltip.svelte
create mode 100644 src/lib/components/TrackRow.svelte
create mode 100644 src/lib/components/UserCard.svelte
create mode 100644 src/lib/components/VolumeSlider.svelte
create mode 100644 src/lib/components/views/AlbumDetailView.svelte
create mode 100644 src/lib/components/views/HomeView.svelte
create mode 100644 src/lib/components/views/SettingsView.svelte
diff --git a/package-lock.json b/package-lock.json
index fa971cfaa..1621999e9 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -11,6 +11,7 @@
"dependencies": {
"@tauri-apps/api": "^2",
"@tauri-apps/plugin-opener": "^2",
+ "lucide-svelte": "^0.562.0",
"svelte-i18n": "^4.0.0"
},
"devDependencies": {
@@ -1629,6 +1630,15 @@
"es5-ext": "~0.10.2"
}
},
+ "node_modules/lucide-svelte": {
+ "version": "0.562.0",
+ "resolved": "https://registry.npmjs.org/lucide-svelte/-/lucide-svelte-0.562.0.tgz",
+ "integrity": "sha512-kSJDH/55lf0mun/o4nqWBXOcq0fWYzPeIjbTD97ywoeumAB9kWxtM06gC7oynqjtK3XhAljWSz5RafIzPEYIQA==",
+ "license": "ISC",
+ "peerDependencies": {
+ "svelte": "^3 || ^4 || ^5.0.0-next.42"
+ }
+ },
"node_modules/magic-string": {
"version": "0.30.21",
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz",
diff --git a/package.json b/package.json
index addc94f0d..9b17ddc7d 100644
--- a/package.json
+++ b/package.json
@@ -15,16 +15,17 @@
"dependencies": {
"@tauri-apps/api": "^2",
"@tauri-apps/plugin-opener": "^2",
+ "lucide-svelte": "^0.562.0",
"svelte-i18n": "^4.0.0"
},
"devDependencies": {
"@sveltejs/adapter-static": "^3.0.6",
"@sveltejs/kit": "^2.9.0",
"@sveltejs/vite-plugin-svelte": "^5.0.0",
+ "@tauri-apps/cli": "^2",
"svelte": "^5.0.0",
"svelte-check": "^4.0.0",
"typescript": "~5.6.2",
- "vite": "^6.0.3",
- "@tauri-apps/cli": "^2"
+ "vite": "^6.0.3"
}
}
diff --git a/src/app.css b/src/app.css
new file mode 100644
index 000000000..04d22d101
--- /dev/null
+++ b/src/app.css
@@ -0,0 +1,129 @@
+@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400&display=swap');
+
+:root {
+ /* QBZ-NIX Dark Theme Colors */
+ --bg-primary: #0f0f0f;
+ --bg-secondary: #1a1a1a;
+ --bg-tertiary: #2a2a2a;
+ --bg-hover: #1f1f1f;
+
+ --text-primary: #ffffff;
+ --text-secondary: #cccccc;
+ --text-muted: #888888;
+ --text-disabled: #555555;
+
+ --accent-primary: #4285F4;
+ --accent-hover: #5a9bf4;
+ --accent-active: #3275e4;
+
+ --border-subtle: #2a2a2a;
+ --border-strong: #3a3a3a;
+
+ /* Spacing System */
+ --spacing-micro: 4px;
+ --spacing-tight: 8px;
+ --spacing-default: 12px;
+ --spacing-component: 16px;
+ --spacing-section: 24px;
+ --spacing-large: 32px;
+ --spacing-page: 48px;
+
+ /* Typography */
+ --font-sans: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif;
+ --font-mono: 'JetBrains Mono', monospace;
+}
+
+/* Light Theme */
+[data-theme="light"] {
+ --bg-primary: #ffffff;
+ --bg-secondary: #f5f5f5;
+ --bg-tertiary: #e8e8e8;
+ --bg-hover: #ebebeb;
+
+ --text-primary: #1a1a1a;
+ --text-secondary: #444444;
+ --text-muted: #666666;
+ --text-disabled: #999999;
+
+ --border-subtle: #e0e0e0;
+ --border-strong: #cccccc;
+}
+
+/* OLED Theme */
+[data-theme="oled"] {
+ --bg-primary: #000000;
+ --bg-secondary: #0a0a0a;
+ --bg-tertiary: #1a1a1a;
+ --bg-hover: #111111;
+}
+
+/* Warm Theme */
+[data-theme="warm"] {
+ --bg-primary: #1a1512;
+ --bg-secondary: #231d18;
+ --bg-tertiary: #2d2520;
+ --bg-hover: #28211c;
+
+ --accent-primary: #e89b5a;
+ --accent-hover: #f0ab6e;
+ --accent-active: #d88b4a;
+}
+
+* {
+ margin: 0;
+ padding: 0;
+ box-sizing: border-box;
+}
+
+html {
+ font-size: 16px;
+}
+
+body {
+ font-family: var(--font-sans);
+ background-color: var(--bg-primary);
+ color: var(--text-primary);
+ -webkit-font-smoothing: antialiased;
+ -moz-osx-font-smoothing: grayscale;
+ overflow-x: hidden;
+}
+
+/* Scrollbar Styling */
+::-webkit-scrollbar {
+ width: 12px;
+ height: 12px;
+}
+
+::-webkit-scrollbar-track {
+ background: var(--bg-secondary);
+}
+
+::-webkit-scrollbar-thumb {
+ background: var(--bg-tertiary);
+ border-radius: 6px;
+}
+
+::-webkit-scrollbar-thumb:hover {
+ background: var(--border-strong);
+}
+
+/* Hide scrollbar for horizontal scroll containers */
+.hide-scrollbar {
+ -ms-overflow-style: none;
+ scrollbar-width: none;
+}
+
+.hide-scrollbar::-webkit-scrollbar {
+ display: none;
+}
+
+/* Utility classes */
+.truncate {
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+.tabular-nums {
+ font-variant-numeric: tabular-nums;
+}
diff --git a/src/lib/components/AlbumCard.svelte b/src/lib/components/AlbumCard.svelte
new file mode 100644
index 000000000..09066033b
--- /dev/null
+++ b/src/lib/components/AlbumCard.svelte
@@ -0,0 +1,132 @@
+
+
+
(isHovered = true)}
+ onmouseleave={() => (isHovered = false)}
+ onclick={onclick}
+ role="button"
+ tabindex="0"
+ onkeydown={(e) => e.key === 'Enter' && onclick?.()}
+>
+
+
+

+
+
+ {#if quality}
+
{quality}
+ {/if}
+
+
+ {#if isHovered}
+
+ {/if}
+
+
+
+
+
+
+
diff --git a/src/lib/components/Dropdown.svelte b/src/lib/components/Dropdown.svelte
new file mode 100644
index 000000000..63efcd34d
--- /dev/null
+++ b/src/lib/components/Dropdown.svelte
@@ -0,0 +1,116 @@
+
+
+
+
+
+ {#if isOpen}
+
+ {/if}
+
+
+
diff --git a/src/lib/components/FocusMode.svelte b/src/lib/components/FocusMode.svelte
new file mode 100644
index 000000000..2d547915d
--- /dev/null
+++ b/src/lib/components/FocusMode.svelte
@@ -0,0 +1,346 @@
+
+
+{#if isOpen}
+
+
+
+
+
+
+

+
+
+
+
+
+
+
+
+
+
+
+
+
+
{trackTitle}
+ {artist}
+
+
+
+
+
+
+ {formatTime(currentTime)}
+ {formatTime(duration)}
+
+
+
+{/if}
+
+
diff --git a/src/lib/components/FullScreenNowPlaying.svelte b/src/lib/components/FullScreenNowPlaying.svelte
new file mode 100644
index 000000000..18b63d9c8
--- /dev/null
+++ b/src/lib/components/FullScreenNowPlaying.svelte
@@ -0,0 +1,501 @@
+
+
+{#if isOpen}
+
+
+
+
+
+
+

+
+
+
+
+
{trackTitle}
+ {artist}
+ {album}
+
+
+
+
+
{quality}
+
+ {#each Array(5) as _, i}
+
+ {/each}
+
+
+
+
+
+
+
+ {formatTime(currentTime)}
+ {formatTime(duration)}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+{/if}
+
+
diff --git a/src/lib/components/HeroSection.svelte b/src/lib/components/HeroSection.svelte
new file mode 100644
index 000000000..b55c96655
--- /dev/null
+++ b/src/lib/components/HeroSection.svelte
@@ -0,0 +1,127 @@
+
+
+
+
+
+
+
+
+
{title}
+
{artist}{year ? ` • ${year}` : ''}
+
+
+
+
+
+
+
+
diff --git a/src/lib/components/HorizontalScrollRow.svelte b/src/lib/components/HorizontalScrollRow.svelte
new file mode 100644
index 000000000..8fc85e115
--- /dev/null
+++ b/src/lib/components/HorizontalScrollRow.svelte
@@ -0,0 +1,103 @@
+
+
+
+
+
diff --git a/src/lib/components/NavigationItem.svelte b/src/lib/components/NavigationItem.svelte
new file mode 100644
index 000000000..d6ee57b1c
--- /dev/null
+++ b/src/lib/components/NavigationItem.svelte
@@ -0,0 +1,62 @@
+
+
+
+
+
diff --git a/src/lib/components/NowPlayingBar.svelte b/src/lib/components/NowPlayingBar.svelte
new file mode 100644
index 000000000..9bd551665
--- /dev/null
+++ b/src/lib/components/NowPlayingBar.svelte
@@ -0,0 +1,525 @@
+
+
+
+
+
+
+
+
{trackTitle}
+
{artist}
+
+
{quality}
+
•
+
+ {#each Array(5) as _, i}
+
+ {/each}
+
+
+
+
+
+
+
+
+
+
+
+
{formatTime(currentTime)}
+
+
{formatTime(duration)}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
2:34
+
+
+
+
diff --git a/src/lib/components/QueuePanel.svelte b/src/lib/components/QueuePanel.svelte
new file mode 100644
index 000000000..27494e5c4
--- /dev/null
+++ b/src/lib/components/QueuePanel.svelte
@@ -0,0 +1,362 @@
+
+
+{#if isOpen}
+
+
+
+
+
+
+
+
+
+
+
+ {#if currentTrack}
+
+
+
+

+
+
{currentTrack.title}
+
{currentTrack.artist}
+
+
+
+ {/if}
+
+
+ {#if upcomingTracks.length > 0}
+
+
+
+ {#each upcomingTracks as track, index}
+
(hoveredTrack = track.id)}
+ onmouseleave={() => (hoveredTrack = null)}
+ onclick={() => onPlayTrack?.(track.id)}
+ role="button"
+ tabindex="0"
+ onkeydown={(e) => e.key === 'Enter' && onPlayTrack?.(track.id)}
+ >
+
+
+
+
+
+
+
+ {#if hoveredTrack === track.id}
+
+ {:else}
+
{index + 1}
+ {/if}
+
+
+
+
+
{track.title}
+
{track.artist}
+
+
+
+
{track.duration}
+
+ {/each}
+
+
+ {/if}
+
+
+ {#if upcomingTracks.length === 0 && !currentTrack}
+
+
🎵
+
Queue is empty
+
Search for music or browse your library to add tracks.
+
+ {/if}
+
+
+
+ {#if upcomingTracks.length > 0 || currentTrack}
+
+ {/if}
+
+{/if}
+
+
diff --git a/src/lib/components/Sidebar.svelte b/src/lib/components/Sidebar.svelte
new file mode 100644
index 000000000..f63accef3
--- /dev/null
+++ b/src/lib/components/Sidebar.svelte
@@ -0,0 +1,285 @@
+
+
+
+
+
diff --git a/src/lib/components/Toast.svelte b/src/lib/components/Toast.svelte
new file mode 100644
index 000000000..3e45743e7
--- /dev/null
+++ b/src/lib/components/Toast.svelte
@@ -0,0 +1,101 @@
+
+
+
+
+ {#if type === 'success'}
+
+ {:else if type === 'error'}
+
+ {:else}
+
+ {/if}
+
+
{message}
+
+
+
+
diff --git a/src/lib/components/Toggle.svelte b/src/lib/components/Toggle.svelte
new file mode 100644
index 000000000..1a75fcc5e
--- /dev/null
+++ b/src/lib/components/Toggle.svelte
@@ -0,0 +1,50 @@
+
+
+
+
+
diff --git a/src/lib/components/Tooltip.svelte b/src/lib/components/Tooltip.svelte
new file mode 100644
index 000000000..bbebd9ab9
--- /dev/null
+++ b/src/lib/components/Tooltip.svelte
@@ -0,0 +1,55 @@
+
+
+
+
+
diff --git a/src/lib/components/TrackRow.svelte b/src/lib/components/TrackRow.svelte
new file mode 100644
index 000000000..320082633
--- /dev/null
+++ b/src/lib/components/TrackRow.svelte
@@ -0,0 +1,219 @@
+
+
+ (isHovered = true)}
+ onmouseleave={() => (isHovered = false)}
+ onclick={onPlay}
+ role="button"
+ tabindex="0"
+ onkeydown={(e) => e.key === 'Enter' && onPlay?.()}
+>
+
+
+ {#if isHovered && !isPlaying}
+
+ {:else if isPlaying}
+
+ {:else}
+
{number}
+ {/if}
+
+
+
+
+
{title}
+ {#if artist}
+
{artist}
+ {/if}
+
+
+
+
{duration}
+
+
+ {#if quality}
+
{quality}
+ {/if}
+
+
+ {#if isHovered && !isPlaying}
+
+
+
+
+ {/if}
+
+
+
diff --git a/src/lib/components/UserCard.svelte b/src/lib/components/UserCard.svelte
new file mode 100644
index 000000000..5e10a4ee6
--- /dev/null
+++ b/src/lib/components/UserCard.svelte
@@ -0,0 +1,117 @@
+
+
+
+
+
+ {#if avatarUrl}
+

+ {:else}
+ {username.charAt(0).toUpperCase()}
+ {/if}
+
+
+
+
+
{username}
+
{subscription}
+
+
+
+
+
+
+
diff --git a/src/lib/components/VolumeSlider.svelte b/src/lib/components/VolumeSlider.svelte
new file mode 100644
index 000000000..fda0f8697
--- /dev/null
+++ b/src/lib/components/VolumeSlider.svelte
@@ -0,0 +1,112 @@
+
+
+
+
+ {#if showValue}
+
{value}s
+ {/if}
+
+
+
diff --git a/src/lib/components/views/AlbumDetailView.svelte b/src/lib/components/views/AlbumDetailView.svelte
new file mode 100644
index 000000000..571279ea6
--- /dev/null
+++ b/src/lib/components/views/AlbumDetailView.svelte
@@ -0,0 +1,312 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {#each album.tracks as track}
+ (currentTrack = track.number)}
+ />
+ {/each}
+
+
+
+
+
diff --git a/src/lib/components/views/HomeView.svelte b/src/lib/components/views/HomeView.svelte
new file mode 100644
index 000000000..73d3c87dc
--- /dev/null
+++ b/src/lib/components/views/HomeView.svelte
@@ -0,0 +1,118 @@
+
+
+
+
+
+
+
+ {#if recentAlbums.length > 0}
+
+ {#snippet children()}
+ {#each recentAlbums as album}
+ onAlbumClick?.(album.id ?? '')}
+ />
+ {/each}
+
+ {/snippet}
+
+ {/if}
+
+
+ {#if recommendedAlbums.length > 0}
+
+ {#snippet children()}
+ {#each recommendedAlbums as album}
+ onAlbumClick?.(album.id ?? '')}
+ />
+ {/each}
+
+ {/snippet}
+
+ {/if}
+
+
+ {#if newReleases.length > 0}
+
+ {#snippet children()}
+ {#each newReleases as album}
+ onAlbumClick?.(album.id ?? '')}
+ />
+ {/each}
+
+ {/snippet}
+
+ {/if}
+
+
+
diff --git a/src/lib/components/views/SettingsView.svelte b/src/lib/components/views/SettingsView.svelte
new file mode 100644
index 000000000..a9f1a561e
--- /dev/null
+++ b/src/lib/components/views/SettingsView.svelte
@@ -0,0 +1,351 @@
+
+
+
+
+
+
+
+
+ Account
+
+
U
+
+
Username
+
email@example.com
+
Studio Premier
+
+
+
+
+
+
+
+ Audio
+
+ Streaming Quality
+ (streamingQuality = v)}
+ />
+
+
+ Prefer highest available
+ (preferHighest = v)} />
+
+
+ Output Device
+ (outputDevice = v)}
+ />
+
+
+
+ Exclusive Mode
+
+
+
(exclusiveMode = v)} />
+
+
+
+ DAC Passthrough
+
+
+
(dacPassthrough = v)} />
+
+
+ Sample Rate
+ 192 kHz
+
+
+
+
+
+ Playback
+
+ Gapless Playback
+ (gaplessPlayback = v)} />
+
+
+
Crossfade
+
+ (crossfade = v)} max={12} showValue />
+
+
+
+ Normalize Volume
+ (normalizeVolume = v)} />
+
+
+
+
+
+ Appearance
+
+ Theme
+ (theme = v)}
+ />
+
+
+ Compact Mode
+ (compactMode = v)} />
+
+
+
+
+
+ Integrations
+
+ Last.fm
+
+
+ {#if lastfmConnected}
+
+ Scrobbling
+ (scrobbling = v)} />
+
+ {/if}
+
+
+
+
+ Storage
+
+ Cache Size
+ 2.4 GB
+
+
+ Clear Cache
+
+
+
+
+
+
diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte
index 2a0845b11..bfc8f13b9 100644
--- a/src/routes/+page.svelte
+++ b/src/routes/+page.svelte
@@ -1,156 +1,393 @@
-
- Welcome to Tauri + Svelte
+
+
+
navigateTo('settings')}
+ />
-
- Click on the Tauri, Vite, and SvelteKit logos to learn more.
+
+
+ {#if activeView === 'home'}
+
+ {:else if activeView === 'settings'}
+
+ {:else if activeView === 'album' && selectedAlbum}
+
+ {:else if activeView === 'search'}
+
+
Search
+
Search functionality coming soon...
+
+ {:else if activeView === 'library'}
+
+
Library
+
Your library will appear here...
+
+ {/if}
+
-
- {greetMsg}
-
+
+ (isQueueOpen = true)}
+ onOpenFullScreen={() => (isFullScreenOpen = true)}
+ />
+
+
+ (isQueueOpen = false)}
+ {currentTrack}
+ {upcomingTracks}
+ onClearQueue={() => showToast('Queue cleared', 'info')}
+ onSaveAsPlaylist={() => showToast('Saved as playlist', 'success')}
+ />
+
+
+ (isFullScreenOpen = false)}
+ artwork={currentTrack.artwork}
+ trackTitle={currentTrack.title}
+ artist={currentTrack.artist}
+ album="Midnight Dreams"
+ quality="24-Bit / 96 kHz"
+ qualityLevel={4}
+ {isPlaying}
+ onTogglePlay={togglePlay}
+ {currentTime}
+ {duration}
+ onSeek={handleSeek}
+ {volume}
+ onVolumeChange={handleVolumeChange}
+ {isShuffle}
+ onToggleShuffle={toggleShuffle}
+ {repeatMode}
+ onToggleRepeat={toggleRepeat}
+ {isFavorite}
+ onToggleFavorite={toggleFavorite}
+ onOpenQueue={() => {
+ isFullScreenOpen = false;
+ isQueueOpen = true;
+ }}
+ onOpenFocusMode={() => {
+ isFullScreenOpen = false;
+ isFocusModeOpen = true;
+ }}
+ />
+
+
+ (isFocusModeOpen = false)}
+ artwork={currentTrack.artwork}
+ trackTitle={currentTrack.title}
+ artist={currentTrack.artist}
+ {isPlaying}
+ onTogglePlay={togglePlay}
+ {currentTime}
+ {duration}
+ onSeek={handleSeek}
+ />
+
+
+ {#if toast}
+
+ {/if}
+