Selaa lähdekoodia

Improve mobile responsiveness and unify page styling

- Match navbar background with header (bg-card)
- Reduce page title font size and add left padding
- Make filter bar compact on mobile with icon-only buttons
- Standardize page spacing across all pages (py-3 sm:py-6)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
tuanchris 2 viikkoa sitten
vanhempi
sitoutus
311cdc57f7

+ 19 - 7
frontend/src/components/layout/Layout.tsx

@@ -9,6 +9,7 @@ import { cacheAllPreviews } from '@/lib/previewCache'
 import { TableSelector } from '@/components/TableSelector'
 import { useTable } from '@/contexts/TableContext'
 import { apiClient } from '@/lib/apiClient'
+import ShinyText from '@/components/ShinyText'
 
 const navItems = [
   { path: '/', label: 'Browse', icon: 'grid_view', title: 'Browse Patterns' },
@@ -1138,16 +1139,26 @@ export function Layout() {
         </div>
       )}
 
-      {/* Header */}
-      <header className="fixed top-0 left-0 right-0 z-40 border-b border-border bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60">
-        <div className="flex h-14 items-center justify-between px-4">
+      {/* Header - Floating Pill */}
+      <header className="fixed top-0 left-0 right-0 z-40">
+        {/* Blurry backdrop behind header */}
+        <div className="absolute inset-0 h-20 bg-background/80 backdrop-blur-md supports-[backdrop-filter]:bg-background/50" />
+        <div className="relative w-full max-w-5xl mx-auto px-3 sm:px-4 pt-3 pointer-events-none">
+          <div className="flex h-12 items-center justify-between px-4 rounded-full bg-card shadow-lg border border-border pointer-events-auto">
           <Link to="/" className="flex items-center gap-2">
             <img
               src={customLogo ? apiClient.getAssetUrl(`/static/custom/${customLogo}`) : apiClient.getAssetUrl('/static/android-chrome-192x192.png')}
               alt={displayName}
               className="w-8 h-8 rounded-full object-cover"
             />
