diff --git a/spotizerr-ui/dev-dist/sw.js b/spotizerr-ui/dev-dist/sw.js index 9b45108..4ffbcd7 100644 --- a/spotizerr-ui/dev-dist/sw.js +++ b/spotizerr-ui/dev-dist/sw.js @@ -82,7 +82,7 @@ define(['./workbox-f70c5944'], (function (workbox) { 'use strict'; "revision": "3ca0b8505b4bec776b69afdba2768812" }, { "url": "index.html", - "revision": "0.v6eu1k3j5b" + "revision": "0.im8ejsgjrtc" }], {}); workbox.cleanupOutdatedCaches(); workbox.registerRoute(new workbox.NavigationRoute(workbox.createHandlerBoundToURL("index.html"), { diff --git a/spotizerr-ui/public/dark.svg b/spotizerr-ui/public/dark.svg new file mode 100644 index 0000000..c41fc06 --- /dev/null +++ b/spotizerr-ui/public/dark.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/spotizerr-ui/public/light.svg b/spotizerr-ui/public/light.svg new file mode 100644 index 0000000..cf901e9 --- /dev/null +++ b/spotizerr-ui/public/light.svg @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/spotizerr-ui/public/system.svg b/spotizerr-ui/public/system.svg new file mode 100644 index 0000000..6c09538 --- /dev/null +++ b/spotizerr-ui/public/system.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/spotizerr-ui/src/components/SearchResultCard.tsx b/spotizerr-ui/src/components/SearchResultCard.tsx index 2e49350..a471228 100644 --- a/spotizerr-ui/src/components/SearchResultCard.tsx +++ b/spotizerr-ui/src/components/SearchResultCard.tsx @@ -26,11 +26,13 @@ export const SearchResultCard = ({ id, name, subtitle, imageUrl, type, onDownloa return (
- {name} + + {name} + {onDownload && (
@@ -133,7 +133,7 @@ export function GeneralTab({ config, isLoading: isConfigLoading }: GeneralTabPro {globalSettings?.explicitFilter ? "Enabled" : "Disabled"} - ENV + ENV

diff --git a/spotizerr-ui/src/index.css b/spotizerr-ui/src/index.css index 5b4fd95..7551a94 100644 --- a/spotizerr-ui/src/index.css +++ b/spotizerr-ui/src/index.css @@ -1,32 +1,39 @@ @import "tailwindcss"; -@theme { - /* Background Colors - Light theme: pure white to warm grays */ - --color-surface: #ffffff; - --color-surface-secondary: #fafbfc; - --color-surface-muted: #f4f6f8; - --color-surface-accent: #e8ecf0; - --color-surface-overlay: rgba(255, 255, 255, 0.96); - - /* Dark mode backgrounds - Rich blacks with subtle warm undertones */ - --color-surface-dark: #0a0a0a; - --color-surface-secondary-dark: #141414; - --color-surface-muted-dark: #1f1f1f; - --color-surface-accent-dark: #2d2d2d; - --color-surface-overlay-dark: rgba(10, 10, 10, 0.96); +/* Override dark mode to use class-based instead of prefers-color-scheme */ +@custom-variant dark (&:where(.dark, .dark *)); - /* Text Colors - Light theme with enhanced contrast */ +@theme { + /* Background Colors with automatic dark mode variants and gradients */ + --color-surface: #ffffff; + --color-surface-dark: #0a0a0a; + --color-surface-secondary: #fafbfc; + --color-surface-secondary-dark: #141418; + --color-surface-muted: #f4f6f8; + --color-surface-muted-dark: #1f1f23; + --color-surface-accent: #e8ecf0; + --color-surface-accent-dark: #2d2d33; + --color-surface-overlay: rgba(255, 255, 255, 0.85); + --color-surface-overlay-dark: rgba(10, 10, 12, 0.85); + + /* Gradient backgrounds for depth */ + --gradient-surface: linear-gradient(135deg, #ffffff 0%, #fafbfc 100%); + --gradient-surface-dark: linear-gradient(135deg, #0a0a0a 0%, #141418 100%); + --gradient-muted: linear-gradient(135deg, #f4f6f8 0%, #e8ecf0 100%); + --gradient-muted-dark: linear-gradient(135deg, #1f1f23 0%, #2d2d33 100%); + --gradient-accent: linear-gradient(135deg, #e8ecf0 0%, #d6dce4 100%); + --gradient-accent-dark: linear-gradient(135deg, #2d2d33 0%, #3a3a42 100%); + + /* Text Colors with automatic dark mode variants and subtle tints */ --color-content-primary: #0d1117; - --color-content-secondary: #57606a; - --color-content-muted: #768390; - --color-content-accent: #8c959f; - --color-content-inverse: #ffffff; - - /* Dark mode text - Crisp whites and refined grays */ --color-content-primary-dark: #f0f6fc; - --color-content-secondary-dark: #c9d1d9; - --color-content-muted-dark: #8b949e; - --color-content-accent-dark: #6e7681; + --color-content-secondary: #4a5568; + --color-content-secondary-dark: #a0aec0; + --color-content-muted: #718096; + --color-content-muted-dark: #9ca3af; + --color-content-accent: #6b7280; + --color-content-accent-dark: #6b7280; + --color-content-inverse: #ffffff; --color-content-inverse-dark: #0d1117; /* Interactive Colors - Enhanced Spotify green with sophistication */ @@ -73,21 +80,27 @@ --color-processing-muted: #f0ebff; --color-processing-text: #5d3e7a; - /* Border Colors - Subtle and refined */ - --color-border: #e1e5e9; - --color-border-muted: #f4f6f8; - --color-border-accent: #d0d7de; + /* Border Colors with automatic dark mode variants and gradients */ + --color-border: #e2e8f0; + --color-border-dark: #374151; + --color-border-muted: #f1f5f9; + --color-border-muted-dark: #1f2937; + --color-border-accent: #cbd5e1; + --color-border-accent-dark: #4b5563; --color-border-focus: #1ed760; - --color-border-dark: #30363d; - --color-border-muted-dark: #21262d; - --color-border-accent-dark: #373e47; - /* Input Colors */ - --color-input-background: #f4f6f8; - --color-input-border: #e1e5e9; + /* Gradient borders for enhanced depth */ + --gradient-border: linear-gradient(135deg, #e2e8f0 0%, #cbd5e1 100%); + --gradient-border-dark: linear-gradient(135deg, #374151 0%, #4b5563 100%); + + /* Input Colors with automatic dark mode variants and gradients */ + --color-input-background: #f8fafc; + --color-input-background-dark: #1f2937; + --color-input-border: #e2e8f0; + --color-input-border-dark: #374151; --color-input-focus: #1ed760; - --color-input-background-dark: #21262d; - --color-input-border-dark: #30363d; + --gradient-input: linear-gradient(135deg, #f8fafc 0%, #f1f5f9 100%); + --gradient-input-dark: linear-gradient(135deg, #1f2937 0%, #111827 100%); /* Button Colors */ --color-button-primary: #1ed760; @@ -103,8 +116,8 @@ --color-button-success-hover: #1e7b34; --color-button-success-text: #ffffff; - --color-icon-button-hover: #e8ecf0; - --color-icon-button-hover-dark: #30363d; + --color-icon-button-hover: #f1f5f9; + --color-icon-button-hover-dark: #374151; /* Icon Colors */ --color-icon-primary: #000000; @@ -161,182 +174,64 @@ } @layer base { + /* PWA Safe Area Support */ + :root { + --safe-area-inset-top: env(safe-area-inset-top, 0px); + --safe-area-inset-right: env(safe-area-inset-right, 0px); + --safe-area-inset-bottom: env(safe-area-inset-bottom, 0px); + --safe-area-inset-left: env(safe-area-inset-left, 0px); + } + + /* PWA specific body styling */ + body { + background: var(--color-surface); + overscroll-behavior: none; + } + .dark body { + background: var(--color-surface-dark); + } + + /* PWA viewport fixes */ + html { + height: 100vh; + height: 100dvh; /* Dynamic viewport height for mobile */ + } + + body { + min-height: 100vh; + min-height: 100dvh; + margin: 0; + padding: 0; + } + a { @apply no-underline hover:underline cursor-pointer; } - /* SVG Icon Utility Classes - Using filters for img elements */ - .icon-primary { - fill: var(--color-icon-primary); - color: var(--color-icon-primary); - filter: brightness(0); - } - .dark .icon-primary { - fill: var(--color-icon-primary-dark); - color: var(--color-icon-primary-dark); - filter: brightness(0) invert(100%); - } - .icon-primary:hover { - fill: var(--color-icon-primary-hover); - color: var(--color-icon-primary-hover); - filter: brightness(0); - } - .dark .icon-primary:hover { - fill: var(--color-icon-primary-hover-dark); - color: var(--color-icon-primary-hover-dark); - filter: brightness(0) invert(100%); - } - - .icon-secondary { - fill: var(--color-icon-secondary); - color: var(--color-icon-secondary); - filter: brightness(0); - } - .dark .icon-secondary { - fill: var(--color-icon-secondary-dark); - color: var(--color-icon-secondary-dark); - filter: brightness(0) invert(100%); - } - .icon-secondary:hover { - fill: var(--color-icon-secondary-hover); - color: var(--color-icon-secondary-hover); - filter: brightness(0); - } - .dark .icon-secondary:hover { - fill: var(--color-icon-secondary-hover-dark); - color: var(--color-icon-secondary-hover-dark); - filter: brightness(0) invert(100%); - } - - .icon-muted { - fill: var(--color-icon-muted); - color: var(--color-icon-muted); - filter: brightness(0); - } - .dark .icon-muted { - fill: var(--color-icon-muted-dark); - color: var(--color-icon-muted-dark); - filter: brightness(0) invert(100%); - } - .icon-muted:hover { - fill: var(--color-icon-muted-hover); - color: var(--color-icon-muted-hover); - filter: brightness(0); - } - .dark .icon-muted:hover { - fill: var(--color-icon-muted-hover-dark); - color: var(--color-icon-muted-hover-dark); - filter: brightness(0) invert(100%); - } - - .icon-accent { - fill: var(--color-icon-accent); - color: var(--color-icon-accent); - filter: brightness(0); - } - .dark .icon-accent { - fill: var(--color-icon-accent-dark); - color: var(--color-icon-accent-dark); - filter: brightness(0) invert(100%); - } - .icon-accent:hover { - fill: var(--color-icon-accent-hover); - color: var(--color-icon-accent-hover); - filter: brightness(0); - } - .dark .icon-accent:hover { - fill: var(--color-icon-accent-hover-dark); - color: var(--color-icon-accent-hover-dark); - filter: brightness(0) invert(100%); - } - - .icon-success { - fill: var(--color-icon-success); - color: var(--color-icon-success); - filter: brightness(0); - } - .dark .icon-success { - fill: var(--color-icon-success-dark); - color: var(--color-icon-success-dark); - filter: brightness(0) invert(100%); - } - .icon-success:hover { - fill: var(--color-icon-success-hover); - color: var(--color-icon-success-hover); - filter: brightness(0); - } - .dark .icon-success:hover { - fill: var(--color-icon-success-hover-dark); - color: var(--color-icon-success-hover-dark); - filter: brightness(0) invert(100%); - } - - .icon-error { - fill: var(--color-icon-error); - color: var(--color-icon-error); - filter: brightness(0); - } - .dark .icon-error { - fill: var(--color-icon-error-dark); - color: var(--color-icon-error-dark); - filter: brightness(0) invert(100%); - } - .icon-error:hover { - fill: var(--color-icon-error-hover); - color: var(--color-icon-error-hover); - filter: brightness(0); - } - .dark .icon-error:hover { - fill: var(--color-icon-error-hover-dark); - color: var(--color-icon-error-hover-dark); - filter: brightness(0) invert(100%); - } - - .icon-warning { - fill: var(--color-icon-warning); - color: var(--color-icon-warning); - filter: brightness(0); - } - .dark .icon-warning { - fill: var(--color-icon-warning-dark); - color: var(--color-icon-warning-dark); - filter: brightness(0) invert(100%); - } - .icon-warning:hover { - fill: var(--color-icon-warning-hover); - color: var(--color-icon-warning-hover); - filter: brightness(0); - } - .dark .icon-warning:hover { - fill: var(--color-icon-warning-hover-dark); - color: var(--color-icon-warning-hover-dark); - filter: brightness(0) invert(100%); - } - - .icon-inverse { - fill: var(--color-icon-inverse); - color: var(--color-icon-inverse); - filter: brightness(0) invert(100%); - } - .dark .icon-inverse { - fill: var(--color-icon-inverse-dark); - color: var(--color-icon-inverse-dark); - filter: brightness(0); - } - - /* Logo Utility Classes - Automatic dark mode detection like Tailwind v4 */ + /* Logo Utility Classes - Using Tailwind utilities */ .logo { - filter: brightness(0); /* Black in light mode */ + @apply brightness-0 dark:invert; } - @media (prefers-color-scheme: dark) { - .logo { - filter: brightness(0) invert(1); /* White in dark mode */ - } + + + /* PWA Header Safe Area Classes */ + .pwa-header { + padding-top: var(--safe-area-inset-top); + padding-left: var(--safe-area-inset-left); + padding-right: var(--safe-area-inset-right); } - - /* Class-based override for manual toggles */ - .dark .logo { - filter: brightness(0) invert(1); /* White when dark class is present */ + + .pwa-footer { + padding-bottom: var(--safe-area-inset-bottom); + padding-left: var(--safe-area-inset-left); + padding-right: var(--safe-area-inset-right); } + + /* PWA Main Content Safe Area */ + .pwa-main { + padding-left: var(--safe-area-inset-left); + padding-right: var(--safe-area-inset-right); + } + } diff --git a/spotizerr-ui/src/main.tsx b/spotizerr-ui/src/main.tsx index 8f8ca28..ce956c7 100644 --- a/spotizerr-ui/src/main.tsx +++ b/spotizerr-ui/src/main.tsx @@ -5,28 +5,76 @@ import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import { router } from "./router"; import "./index.css"; -// Dark mode detection and setup -function setupDarkMode() { - // Check for saved theme preference or default to system preference - const savedTheme = localStorage.getItem('theme'); - const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches; +// Theme management functions +export function getTheme(): 'light' | 'dark' | 'system' { + return (localStorage.getItem('theme') as 'light' | 'dark' | 'system') || 'system'; +} + +export function setTheme(theme: 'light' | 'dark' | 'system') { + localStorage.setItem('theme', theme); + applyTheme(theme); +} + +export function toggleTheme() { + const currentTheme = getTheme(); + let nextTheme: 'light' | 'dark' | 'system'; - if (savedTheme === 'dark' || (!savedTheme && prefersDark)) { - document.documentElement.classList.add('dark'); - } else { - document.documentElement.classList.remove('dark'); + switch (currentTheme) { + case 'light': + nextTheme = 'dark'; + break; + case 'dark': + nextTheme = 'system'; + break; + default: + nextTheme = 'light'; + break; } - // Listen for system theme changes - window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', (e) => { - if (!localStorage.getItem('theme')) { + setTheme(nextTheme); + return nextTheme; +} + +function applyTheme(theme: 'light' | 'dark' | 'system') { + const root = document.documentElement; + + if (theme === 'system') { + // Use system preference + const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches; + if (prefersDark) { + root.classList.add('dark'); + } else { + root.classList.remove('dark'); + } + } else if (theme === 'dark') { + root.classList.add('dark'); + } else { + root.classList.remove('dark'); + } +} + +// Dark mode detection and setup +function setupDarkMode() { + // First, ensure we start with a clean slate + document.documentElement.classList.remove('dark'); + + const savedTheme = getTheme(); + applyTheme(savedTheme); + + // Listen for system theme changes (only when using system theme) + const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); + const handleSystemThemeChange = (e: MediaQueryListEvent) => { + // Only respond to system changes when we're in system mode + if (getTheme() === 'system') { if (e.matches) { document.documentElement.classList.add('dark'); } else { document.documentElement.classList.remove('dark'); } } - }); + }; + + mediaQuery.addEventListener('change', handleSystemThemeChange); } // Initialize dark mode diff --git a/spotizerr-ui/src/routes/album.tsx b/spotizerr-ui/src/routes/album.tsx index f62e20e..74a7825 100644 --- a/spotizerr-ui/src/routes/album.tsx +++ b/spotizerr-ui/src/routes/album.tsx @@ -70,45 +70,52 @@ export const Album = () => { const hasExplicitTrack = album.tracks.items.some((track) => track.explicit); return ( -

-
+
+ {/* Back Button */} +
-
- {album.name} -
-

{album.name}

-

- By{" "} - {album.artists.map((artist, index) => ( - - - {artist.name} - - {index < album.artists.length - 1 && ", "} - - ))} -

-

- {new Date(album.release_date).getFullYear()} • {album.total_tracks} songs -

-

{album.label}

+ + {/* Album Header - Mobile Optimized */} +
+
+ {album.name} +
+

{album.name}

+

+ By{" "} + {album.artists.map((artist, index) => ( + + + {artist.name} + + {index < album.artists.length - 1 && ", "} + + ))} +

+

+ {new Date(album.release_date).getFullYear()} • {album.total_tracks} songs +

+

{album.label}

+
-
+ + {/* Download Button - Full Width on Mobile */} +
-
-

Tracks

-
- {album.tracks.items.map((track, index) => { - if (isExplicitFilterEnabled && track.explicit) { + {/* Tracks Section */} +
+

Tracks

+
+
+ {album.tracks.items.map((track, index) => { + if (isExplicitFilterEnabled && track.explicit) { + return ( +
+
+ {index + 1} +

Explicit track filtered

+
+ --:-- +
+ ); + } return (
-
- {index + 1} -

Explicit track filtered

+
+ {index + 1} +
+

{track.name}

+

+ {track.artists.map((artist, index) => ( + + + {artist.name} + + {index < track.artists.length - 1 && ", "} + + ))} +

+
+
+
+ + {Math.floor(track.duration_ms / 60000)}: + {((track.duration_ms % 60000) / 1000).toFixed(0).padStart(2, "0")} + +
- --:--
); - } - return ( -
-
- {index + 1} -
-

{track.name}

-

- {track.artists.map((artist, index) => ( - - - {artist.name} - - {index < track.artists.length - 1 && ", "} - - ))} -

-
-
-
- - {Math.floor(track.duration_ms / 60000)}: - {((track.duration_ms % 60000) / 1000).toFixed(0).padStart(2, "0")} - - -
-
- ); - })} + })} +
diff --git a/spotizerr-ui/src/routes/artist.tsx b/spotizerr-ui/src/routes/artist.tsx index 4f479e6..e240c90 100644 --- a/spotizerr-ui/src/routes/artist.tsx +++ b/spotizerr-ui/src/routes/artist.tsx @@ -130,10 +130,10 @@ export const Artist = () => { return (
-
+
-
{renderTabContent()}
+
+ {renderTabContent()} +
); diff --git a/spotizerr-ui/src/routes/playlist.tsx b/spotizerr-ui/src/routes/playlist.tsx index 442e1fe..cf0c8eb 100644 --- a/spotizerr-ui/src/routes/playlist.tsx +++ b/spotizerr-ui/src/routes/playlist.tsx @@ -175,64 +175,66 @@ export const Playlist = () => { }); return ( -
-
+
+ {/* Back Button */} +
- {/* Playlist Header */} -
- {playlistMetadata.name} -
-

{playlistMetadata.name}

- {playlistMetadata.description && ( -

{playlistMetadata.description}

- )} -
-

- By {playlistMetadata.owner.display_name} • {playlistMetadata.followers.total.toLocaleString()} followers •{" "} - {totalTracks} songs + {/* Playlist Header - Mobile Optimized */} +

+
+ {playlistMetadata.name} +
+

{playlistMetadata.name}

+ {playlistMetadata.description && ( +

{playlistMetadata.description}

+ )} +

+ By {playlistMetadata.owner.display_name} • {playlistMetadata.followers.total.toLocaleString()} followers • {totalTracks} songs

-
- - -
+
+ + {/* Action Buttons - Full Width on Mobile */} +
+ +
{/* Tracks Section */} -
-
+
+

Tracks

{tracks.length > 0 && ( @@ -241,72 +243,80 @@ export const Playlist = () => { )}
-
- {filteredTracks.map(({ track }, index) => { - if (!track) return null; - return ( -
-
- {index + 1} - {track.album.name} -
- - {track.name} +
+
+ {filteredTracks.map(({ track }, index) => { + if (!track) return null; + return ( +
+
+ {index + 1} + + {track.album.name} -

- {track.artists.map((artist, index) => ( - - - {artist.name} - - {index < track.artists.length - 1 && ", "} - - ))} -

+
+ + {track.name} + +

+ {track.artists.map((artist, index) => ( + + + {artist.name} + + {index < track.artists.length - 1 && ", "} + + ))} +

+
+
+
+ + {Math.floor(track.duration_ms / 60000)}: + {((track.duration_ms % 60000) / 1000).toFixed(0).padStart(2, "0")} + +
-
- - {Math.floor(track.duration_ms / 60000)}: - {((track.duration_ms % 60000) / 1000).toFixed(0).padStart(2, "0")} - - -
+ ); + })} + + {/* Loading indicator */} + {loadingTracks && ( +
+
- ); - })} - - {/* Loading indicator */} - {loadingTracks && ( -
-
-
- )} - - {/* Intersection observer target */} - {hasMoreTracks && ( -
- )} - - {/* End of tracks indicator */} - {!hasMoreTracks && tracks.length > 0 && ( -
- All tracks loaded -
- )} + )} + + {/* Intersection observer target */} + {hasMoreTracks && ( +
+ )} + + {/* End of tracks indicator */} + {!hasMoreTracks && tracks.length > 0 && ( +
+ All tracks loaded +
+ )} +
diff --git a/spotizerr-ui/src/routes/root.tsx b/spotizerr-ui/src/routes/root.tsx index 219dbb6..6cda271 100644 --- a/spotizerr-ui/src/routes/root.tsx +++ b/spotizerr-ui/src/routes/root.tsx @@ -3,13 +3,84 @@ import { QueueProvider } from "@/contexts/QueueProvider"; import { SettingsProvider } from "@/contexts/SettingsProvider"; import { QueueContext } from "@/contexts/queue-context"; import { Queue } from "@/components/Queue"; -import { useContext } from "react"; +import { useContext, useState, useEffect } from "react"; +import { getTheme, toggleTheme } from "@/main"; + +function ThemeToggle() { + const [currentTheme, setCurrentTheme] = useState<'light' | 'dark' | 'system'>('system'); + + useEffect(() => { + // Set initial theme + setCurrentTheme(getTheme()); + + // Listen for theme changes (in case they happen elsewhere) + const handleStorageChange = () => { + setCurrentTheme(getTheme()); + }; + + // Listen for system theme changes that might affect our display + const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); + const handleSystemChange = () => { + // Force a re-render when system preference changes + // This ensures our toggle shows the correct state + setCurrentTheme(getTheme()); + }; + + window.addEventListener('storage', handleStorageChange); + mediaQuery.addEventListener('change', handleSystemChange); + + return () => { + window.removeEventListener('storage', handleStorageChange); + mediaQuery.removeEventListener('change', handleSystemChange); + }; + }, []); + + const handleToggle = () => { + const newTheme = toggleTheme(); + setCurrentTheme(newTheme); + }; + + const getThemeIcon = () => { + switch (currentTheme) { + case 'light': + return Light theme; + case 'dark': + return Dark theme; + default: + return System theme; + } + }; + + const getThemeLabel = () => { + switch (currentTheme) { + case 'light': + return 'Light'; + case 'dark': + return 'Dark'; + default: + return 'System'; + } + }; + + return ( + + ); +} function AppLayout() { const { toggleVisibility } = useContext(QueueContext) || {}; return ( -
+
{/* Desktop Header */}
@@ -17,6 +88,7 @@ function AppLayout() { Spotizerr
+ Watchlist @@ -33,40 +105,51 @@ function AppLayout() {
- {/* Mobile Header - Just logo/title */} -
-
- - Spotizerr - -
-
- - {/* Main content - flex-1 to push navigation to bottom on mobile */} -
+ {/* Desktop Main Content */} +
- {/* Mobile Bottom Navigation */} - + {/* Mobile Layout Container */} +
+ {/* Mobile Header - Fixed */} +
+
+ + Spotizerr + + +
+
+ + {/* Mobile Main Content - Scrollable container */} +
+
+ +
+
+ + {/* Mobile Bottom Navigation - Fixed */} + +
diff --git a/spotizerr-ui/src/routes/track.tsx b/spotizerr-ui/src/routes/track.tsx index 53ec13e..ad85aab 100644 --- a/spotizerr-ui/src/routes/track.tsx +++ b/spotizerr-ui/src/routes/track.tsx @@ -64,10 +64,10 @@ export const Track = () => { return (
-
+