Sfoglia il codice sorgente

fix(touch): fix invisible button icons in QML touch app

- Add color: "white" to ModernControlButton icon Text (was defaulting to black)
- Replace 🗑 emoji with ✕ symbol for delete button (emoji not in Pi fonts)
- Revert unnecessary frontend Lucide icon changes (3 commits)

Fixes Shutdown Pi button and Delete Playlist button visibility on Pi.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
tuanchris 1 settimana fa
parent
commit
165313004d

+ 1 - 0
dune-weaver-touch/qml/components/ModernControlButton.qml

@@ -52,6 +52,7 @@ Rectangle {
         Text {
             text: parent.parent.icon
             font.pixelSize: parent.parent.fontSize + 2
+            color: "white"
             visible: parent.parent.icon !== ""
         }
         

+ 1 - 1
dune-weaver-touch/qml/pages/ModernPlaylistPage.qml

@@ -336,7 +336,7 @@ Page {
 
                     // Delete playlist button
                     Text {
-                        text: "🗑"
+                        text: ""
                         font.pixelSize: 20
                         color: deletePlaylistMouseArea.pressed ? "#dc2626" : Components.ThemeManager.textSecondary
 

+ 63 - 77
frontend/src/components/layout/Layout.tsx

@@ -1,36 +1,6 @@
 import { Outlet, Link, useLocation } from 'react-router-dom'
 import { useEffect, useState, useRef, useCallback } from 'react'
 import { toast } from 'sonner'
-import {
-  Power,
-  RotateCcw,
-  RefreshCw,
-  Loader2,
-  CheckCircle,
-  Terminal,
-  Copy,
-  Eye,
-  EyeOff,
-  X,
-  Menu,
-  FileText,
-  Download,
-  HardDriveDownload,
-  Grid,
-  ListMusic,
-  SlidersHorizontal,
-  Lightbulb,
-  Settings as SettingsIcon,
-  Wifi,
-  WifiOff,
-  AlertCircle,
-  Home,
-  Sun,
-  Moon,
-  ChevronDown,
-  PlayCircle,
-  StopCircle,
-} from 'lucide-react'
 import { NowPlayingBar } from '@/components/NowPlayingBar'
 import { Button } from '@/components/ui/button'
 import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover'
@@ -42,11 +12,11 @@ import { apiClient } from '@/lib/apiClient'
 import ShinyText from '@/components/ShinyText'
 
 const navItems = [
-  { path: '/', label: 'Browse', icon: Grid, title: 'Browse Patterns' },
-  { path: '/playlists', label: 'Playlists', icon: ListMusic, title: 'Playlists' },
-  { path: '/table-control', label: 'Control', icon: SlidersHorizontal, title: 'Table Control' },
-  { path: '/led', label: 'LED', icon: Lightbulb, title: 'LED Control' },
-  { path: '/settings', label: 'Settings', icon: SettingsIcon, title: 'Settings' },
+  { path: '/', label: 'Browse', icon: 'grid_view', title: 'Browse Patterns' },
+  { path: '/playlists', label: 'Playlists', icon: 'playlist_play', title: 'Playlists' },
+  { path: '/table-control', label: 'Control', icon: 'tune', title: 'Table Control' },
+  { path: '/led', label: 'LED', icon: 'lightbulb', title: 'LED Control' },
+  { path: '/settings', label: 'Settings', icon: 'settings', title: 'Settings' },
 ]
 
 const DEFAULT_APP_NAME = 'Dune Weaver'
@@ -1027,7 +997,9 @@ export function Layout() {
             <div className="p-6">
               <div className="text-center space-y-4">
                 <div className="inline-flex items-center justify-center w-16 h-16 rounded-full bg-destructive/10 mb-2">
-<AlertCircle className="h-10 w-10 text-destructive" />
+                  <span className="material-icons-outlined text-4xl text-destructive">
+                    error_outline
+                  </span>
                 </div>
                 <h2 className="text-xl font-semibold">Sensor Homing Failed</h2>
                 <p className="text-muted-foreground text-sm">
@@ -1057,7 +1029,7 @@ export function Layout() {
                       onClick={() => handleSensorHomingRecovery(false)}
                       className="w-full gap-2"
                     >
-                      <RefreshCw className="h-4 w-4" />
+                      <span className="material-icons text-base">refresh</span>
                       Retry Sensor Homing
                     </Button>
                     <Button
@@ -1065,7 +1037,7 @@ export function Layout() {
                       onClick={() => handleSensorHomingRecovery(true)}
                       className="w-full gap-2"
                     >
-                      <RefreshCw className="h-4 w-4" />
+                      <span className="material-icons text-base">sync_alt</span>
                       Switch to Crash Homing
                     </Button>
                     <p className="text-xs text-muted-foreground">
@@ -1074,7 +1046,7 @@ export function Layout() {
                   </div>
                 ) : (
                   <div className="flex items-center justify-center gap-2 py-4">
-                    <Loader2 className="h-5 w-5 text-primary animate-spin" />
+                    <span className="material-icons-outlined text-primary animate-spin">sync</span>
                     <span className="text-muted-foreground">Attempting recovery...</span>
                   </div>
                 )}
@@ -1090,7 +1062,9 @@ export function Layout() {
           <div className="w-full max-w-md space-y-6">
             <div className="text-center space-y-4">
               <div className="inline-flex items-center justify-center w-16 h-16 rounded-full bg-primary/10 mb-2">
-<RefreshCw className="h-10 w-10 text-primary animate-pulse" />
+                <span className="material-icons-outlined text-4xl text-primary animate-pulse">
+                  cached
+                </span>
               </div>
               <h2 className="text-2xl font-bold">Initializing Pattern Cache</h2>
               <p className="text-muted-foreground">
@@ -1139,7 +1113,9 @@ export function Layout() {
             <div className="p-6">
               <div className="text-center space-y-4">
                 <div className="inline-flex items-center justify-center w-12 h-12 rounded-full bg-primary/10 mb-2">
-<HardDriveDownload className="h-6 w-6 text-primary" />
+                  <span className="material-icons-outlined text-2xl text-primary">
+                    download_for_offline
+                  </span>
                 </div>
                 <h2 className="text-xl font-semibold">Cache All Pattern Previews?</h2>
                 <p className="text-muted-foreground text-sm">
@@ -1159,7 +1135,7 @@ export function Layout() {
                       Skip for now
                     </Button>
                     <Button variant="secondary" onClick={handleCacheAllPreviews} className="gap-2">
-                      <RefreshCw className="h-5 w-5" />
+                      <span className="material-icons-outlined text-lg">cached</span>
                       Cache All
                     </Button>
                   </div>
@@ -1187,7 +1163,7 @@ export function Layout() {
                 {cacheAllProgress?.done && (
                   <div className="space-y-4">
                     <p className="text-green-600 dark:text-green-400 flex items-center justify-center gap-2">
-                      <CheckCircle className="h-4 w-4" />
+                      <span className="material-icons text-base">check_circle</span>
                       All {cacheAllProgress.total} previews cached successfully!
                     </p>
                     <Button onClick={handleCloseCacheAllDone} className="w-full">
@@ -1215,13 +1191,15 @@ export function Layout() {
                     ? 'bg-primary/10'
                     : 'bg-amber-500/10'
               }`}>
-{homingJustCompleted ? (
-                  <CheckCircle className="h-10 w-10 text-green-500" />
-                ) : isHoming ? (
-                  <Loader2 className="h-10 w-10 text-primary animate-spin" />
-                ) : (
-                  <Home className="h-10 w-10 text-amber-500 animate-pulse" />
-                )}
+                <span className={`material-icons-outlined text-4xl ${
+                  homingJustCompleted
+                    ? 'text-green-500'
+                    : isHoming
+                      ? 'text-primary animate-spin'
+                      : 'text-amber-500 animate-pulse'
+                }`}>
+                  {homingJustCompleted ? 'check_circle' : 'sync'}
+                </span>
               </div>
               <h2 className="text-2xl font-bold">
                 {homingJustCompleted
@@ -1266,7 +1244,7 @@ export function Layout() {
             <div className="bg-muted/50 rounded-lg border overflow-hidden">
               <div className="flex items-center justify-between px-4 py-2 border-b bg-muted">
                 <div className="flex items-center gap-2">
-                  <Terminal className="h-4 w-4" />
+                  <span className="material-icons-outlined text-base">terminal</span>
                   <span className="text-sm font-medium">
                     {isHoming || homingJustCompleted ? 'Homing Log' : 'Connection Log'}
                   </span>
@@ -1282,7 +1260,7 @@ export function Layout() {
                     className="text-xs text-muted-foreground hover:text-foreground flex items-center gap-1 transition-colors"
                     title="Copy logs to clipboard"
                   >
-                    <Copy className="h-3.5 w-3.5" />
+                    <span className="material-icons text-sm">content_copy</span>
                     Copy
                   </button>
                   <span className="text-xs text-muted-foreground">
@@ -1323,7 +1301,7 @@ export function Layout() {
                       onClick={() => setKeepHomingLogsOpen(true)}
                       className="gap-2"
                     >
-                      <Eye className="h-4 w-4" />
+                      <span className="material-icons text-base">visibility</span>
                       Keep Open
                     </Button>
                     <Button
@@ -1333,7 +1311,7 @@ export function Layout() {
                       }}
                       className="gap-2"
                     >
-                      <X className="h-4 w-4" />
+                      <span className="material-icons text-base">close</span>
                       Dismiss
                     </Button>
                   </>
@@ -1345,7 +1323,7 @@ export function Layout() {
                     }}
                     className="gap-2"
                   >
-                    <X className="h-4 w-4" />
+                    <span className="material-icons text-base">close</span>
                     Close Logs
                   </Button>
                 )}
@@ -1360,7 +1338,7 @@ export function Layout() {
                   onClick={() => setHomingDismissed(true)}
                   className="gap-2 text-muted-foreground"
                 >
-                  <EyeOff className="h-4 w-4" />
+                  <span className="material-icons text-base">visibility_off</span>
                   Dismiss
                 </Button>
               </div>
@@ -1405,7 +1383,9 @@ export function Layout() {
                   shineColor={isDark ? '#ffffff' : '#999999'}
                   spread={75}
                 />
-<ChevronDown className="h-3.5 w-3.5 text-muted-foreground group-hover:text-foreground transition-colors" />
+                <span className="material-icons-outlined text-muted-foreground text-sm group-hover:text-foreground transition-colors">
+                  expand_more
+                </span>
                 <span
                   className={`w-2 h-2 rounded-full ${
                     !isBackendConnected
@@ -1436,7 +1416,7 @@ export function Layout() {
                   className="rounded-full"
                   aria-label="Open menu"
                 >
-                  <Menu className="h-5 w-5" />
+                  <span className="material-icons-outlined">menu</span>
                 </Button>
               </PopoverTrigger>
               <PopoverContent align="end" className="w-56 p-2">
@@ -1445,14 +1425,16 @@ export function Layout() {
                     onClick={() => setIsDark(!isDark)}
                     className="flex items-center gap-3 w-full px-3 py-2 text-sm rounded-md hover:bg-accent transition-colors"
                   >
-                    {isDark ? <Sun className="h-5 w-5" /> : <Moon className="h-5 w-5" />}
+                    <span className="material-icons-outlined text-xl">
+                      {isDark ? 'light_mode' : 'dark_mode'}
+                    </span>
                     {isDark ? 'Light Mode' : 'Dark Mode'}
                   </button>
                   <button
                     onClick={handleToggleLogs}
                     className="flex items-center gap-3 w-full px-3 py-2 text-sm rounded-md hover:bg-accent transition-colors"
                   >
-                    <FileText className="h-5 w-5" />
+                    <span className="material-icons-outlined text-xl">article</span>
                     View Logs
                   </button>
                   <Separator className="my-1" />
@@ -1460,14 +1442,14 @@ export function Layout() {
                     onClick={handleRestart}
                     className="flex items-center gap-3 w-full px-3 py-2 text-sm rounded-md hover:bg-accent transition-colors text-amber-500"
                   >
-                    <RotateCcw className="h-5 w-5" />
+                    <span className="material-icons-outlined text-xl">restart_alt</span>
                     Restart Docker
                   </button>
                   <button
                     onClick={handleShutdown}
                     className="flex items-center gap-3 w-full px-3 py-2 text-sm rounded-md hover:bg-accent transition-colors text-red-500"
                   >
-                    <Power className="h-5 w-5" />
+                    <span className="material-icons-outlined text-xl">power_settings_new</span>
                     Shutdown
                   </button>
                 </div>
@@ -1485,7 +1467,9 @@ export function Layout() {
                   className="rounded-full"
                   aria-label="Open menu"
                 >
-{isMobileMenuOpen ? <X className="h-5 w-5" /> : <Menu className="h-5 w-5" />}
+                  <span className="material-icons-outlined">
+                    {isMobileMenuOpen ? 'close' : 'menu'}
+                  </span>
                 </Button>
               </PopoverTrigger>
               <PopoverContent align="end" className="w-56 p-2">
@@ -1497,7 +1481,9 @@ export function Layout() {
                     }}
                     className="flex items-center gap-3 w-full px-3 py-2 text-sm rounded-md hover:bg-accent transition-colors"
                   >
-                    {isDark ? <Sun className="h-5 w-5" /> : <Moon className="h-5 w-5" />}
+                    <span className="material-icons-outlined text-xl">
+                      {isDark ? 'light_mode' : 'dark_mode'}
+                    </span>
                     {isDark ? 'Light Mode' : 'Dark Mode'}
                   </button>
                   <button
@@ -1507,7 +1493,7 @@ export function Layout() {
                     }}
                     className="flex items-center gap-3 w-full px-3 py-2 text-sm rounded-md hover:bg-accent transition-colors"
                   >
-                    <FileText className="h-5 w-5" />
+                    <span className="material-icons-outlined text-xl">article</span>
                     View Logs
                   </button>
                   <Separator className="my-1" />
@@ -1518,7 +1504,7 @@ export function Layout() {
                     }}
                     className="flex items-center gap-3 w-full px-3 py-2 text-sm rounded-md hover:bg-accent transition-colors text-amber-500"
                   >
-                    <RotateCcw className="h-5 w-5" />
+                    <span className="material-icons-outlined text-xl">restart_alt</span>
                     Restart Docker
                   </button>
                   <button
@@ -1528,7 +1514,7 @@ export function Layout() {
                     }}
                     className="flex items-center gap-3 w-full px-3 py-2 text-sm rounded-md hover:bg-accent transition-colors text-red-500"
                   >
-                    <Power className="h-5 w-5" />
+                    <span className="material-icons-outlined text-xl">power_settings_new</span>
                     Shutdown
                   </button>
                 </div>
@@ -1617,7 +1603,7 @@ export function Layout() {
                   className="rounded-full"
                   title="Copy logs"
                 >
-                  <Copy className="h-4 w-4" />
+                  <span className="material-icons-outlined text-base">content_copy</span>
                 </Button>
                 <Button
                   variant="ghost"
@@ -1626,7 +1612,7 @@ export function Layout() {
                   className="rounded-full"
                   title="Download logs"
                 >
-                  <Download className="h-4 w-4" />
+                  <span className="material-icons-outlined text-base">download</span>
                 </Button>
                 <Button
                   variant="ghost"
@@ -1635,7 +1621,7 @@ export function Layout() {
                   className="rounded-full"
                   title="Close"
                 >
-                  <X className="h-4 w-4" />
+                  <span className="material-icons-outlined text-base">close</span>
                 </Button>
               </div>
             </div>
@@ -1648,7 +1634,7 @@ export function Layout() {
               {/* Loading indicator for older logs */}
               {isLoadingMoreLogs && (
                 <div className="flex items-center justify-center gap-2 py-2 text-muted-foreground">
-                  <Loader2 className="h-3.5 w-3.5 animate-spin" />
+                  <span className="material-icons-outlined text-sm animate-spin">sync</span>
                   <span>Loading older logs...</span>
                 </div>
               )}
@@ -1691,11 +1677,9 @@ export function Layout() {
           style={{ bottom: 'calc(4.5rem + env(safe-area-inset-bottom, 0px))' }}
           aria-label={isCurrentlyPlaying ? 'Now Playing' : 'Not Playing'}
         >
-{isCurrentlyPlaying ? (
-            <PlayCircle className="h-5 w-5 text-primary" />
-          ) : (
-            <StopCircle className="h-5 w-5 text-muted-foreground" />
-          )}
+          <span className={`material-icons-outlined text-xl ${isCurrentlyPlaying ? 'text-primary' : 'text-muted-foreground'}`}>
+            {isCurrentlyPlaying ? 'play_circle' : 'stop_circle'}
+          </span>
           <span className="text-sm font-medium">
             {isCurrentlyPlaying ? 'Now Playing' : 'Not Playing'}
           </span>
@@ -1721,7 +1705,9 @@ export function Layout() {
                 {isActive && (
                   <span className="absolute -top-0.5 w-8 h-1 rounded-full bg-primary" />
                 )}
-                <item.icon className="h-5 w-5" strokeWidth={isActive ? 2.5 : 1.5} />
+                <span className={`text-xl ${isActive ? 'material-icons' : 'material-icons-outlined'}`}>
+                  {item.icon}
+                </span>
                 <span className="text-xs font-medium">{item.label}</span>
               </Link>
             )

+ 43 - 61
frontend/src/pages/PlaylistsPage.tsx

@@ -1,32 +1,6 @@
 import { useState, useEffect, useMemo, useCallback, useRef } from 'react'
 import { toast } from 'sonner'
-import {
-  Trash2,
-  Plus,
-  ListPlus,
-  ListMusic,
-  Pencil,
-  ArrowLeft,
-  Hand,
-  Music,
-  X,
-  Shuffle,
-  Repeat,
-  Minus,
-  ArrowUpDown,
-  ArrowUp,
-  ArrowDown,
-  Loader2,
-  Play,
-  Save,
-  Search,
-  SearchX,
-  Folder,
-  CheckCircle,
-  Check,
-  Image,
-  Sparkles,
-} from 'lucide-react'
+import { Trash2 } from 'lucide-react'
 import { apiClient } from '@/lib/apiClient'
 import {
   initPreviewCacheDB,
@@ -577,7 +551,7 @@ export function PlaylistsPage() {
                 setIsCreateModalOpen(true)
               }}
             >
-              <Plus className="h-5 w-5" />
+              <span className="material-icons-outlined text-xl">add</span>
             </Button>
           </div>
 
@@ -588,7 +562,7 @@ export function PlaylistsPage() {
             </div>
           ) : playlists.length === 0 ? (
             <div className="flex flex-col items-center justify-center py-8 text-muted-foreground gap-2">
-              <ListPlus className="h-8 w-8" />
+              <span className="material-icons-outlined text-3xl">playlist_add</span>
               <span className="text-sm">No playlists yet</span>
             </div>
           ) : (
@@ -603,7 +577,7 @@ export function PlaylistsPage() {
                 onClick={() => handleSelectPlaylist(name)}
               >
                 <div className="flex items-center gap-2 min-w-0">
-                  <ListMusic className="h-5 w-5" />
+                  <span className="material-icons-outlined text-lg">playlist_play</span>
                   <span className="truncate text-sm font-medium">{name}</span>
                 </div>
                 <div className="flex items-center gap-1 opacity-100 sm:opacity-0 sm:group-hover:opacity-100 transition-opacity">
@@ -618,7 +592,7 @@ export function PlaylistsPage() {
                       setIsRenameModalOpen(true)
                     }}
                   >
-                    <Pencil className="h-4 w-4" />
+                    <span className="material-icons-outlined text-base">edit</span>
                   </Button>
                   <Button
                     variant="ghost"
@@ -656,7 +630,7 @@ export function PlaylistsPage() {
                 className="h-8 w-8 lg:hidden shrink-0"
                 onClick={handleMobileBack}
               >
-                <ArrowLeft className="h-5 w-5" />
+                <span className="material-icons-outlined">arrow_back</span>
               </Button>
               <div className="min-w-0">
                 <h2 className="text-lg font-semibold truncate">
@@ -675,7 +649,7 @@ export function PlaylistsPage() {
               size="sm"
               className="gap-2"
             >
-              <Plus className="h-4 w-4" />
+              <span className="material-icons-outlined text-base">add</span>
               <span className="hidden sm:inline">Add Patterns</span>
             </Button>
           </header>
@@ -685,7 +659,7 @@ export function PlaylistsPage() {
             {!selectedPlaylist ? (
               <div className="flex flex-col items-center justify-center h-full text-muted-foreground gap-3">
                 <div className="p-4 rounded-full bg-muted">
-                  <Hand className="h-12 w-12" />
+                  <span className="material-icons-outlined text-5xl">touch_app</span>
                 </div>
                 <div className="text-center">
                   <p className="font-medium">No playlist selected</p>
@@ -695,14 +669,14 @@ export function PlaylistsPage() {
             ) : playlistPatterns.length === 0 ? (
               <div className="flex flex-col items-center justify-center h-full text-muted-foreground gap-3">
                 <div className="p-4 rounded-full bg-muted">
-                  <Music className="h-12 w-12" />
+                  <span className="material-icons-outlined text-5xl">library_music</span>
                 </div>
                 <div className="text-center">
                   <p className="font-medium">Empty playlist</p>
                   <p className="text-sm">Add patterns to get started</p>
                 </div>
                 <Button variant="secondary" className="mt-2 gap-2" onClick={openPatternPicker}>
-                  <Plus className="h-4 w-4" />
+                  <span className="material-icons-outlined text-base">add</span>
                   Add Patterns
                 </Button>
               </div>
@@ -727,7 +701,7 @@ export function PlaylistsPage() {
                         onClick={() => handleRemovePattern(path)}
                         title="Remove from playlist"
                       >
-                        <X className="h-3 w-3" />
+                        <span className="material-icons" style={{ fontSize: '12px' }}>close</span>
                       </button>
                     </div>
                     <p className="text-[10px] sm:text-xs truncate font-medium w-full text-center">{getPatternName(path)}</p>
@@ -758,7 +732,7 @@ export function PlaylistsPage() {
                       }`}
                       title="Shuffle"
                     >
-                      <Shuffle className="h-5 w-5 sm:h-6 sm:w-6" />
+                      <span className="material-icons-outlined text-lg sm:text-xl">shuffle</span>
                     </button>
                     <button
                       onClick={() => setRunMode(runMode === 'indefinite' ? 'single' : 'indefinite')}
@@ -769,7 +743,7 @@ export function PlaylistsPage() {
                       }`}
                       title={runMode === 'indefinite' ? 'Loop mode' : 'Play once mode'}
                     >
-                      <Repeat className="h-5 w-5 sm:h-6 sm:w-6" />
+                      <span className="material-icons-outlined text-lg sm:text-xl">repeat</span>
                     </button>
                   </div>
 
@@ -786,7 +760,7 @@ export function PlaylistsPage() {
                           setPauseTime(Math.max(0, pauseTime - step))
                         }}
                       >
-                        <Minus className="h-3 w-3" />
+                        <span className="material-icons-outlined text-sm">remove</span>
                       </Button>
                       <button
                         onClick={() => {
@@ -798,7 +772,7 @@ export function PlaylistsPage() {
                         title="Click to change unit"
                       >
                         {pauseTime}{pauseUnit === 'sec' ? 's' : pauseUnit === 'min' ? 'm' : 'h'}
-                        <ArrowUpDown className="h-2.5 w-2.5 opacity-50 ml-0.5" />
+                        <span className="material-icons-outlined text-xs opacity-50 scale-75 ml-0.5">swap_vert</span>
                       </button>
                       <Button
                         variant="secondary"
@@ -809,7 +783,7 @@ export function PlaylistsPage() {
                           setPauseTime(pauseTime + step)
                         }}
                       >
-                        <Plus className="h-3 w-3" />
+                        <span className="material-icons-outlined text-sm">add</span>
                       </Button>
                     </div>
                   </div>
@@ -820,9 +794,9 @@ export function PlaylistsPage() {
                       <SelectTrigger className={`w-9 h-9 sm:w-10 sm:h-10 rounded-full border-0 p-0 shadow-none focus:ring-0 justify-center [&>svg]:hidden transition ${
                         clearPattern !== 'none' ? '!bg-primary/10' : '!bg-transparent hover:!bg-muted'
                       }`}>
-                        <Sparkles className={`h-5 w-5 sm:h-6 sm:w-6 ${
+                        <span className={`material-icons-outlined text-lg sm:text-xl ${
                           clearPattern !== 'none' ? 'text-primary' : 'text-muted-foreground'
-                        }`} />
+                        }`}>cleaning_services</span>
                       </SelectTrigger>
                       <SelectContent>
                         {preExecutionOptions.map(opt => (
@@ -843,9 +817,9 @@ export function PlaylistsPage() {
                   title="Run Playlist"
                 >
                   {isRunning ? (
-                    <Loader2 className="h-5 w-5 sm:h-6 sm:w-6 animate-spin" />
+                    <span className="material-icons-outlined text-xl sm:text-2xl animate-spin">sync</span>
                   ) : (
-                    <Play className="h-5 w-5 sm:h-6 sm:w-6 ml-0.5" />
+                    <span className="material-icons text-xl sm:text-2xl ml-0.5">play_arrow</span>
                   )}
                 </button>
               </div>
@@ -859,7 +833,7 @@ export function PlaylistsPage() {
         <DialogContent className="sm:max-w-md">
           <DialogHeader>
             <DialogTitle className="flex items-center gap-2">
-              <ListPlus className="h-5 w-5 text-primary" />
+              <span className="material-icons-outlined text-primary">playlist_add</span>
               Create New Playlist
             </DialogTitle>
           </DialogHeader>
@@ -881,7 +855,7 @@ export function PlaylistsPage() {
               Cancel
             </Button>
             <Button onClick={handleCreatePlaylist} className="gap-2">
-              <Plus className="h-4 w-4" />
+              <span className="material-icons-outlined text-base">add</span>
               Create Playlist
             </Button>
           </DialogFooter>
@@ -893,7 +867,7 @@ export function PlaylistsPage() {
         <DialogContent className="sm:max-w-md">
           <DialogHeader>
             <DialogTitle className="flex items-center gap-2">
-              <Pencil className="h-5 w-5 text-primary" />
+              <span className="material-icons-outlined text-primary">edit</span>
               Rename Playlist
             </DialogTitle>
           </DialogHeader>
@@ -915,7 +889,7 @@ export function PlaylistsPage() {
               Cancel
             </Button>
             <Button onClick={handleRenamePlaylist} className="gap-2">
-              <Save className="h-4 w-4" />
+              <span className="material-icons-outlined text-base">save</span>
               Save Name
             </Button>
           </DialogFooter>
@@ -927,7 +901,7 @@ export function PlaylistsPage() {
         <DialogContent className="max-w-4xl max-h-[90vh] flex flex-col">
           <DialogHeader>
             <DialogTitle className="flex items-center gap-2">
-              <ListPlus className="h-5 w-5 text-primary" />
+              <span className="material-icons-outlined text-primary">playlist_add</span>
               Add Patterns to {selectedPlaylist}
             </DialogTitle>
           </DialogHeader>
@@ -935,7 +909,9 @@ export function PlaylistsPage() {
           {/* Search and Filters */}
           <div className="space-y-3 py-2">
             <div className="relative">
-              <Search className="absolute left-3 top-1/2 -translate-y-1/2 text-muted-foreground h-5 w-5" />
+              <span className="material-icons-outlined absolute left-3 top-1/2 -translate-y-1/2 text-muted-foreground text-lg">
+                search
+              </span>
               <Input
                 value={searchQuery}
                 onChange={(e) => setSearchQuery(e.target.value)}
@@ -947,7 +923,7 @@ export function PlaylistsPage() {
                   className="absolute right-3 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground"
                   onClick={() => setSearchQuery('')}
                 >
-                  <X className="h-5 w-5" />
+                  <span className="material-icons-outlined text-lg">close</span>
                 </button>
               )}
             </div>
@@ -956,7 +932,7 @@ export function PlaylistsPage() {
               {/* Folder dropdown - icon only on mobile, with text on sm+ */}
               <Select value={selectedCategory} onValueChange={setSelectedCategory}>
                 <SelectTrigger className="h-9 w-9 sm:w-auto rounded-full bg-card border-border shadow-sm text-sm px-0 sm:px-3 justify-center sm:justify-between [&>svg]:hidden sm:[&>svg]:block [&>span:last-of-type]:hidden sm:[&>span:last-of-type]:inline gap-2">
-                  <Folder className="h-5 w-5 shrink-0" />
+                  <span className="material-icons-outlined text-lg shrink-0">folder</span>
                   <SelectValue />
                 </SelectTrigger>
                 <SelectContent>
@@ -971,7 +947,7 @@ export function PlaylistsPage() {
               {/* Sort dropdown - icon only on mobile, with text on sm+ */}
               <Select value={sortBy} onValueChange={(v) => setSortBy(v as SortOption)}>
                 <SelectTrigger className="h-9 w-9 sm:w-auto rounded-full bg-card border-border shadow-sm text-sm px-0 sm:px-3 justify-center sm:justify-between [&>svg]:hidden sm:[&>svg]:block [&>span:last-of-type]:hidden sm:[&>span:last-of-type]:inline gap-2">
-                  <ArrowUpDown className="h-5 w-5 shrink-0" />
+                  <span className="material-icons-outlined text-lg shrink-0">sort</span>
                   <SelectValue />
                 </SelectTrigger>
                 <SelectContent>
@@ -990,14 +966,16 @@ export function PlaylistsPage() {
                 onClick={() => setSortAsc(!sortAsc)}
                 title={sortAsc ? 'Ascending' : 'Descending'}
               >
-                {sortAsc ? <ArrowUp className="h-5 w-5" /> : <ArrowDown className="h-5 w-5" />}
+                <span className="material-icons-outlined text-lg">
+                  {sortAsc ? 'arrow_upward' : 'arrow_downward'}
+                </span>
               </Button>
 
               <div className="flex-1" />
 
               {/* Selection count - compact on mobile */}
               <div className="flex items-center gap-1 sm:gap-2 text-sm bg-card rounded-full px-2 sm:px-3 py-2 shadow-sm border">
-                <CheckCircle className="h-4 w-4 text-primary" />
+                <span className="material-icons-outlined text-base text-primary">check_circle</span>
                 <span className="font-medium">{selectedPatternPaths.size}</span>
                 <span className="hidden sm:inline text-muted-foreground">selected</span>
               </div>
@@ -1009,7 +987,7 @@ export function PlaylistsPage() {
             {filteredPatterns.length === 0 ? (
               <div className="flex flex-col items-center justify-center h-full text-muted-foreground gap-3">
                 <div className="p-4 rounded-full bg-muted">
-                  <SearchX className="h-12 w-12" />
+                  <span className="material-icons-outlined text-5xl">search_off</span>
                 </div>
                 <span className="text-sm">No patterns found</span>
               </div>
@@ -1038,7 +1016,9 @@ export function PlaylistsPage() {
                         />
                         {isSelected && (
                           <div className="absolute -top-1 -right-1 w-5 h-5 rounded-full bg-primary flex items-center justify-center shadow-md">
-                            <Check className="h-3.5 w-3.5 text-primary-foreground" />
+                            <span className="material-icons text-primary-foreground" style={{ fontSize: '14px' }}>
+                              check
+                            </span>
                           </div>
                         )}
                       </div>
@@ -1057,7 +1037,7 @@ export function PlaylistsPage() {
               Cancel
             </Button>
             <Button onClick={handleSavePatterns} className="gap-2">
-              <Save className="h-4 w-4" />
+              <span className="material-icons-outlined text-base">save</span>
               Save Selection
             </Button>
           </DialogFooter>
@@ -1111,7 +1091,9 @@ function LazyPatternPreview({ path, previewUrl, requestPreview, alt, className =
           className="w-full h-full object-cover pattern-preview"
         />
       ) : (
-        <Image className="h-4 w-4 sm:h-5 sm:w-5 text-muted-foreground" />
+        <span className="material-icons-outlined text-muted-foreground text-sm sm:text-base">
+          image
+        </span>
       )}
     </div>
   )

+ 35 - 52
frontend/src/pages/TableControlPage.tsx

@@ -1,24 +1,5 @@
 import { useState, useEffect, useRef } from 'react'
 import { toast } from 'sonner'
-import {
-  Loader2,
-  Home,
-  StopCircle,
-  RotateCcw,
-  RotateCw,
-  AlertTriangle,
-  Check,
-  Target,
-  Circle,
-  Compass,
-  Maximize2,
-  ArrowLeftRight,
-  Terminal,
-  Trash2,
-  Power,
-  PowerOff,
-  Send,
-} from 'lucide-react'
 import { Button } from '@/components/ui/button'
 import {
   Card,
@@ -442,9 +423,9 @@ export function TableControlPage() {
                       className="h-16 gap-1 flex-col items-center justify-center"
                     >
                       {isLoading === 'home' ? (
-                        <Loader2 className="h-6 w-6 animate-spin" />
+                        <span className="material-icons-outlined animate-spin text-2xl">sync</span>
                       ) : (
-                        <Home className="h-6 w-6" />
+                        <span className="material-icons-outlined text-2xl">home</span>
                       )}
                       <span className="text-xs">Home</span>
                     </Button>
@@ -461,9 +442,9 @@ export function TableControlPage() {
                       className="h-16 gap-1 flex-col items-center justify-center"
                     >
                       {isLoading === 'stop' ? (
-                        <Loader2 className="h-6 w-6 animate-spin" />
+                        <span className="material-icons-outlined animate-spin text-2xl">sync</span>
                       ) : (
-                        <StopCircle className="h-6 w-6" />
+                        <span className="material-icons-outlined text-2xl">stop_circle</span>
                       )}
                       <span className="text-xs">Stop</span>
                     </Button>
@@ -481,9 +462,9 @@ export function TableControlPage() {
                           className="h-16 gap-1 flex-col items-center justify-center"
                         >
                           {isLoading === 'reset' ? (
-                            <Loader2 className="h-6 w-6 animate-spin" />
+                            <span className="material-icons-outlined animate-spin text-2xl">sync</span>
                           ) : (
-                            <RotateCcw className="h-6 w-6" />
+                            <span className="material-icons-outlined text-2xl">restart_alt</span>
                           )}
                           <span className="text-xs">Reset</span>
                         </Button>
@@ -499,7 +480,7 @@ export function TableControlPage() {
                       </DialogDescription>
                     </DialogHeader>
                     <Alert className="flex items-center border-amber-500/50">
-                      <AlertTriangle className="h-4 w-4 text-amber-500 mr-2 shrink-0" />
+                      <span className="material-icons-outlined text-amber-500 text-base mr-2 shrink-0">warning</span>
                       <AlertDescription className="text-amber-600 dark:text-amber-400">
                         Homing is required after resetting. The table will lose its position reference.
                       </AlertDescription>
@@ -551,9 +532,9 @@ export function TableControlPage() {
                   className="gap-2"
                 >
                   {isLoading === 'speed' ? (
-                    <Loader2 className="h-4 w-4 animate-spin" />
+                    <span className="material-icons-outlined animate-spin">sync</span>
                   ) : (
-                    <Check className="h-4 w-4" />
+                    <span className="material-icons-outlined">check</span>
                   )}
                   Set
                 </Button>
@@ -578,9 +559,9 @@ export function TableControlPage() {
                       className="h-16 gap-1 flex-col items-center justify-center"
                     >
                       {isLoading === 'center' ? (
-                        <Loader2 className="h-6 w-6 animate-spin" />
+                        <span className="material-icons-outlined animate-spin text-2xl">sync</span>
                       ) : (
-                        <Target className="h-6 w-6" />
+                        <span className="material-icons-outlined text-2xl">center_focus_strong</span>
                       )}
                       <span className="text-xs">Center</span>
                     </Button>
@@ -597,9 +578,9 @@ export function TableControlPage() {
                       className="h-16 gap-1 flex-col items-center justify-center"
                     >
                       {isLoading === 'perimeter' ? (
-                        <Loader2 className="h-6 w-6 animate-spin" />
+                        <span className="material-icons-outlined animate-spin text-2xl">sync</span>
                       ) : (
-                        <Circle className="h-6 w-6" />
+                        <span className="material-icons-outlined text-2xl">trip_origin</span>
                       )}
                       <span className="text-xs">Perimeter</span>
                     </Button>
@@ -615,7 +596,7 @@ export function TableControlPage() {
                           variant="secondary"
                           className="h-16 gap-1 flex-col items-center justify-center"
                         >
-                          <Compass className="h-6 w-6" />
+                          <span className="material-icons-outlined text-2xl">screen_rotation</span>
                           <span className="text-xs">Align</span>
                         </Button>
                       </DialogTrigger>
@@ -652,7 +633,9 @@ export function TableControlPage() {
                     <Separator />
 
                     <Alert className="flex items-start border-amber-500/50">
-                      <AlertTriangle className="h-4 w-4 text-amber-500 mr-2 shrink-0" />
+                      <span className="material-icons-outlined text-amber-500 text-base mr-2 shrink-0">
+                        warning
+                      </span>
                       <AlertDescription className="text-amber-600 dark:text-amber-400">
                         Only perform this when you want to change the orientation reference.
                       </AlertDescription>
@@ -666,7 +649,7 @@ export function TableControlPage() {
                           onClick={() => handleRotate(-10)}
                           disabled={isLoading === 'rotate'}
                         >
-                          <RotateCcw className="h-5 w-5 mr-1" />
+                          <span className="material-icons text-lg mr-1">rotate_left</span>
                           CCW 10°
                         </Button>
                         <Button
@@ -675,7 +658,7 @@ export function TableControlPage() {
                           disabled={isLoading === 'rotate'}
                         >
                           CW 10°
-                          <RotateCw className="h-5 w-5 ml-1" />
+                          <span className="material-icons text-lg ml-1">rotate_right</span>
                         </Button>
                       </div>
                       <p className="text-xs text-muted-foreground text-center">
@@ -711,9 +694,9 @@ export function TableControlPage() {
                       className="h-16 gap-1 flex-col items-center justify-center"
                     >
                       {isLoading === 'clear_from_in.thr' ? (
-                        <Loader2 className="h-6 w-6 animate-spin" />
+                        <span className="material-icons-outlined animate-spin text-2xl">sync</span>
                       ) : (
-                        <Target className="h-6 w-6" />
+                        <span className="material-icons-outlined text-2xl">center_focus_strong</span>
                       )}
                       <span className="text-xs">Clear Center</span>
                     </Button>
@@ -730,9 +713,9 @@ export function TableControlPage() {
                       className="h-16 gap-1 flex-col items-center justify-center"
                     >
                       {isLoading === 'clear_from_out.thr' ? (
-                        <Loader2 className="h-6 w-6 animate-spin" />
+                        <span className="material-icons-outlined animate-spin text-2xl">sync</span>
                       ) : (
-                        <Maximize2 className="h-6 w-6" />
+                        <span className="material-icons-outlined text-2xl">all_out</span>
                       )}
                       <span className="text-xs">Clear Edge</span>
                     </Button>
@@ -749,9 +732,9 @@ export function TableControlPage() {
                       className="h-16 gap-1 flex-col items-center justify-center"
                     >
                       {isLoading === 'clear_sideway.thr' ? (
-                        <Loader2 className="h-6 w-6 animate-spin" />
+                        <span className="material-icons-outlined animate-spin text-2xl">sync</span>
                       ) : (
-                        <ArrowLeftRight className="h-6 w-6" />
+                        <span className="material-icons-outlined text-2xl">swap_horiz</span>
                       )}
                       <span className="text-xs">Clear Sideway</span>
                     </Button>
@@ -769,13 +752,13 @@ export function TableControlPage() {
             <div className="flex items-start justify-between gap-2">
               <div className="min-w-0 space-y-2">
                 <CardTitle className="text-lg flex items-center gap-2">
-                  <Terminal className="h-5 w-5" />
+                  <span className="material-icons-outlined text-xl">terminal</span>
                   Serial Terminal
                 </CardTitle>
                 <CardDescription className="hidden sm:block">Send raw commands to the table controller</CardDescription>
                 {/* Warning about pattern interference */}
                 <Alert className="flex items-center border-amber-500/50 py-2">
-                  <AlertTriangle className="h-4 w-4 text-amber-500 mr-2 shrink-0" />
+                  <span className="material-icons-outlined text-amber-500 text-base mr-2 shrink-0">warning</span>
                   <AlertDescription className="text-xs text-amber-600 dark:text-amber-400">
                     Do not use while a pattern is running. This will interfere with the main connection.
                   </AlertDescription>
@@ -790,7 +773,7 @@ export function TableControlPage() {
                     onClick={() => setSerialHistory([])}
                     title="Clear history"
                   >
-                    <Trash2 className="h-4 w-4" />
+                    <span className="material-icons-outlined">delete_sweep</span>
                   </Button>
                 )}
               </div>
@@ -821,9 +804,9 @@ export function TableControlPage() {
                   title="Connect"
                 >
                   {serialLoading ? (
-                    <Loader2 className="h-4 w-4 animate-spin sm:mr-1" />
+                    <span className="material-icons-outlined animate-spin sm:mr-1">sync</span>
                   ) : (
-                    <Power className="h-4 w-4 sm:mr-1" />
+                    <span className="material-icons-outlined sm:mr-1">power</span>
                   )}
                   <span className="hidden sm:inline">Connect</span>
                 </Button>
@@ -836,7 +819,7 @@ export function TableControlPage() {
                     disabled={serialLoading}
                     title="Disconnect"
                   >
-                    <PowerOff className="h-4 w-4 sm:mr-1" />
+                    <span className="material-icons-outlined sm:mr-1">power_off</span>
                     <span className="hidden sm:inline">Disconnect</span>
                   </Button>
                   <Button
@@ -846,7 +829,7 @@ export function TableControlPage() {
                     disabled={serialLoading}
                     title="Send soft reset to controller"
                   >
-                    <RotateCcw className="h-4 w-4 sm:mr-1" />
+                    <span className="material-icons-outlined sm:mr-1">restart_alt</span>
                     <span className="hidden sm:inline">Reset</span>
                   </Button>
                 </>
@@ -860,7 +843,7 @@ export function TableControlPage() {
                   onClick={() => setSerialHistory([])}
                   title="Clear history"
                 >
-                  <Trash2 className="h-4 w-4" />
+                  <span className="material-icons-outlined">delete</span>
                 </Button>
               )}
             </div>
@@ -915,9 +898,9 @@ export function TableControlPage() {
                 className="h-11 px-6"
               >
                 {serialLoading ? (
-                  <Loader2 className="h-4 w-4 animate-spin" />
+                  <span className="material-icons-outlined animate-spin">sync</span>
                 ) : (
-                  <Send className="h-4 w-4" />
+                  <span className="material-icons-outlined">send</span>
                 )}
               </Button>
             </div>