-            <span className="font-semibold text-lg">{displayName}</span>
+            <ShinyText
+              text={displayName}
+              className="font-semibold text-lg"
+              speed={4}
+              color={isDark ? '#a8a8a8' : '#555555'}
+              shineColor={isDark ? '#ffffff' : '#999999'}
+              spread={75}
+            />
             <span
               className={`w-2 h-2 rounded-full ${
                 !isBackendConnected
@@ -1167,7 +1178,7 @@ export function Layout() {
           </Link>
 
           {/* Desktop actions */}
-          <div className="hidden md:flex items-center gap-1">
+          <div className="hidden md:flex items-center gap-1 ml-6">
             {/* Now Playing button */}
             <Button
               variant="ghost"
@@ -1227,7 +1238,7 @@ export function Layout() {
           </div>
 
           {/* Mobile actions */}
-          <div className="flex md:hidden items-center gap-1">
+          <div className="flex md:hidden items-center gap-1 ml-4">
             {/* Now Playing button */}
             <Button
               variant="ghost"
@@ -1303,6 +1314,7 @@ export function Layout() {
                 </div>
               </PopoverContent>
             </Popover>
+            </div>
           </div>
         </div>
       </header>
@@ -1436,7 +1448,7 @@ export function Layout() {
       </div>
 
       {/* Bottom Navigation */}
-      <nav className="fixed bottom-0 left-0 right-0 z-40 border-t border-border bg-background pb-safe">
+      <nav className="fixed bottom-0 left-0 right-0 z-40 border-t border-border bg-card pb-safe">
         <div className="max-w-5xl mx-auto grid grid-cols-5 h-16">
           {navItems.map((item) => {
             const isActive = location.pathname === item.path

+ 52 - 56
frontend/src/pages/BrowsePage.tsx

@@ -12,7 +12,6 @@ import { useOnBackendConnected } from '@/hooks/useBackendConnection'
 import { Button } from '@/components/ui/button'
 import { Input } from '@/components/ui/input'
 import { Label } from '@/components/ui/label'
-import { Separator } from '@/components/ui/separator'
 import { Slider } from '@/components/ui/slider'
 import {
   Select,
@@ -813,76 +812,74 @@ export function BrowsePage() {
         className="hidden"
       />
 
-      {/* Page Header - Compact on mobile */}
-      <div className="flex items-center justify-between gap-2 sm:gap-4">
-        <div className="space-y-0 sm:space-y-1 min-w-0">
-          <h1 className="text-xl sm:text-3xl font-bold tracking-tight">Browse Patterns</h1>
-          <p className="text-xs sm:text-base text-muted-foreground truncate">
+      {/* Page Header */}
+      <div className="flex items-start justify-between gap-4 pl-1">
+        <div className="space-y-0.5">
+          <h1 className="text-xl font-semibold tracking-tight">Browse Patterns</h1>
+          <p className="text-xs text-muted-foreground">
             {patterns.length} patterns available
           </p>
         </div>
         <Button
-          variant="secondary"
-          size="sm"
+          variant="ghost"
           onClick={() => fileInputRef.current?.click()}
           disabled={isUploading}
-          className="gap-1.5 sm:gap-2 shrink-0 h-8 sm:h-9"
+          className="gap-2 shrink-0 h-11 rounded-full px-4 bg-card border border-border shadow-sm hover:bg-accent"
         >
           {isUploading ? (
-            <span className="material-icons-outlined animate-spin text-base sm:text-lg">sync</span>
+            <span className="material-icons-outlined animate-spin text-lg">sync</span>
           ) : (
-            <span className="material-icons-outlined text-base sm:text-lg">add</span>
+            <span className="material-icons-outlined text-lg">add</span>
           )}
           <span className="hidden sm:inline">Add Pattern</span>
         </Button>
       </div>
 
-      <Separator className="my-0" />
-
-      {/* Sticky Filters - Compact on mobile */}
-      <div className="sticky top-14 z-30 py-2 sm:py-4 -mx-4 px-4 bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60">
-        {/* Mobile: Single row with all controls */}
-        <div className="flex items-center gap-1.5 sm:gap-3">
-          {/* Search */}
+      {/* Filter Bar */}
+      <div className="sticky top-[4.5rem] z-30 py-3 -mx-3 sm:-mx-4 px-3 sm:px-4 bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60">
+        <div className="flex items-center gap-2 sm:gap-3">
+          {/* Search - Pill shaped, white background */}
           <div className="relative flex-1 min-w-0">
-            <span className="material-icons-outlined absolute left-2.5 top-1/2 -translate-y-1/2 text-muted-foreground text-lg">
+            <span className="material-icons-outlined absolute left-3 sm:left-4 top-1/2 -translate-y-1/2 text-muted-foreground text-lg sm:text-xl">
               search
             </span>
             <Input
               value={searchQuery}
               onChange={(e) => setSearchQuery(e.target.value)}
               placeholder="Search..."
-              className="pl-8 pr-8 h-9 text-sm"
+              className="pl-9 sm:pl-11 pr-10 h-9 sm:h-11 rounded-full bg-card border-border shadow-sm text-xs sm:text-sm focus:ring-2 focus:ring-ring"
             />
             {searchQuery && (
               <Button
                 variant="ghost"
-                size="icon-sm"
+                size="icon"
                 onClick={() => setSearchQuery('')}
-                className="absolute right-1 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground h-6 w-6"
+                className="absolute right-2 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground h-7 w-7 rounded-full"
               >
                 <span className="material-icons-outlined text-lg">close</span>
               </Button>
             )}
           </div>
 
-          {/* Category */}
+          {/* Category - Icon on mobile, text on desktop */}
           <Select value={selectedCategory} onValueChange={setSelectedCategory}>
-            <SelectTrigger className="w-[4.5rem] sm:w-36 h-9 text-sm shrink-0">
+            <SelectTrigger className="h-9 w-9 sm:h-11 sm:w-auto rounded-full bg-card border-border shadow-sm text-xs sm:text-sm shrink-0 [&>svg]:hidden sm:[&>svg]:block px-0 sm:px-3 justify-center sm:justify-between [&>span:last-of-type]:hidden sm:[&>span:last-of-type]:inline">
+              <span className="material-icons-outlined text-lg shrink-0 sm:hidden">folder</span>
               <SelectValue placeholder="All" />
             </SelectTrigger>
             <SelectContent>
               {categories.map((cat) => (
                 <SelectItem key={cat} value={cat}>
-                  {cat === 'all' ? 'All' : cat === 'root' ? 'Default Patterns' : cat}
+                  {cat === 'all' ? 'All' : cat === 'root' ? 'Default' : cat}
                 </SelectItem>
               ))}
             </SelectContent>
           </Select>
 
-          {/* Sort */}
+          {/* Sort - Icon on mobile, text on desktop */}
           <Select value={sortBy} onValueChange={(v) => setSortBy(v as SortOption)}>
-            <SelectTrigger className="w-[4.5rem] sm:w-32 h-9 text-sm shrink-0">
+            <SelectTrigger className="h-9 w-9 sm:h-11 sm:w-auto rounded-full bg-card border-border shadow-sm text-xs sm:text-sm shrink-0 [&>svg]:hidden sm:[&>svg]:block px-0 sm:px-3 justify-center sm:justify-between [&>span:last-of-type]:hidden sm:[&>span:last-of-type]:inline">
+              <span className="material-icons-outlined text-lg shrink-0 sm:hidden">sort</span>
               <SelectValue placeholder="Sort" />
             </SelectTrigger>
             <SelectContent>
@@ -892,37 +889,36 @@ export function BrowsePage() {
             </SelectContent>
           </Select>
 
-          {/* Sort direction */}
+          {/* Sort direction - Pill shaped, white background */}
           <Button
-            variant="secondary"
+            variant="outline"
             size="icon"
             onClick={() => setSortAsc(!sortAsc)}
-            className="shrink-0 h-9 w-9"
+            className="shrink-0 h-9 w-9 sm:h-11 sm:w-11 rounded-full bg-card shadow-sm"
             title={sortAsc ? 'Ascending' : 'Descending'}
           >
-            <span className="material-icons-outlined text-lg">
+            <span className="material-icons-outlined text-lg sm:text-xl">
               {sortAsc ? 'arrow_upward' : 'arrow_downward'}
             </span>
           </Button>
 
-          {/* Cache button - icon only on mobile */}
+          {/* Cache button - Pill shaped, white background */}
           {!allCached && (
             <Button
-              variant="secondary"
-              size="icon"
+              variant="outline"
               onClick={handleCacheAllPreviews}
-              className="shrink-0 h-9 w-9 sm:w-auto sm:px-3 sm:gap-2"
+              className="shrink-0 h-11 rounded-full lg:rounded-full bg-card shadow-sm px-3 sm:px-4 gap-2"
               title="Cache All Previews"
             >
               {isCaching ? (
                 <>
                   <span className="material-icons-outlined animate-spin text-lg">sync</span>
-                  <span className="hidden sm:inline">{cacheProgress}%</span>
+                  <span className="hidden sm:inline text-sm">{cacheProgress}%</span>
                 </>
               ) : (
                 <>
                   <span className="material-icons-outlined text-lg">cached</span>
-                  <span className="hidden sm:inline">Cache All</span>
+                  <span className="hidden sm:inline text-sm">Cache</span>
                 </>
               )}
             </Button>
@@ -1292,12 +1288,12 @@ function PatternCard({ pattern, isSelected, isFavorite, onToggleFavorite, onClic
     <button
       ref={cardRef}
       onClick={onClick}
-      className={`group flex flex-col items-center gap-2 p-2 rounded-lg transition-all duration-200 ease-out hover:-translate-y-1 hover:scale-[1.02] hover:shadow-lg hover:bg-accent/30 active:scale-95 focus:outline-none focus:ring-2 focus:ring-primary focus:ring-offset-2 ${
-        isSelected ? 'ring-2 ring-primary ring-offset-2 ring-offset-background bg-accent/20' : ''
+      className={`group flex flex-col items-center gap-2 p-2.5 rounded-xl bg-card border border-border shadow-sm transition-all duration-200 ease-out hover:-translate-y-1 hover:shadow-lg active:scale-95 focus:outline-none focus:ring-2 focus:ring-primary focus:ring-offset-2 ${
+        isSelected ? 'ring-2 ring-primary ring-offset-2 ring-offset-background' : ''
       }`}
     >
       <div className="relative w-full aspect-square">
-        <div className="w-full h-full rounded-full overflow-hidden border bg-muted">
+        <div className="w-full h-full rounded-full overflow-hidden border border-border bg-muted">
           {previewUrl && !imageError ? (
             <>
               {!imageLoaded && (
@@ -1326,28 +1322,28 @@ function PatternCard({ pattern, isSelected, isFavorite, onToggleFavorite, onClic
             </div>
           )}
         </div>
-        {/* Favorite heart button */}
-        <div
-          className={`absolute -top-1 -right-1 w-6 h-6 rounded-full flex items-center justify-center shadow-sm z-10 transition-opacity duration-200 cursor-pointer bg-white/90 dark:bg-gray-800/90 ${
-            isFavorite ? 'opacity-100' : 'opacity-0 group-hover:opacity-100'
+      </div>
+
+      {/* Name and favorite row */}
+      <div className="flex items-center justify-between w-full gap-1 px-0.5">
+        <span className="text-xs font-bold text-foreground truncate" title={pattern.name}>
+          {pattern.name}
+        </span>
+        <button
+          className={`shrink-0 transition-colors ${
+            isFavorite ? 'text-red-500 hover:text-red-600' : 'text-muted-foreground hover:text-red-500'
           }`}
-          onClick={(e) => onToggleFavorite(pattern.path, e)}
+          onClick={(e) => {
+            e.stopPropagation()
+            onToggleFavorite(pattern.path, e)
+          }}
           title={isFavorite ? 'Remove from favorites' : 'Add to favorites'}
         >
-          <span
-            className={`material-icons transition-colors ${
-              isFavorite ? 'text-red-500 hover:text-red-600' : 'text-gray-400 hover:text-red-500'
-            }`}
-            style={{ fontSize: '14px' }}
-          >
+          <span className="material-icons" style={{ fontSize: '16px' }}>
             {isFavorite ? 'favorite' : 'favorite_border'}
           </span>
-        </div>
+        </button>
       </div>
-
-      <span className="text-xs font-medium text-center truncate w-full px-1" title={pattern.name}>
-        {pattern.name}
-      </span>
     </button>
   )
 }

+ 4 - 4
frontend/src/pages/LEDPage.tsx

@@ -393,11 +393,11 @@ export function LEDPage() {
 
   // DW LEDs control panel
   return (
-    <div className="flex flex-col w-full max-w-5xl mx-auto gap-6 py-6 px-4">
+    <div className="flex flex-col w-full max-w-5xl mx-auto gap-6 py-3 sm:py-6 px-3 sm:px-4">
       {/* Page Header */}
-      <div className="space-y-0.5 sm:space-y-1">
-        <h1 className="text-xl sm:text-3xl font-bold tracking-tight">LED Control</h1>
-        <p className="text-xs sm:text-base text-muted-foreground">DW LEDs - GPIO controlled LED strip</p>
+      <div className="space-y-0.5 sm:space-y-1 pl-1">
+        <h1 className="text-xl font-semibold tracking-tight">LED Control</h1>
+        <p className="text-xs text-muted-foreground">DW LEDs - GPIO controlled LED strip</p>
       </div>
 
       <Separator />

+ 4 - 4
frontend/src/pages/PlaylistsPage.tsx

@@ -474,11 +474,11 @@ export function PlaylistsPage() {
   }
 
   return (
-    <div className="flex flex-col w-full max-w-5xl mx-auto gap-4 sm:gap-6 py-4 sm:py-6 h-[calc(100dvh-10rem)] sm:h-[calc(100dvh-10.5rem)] overflow-hidden">
+    <div className="flex flex-col w-full max-w-5xl mx-auto gap-4 sm:gap-6 py-3 sm:py-6 px-3 sm:px-4 h-[calc(100dvh-7rem)] overflow-hidden">
       {/* Page Header */}
-      <div className="space-y-0.5 sm:space-y-1 shrink-0">
-        <h1 className="text-xl sm:text-3xl font-bold tracking-tight">Playlists</h1>
-        <p className="text-sm sm:text-base text-muted-foreground">
+      <div className="space-y-0.5 sm:space-y-1 shrink-0 pl-1">
+        <h1 className="text-xl font-semibold tracking-tight">Playlists</h1>
+        <p className="text-xs text-muted-foreground">
           Create and manage pattern playlists
         </p>
       </div>

+ 4 - 4
frontend/src/pages/SettingsPage.tsx

@@ -719,11 +719,11 @@ export function SettingsPage() {
   }
 
   return (
-    <div className="flex flex-col w-full max-w-5xl mx-auto gap-6 py-6 px-4">
+    <div className="flex flex-col w-full max-w-5xl mx-auto gap-6 py-3 sm:py-6 px-3 sm:px-4">
       {/* Page Header */}
-      <div className="space-y-0.5 sm:space-y-1">
-        <h1 className="text-xl sm:text-3xl font-bold tracking-tight">Settings</h1>
-        <p className="text-xs sm:text-base text-muted-foreground">
+      <div className="space-y-0.5 sm:space-y-1 pl-1">
+        <h1 className="text-xl font-semibold tracking-tight">Settings</h1>
+        <p className="text-xs text-muted-foreground">
           Configure your sand table
         </p>
       </div>

+ 4 - 4
frontend/src/pages/TableControlPage.tsx

@@ -371,11 +371,11 @@ export function TableControlPage() {
 
   return (
     <TooltipProvider>
-      <div className="flex flex-col w-full max-w-5xl mx-auto gap-6 py-6">
+      <div className="flex flex-col w-full max-w-5xl mx-auto gap-6 py-3 sm:py-6 px-3 sm:px-4">
         {/* Page Header */}
-        <div className="space-y-0.5 sm:space-y-1">
-          <h1 className="text-xl sm:text-3xl font-bold tracking-tight">Table Control</h1>
-          <p className="text-xs sm:text-base text-muted-foreground">
+        <div className="space-y-0.5 sm:space-y-1 pl-1">
+          <h1 className="text-xl font-semibold tracking-tight">Table Control</h1>
+          <p className="text-xs text-muted-foreground">
             Manual controls for your sand table
           </p>
         </div>