diff --git a/public/assets/favicon.a64.png b/public/assets/favicon.a64.png new file mode 100644 index 0000000..cddd3bc Binary files /dev/null and b/public/assets/favicon.a64.png differ diff --git a/public/assets/favicon.cbm.png b/public/assets/favicon.cbm.png new file mode 100644 index 0000000..12ae064 Binary files /dev/null and b/public/assets/favicon.cbm.png differ diff --git a/src/app/App.tsx b/src/app/App.tsx index 187c8ad..72fccd9 100644 --- a/src/app/App.tsx +++ b/src/app/App.tsx @@ -1,5 +1,6 @@ import { lazy, Suspense, useEffect, useState } from 'react'; import { Cpu, Wifi, Network, HardDrive, Activity, Search, Wrench, User, FileText, AppWindow, Folder, Edit, Eye, Database, Upload, Download, Code2, LayoutList, Image, ChevronLeft, Loader2, Terminal, Link, Printer, Maximize2, Minimize2 } from 'lucide-react'; +import { AnimatePresence } from 'motion/react'; import { Toaster, toast } from 'sonner'; import StatusPage from './components/StatusPage'; import DevicesPage from './components/DevicesPage'; @@ -54,8 +55,7 @@ type AppId = export default function App() { const [currentPage, setCurrentPage] = useState('status'); const { config, setConfig, saveStatus, pendingCount, flushNow, reload } = useSettings(); - const [showSearch, setShowSearch] = useState(false); - const [searchInitialTab, setSearchInitialTab] = useState<0 | 1 | 2 | 3 | undefined>(undefined); + const [searchPanel, setSearchPanel] = useState<'local' | 'assembly64' | 'commoserve' | 'csdb' | 'last' | null>(null); const [devicesOpenId, setDevicesOpenId] = useState(null); const [isFullscreen, setIsFullscreen] = useState(false); const [fileManagerInitialPath, setFileManagerInitialPath] = useState(undefined); @@ -118,9 +118,9 @@ export default function App() {

Management

} label="Media Manager" onClick={() => { setFileManagerInitialPath(undefined); setCurrentPage('file-manager'); }} /> - } label="Assembly64" onClick={() => { setSearchInitialTab(1); setShowSearch(true); }} /> - } label="CommoServe" onClick={() => { setSearchInitialTab(2); setShowSearch(true); }} /> - } label="CSDb" onClick={() => { setSearchInitialTab(3); setShowSearch(true); }} /> + } label="Assembly64" onClick={() => setSearchPanel('assembly64')} /> + } label="CommoServe" onClick={() => setSearchPanel('commoserve')} /> + } label="CSDb" onClick={() => setSearchPanel('csdb')} /> } label="Print Manager" onClick={() => setCurrentPage('print-manager')} /> } label="Serial Console" onClick={() => setCurrentPage('serial-console')} /> } label="Short Codes" onClick={() => setCurrentPage('serial-console')} /> @@ -262,7 +262,7 @@ function AppPage({ title, onBack }: { title: string; onBack: () => void }) { {isFullscreen ? : }
- {showSearch && ( - setShowSearch(false)} - onOpenFolder={(path: string) => { - setFileManagerInitialPath(path); - setFileManagerReturnPage('status'); - setShowSearch(false); - setCurrentPage('file-manager'); - }} - /> - )} + + {searchPanel && ( + setSearchPanel(null)} + onOpenFolder={(path: string) => { setSearchPanel(null); setFileManagerInitialPath(path); setFileManagerReturnPage('status'); setCurrentPage('file-manager'); }} + /> + )} + ); diff --git a/src/app/components/SearchAssembly64.tsx b/src/app/components/SearchAssembly64.tsx index c1ece59..c5e2ab9 100644 --- a/src/app/components/SearchAssembly64.tsx +++ b/src/app/components/SearchAssembly64.tsx @@ -1,5 +1,5 @@ import { useEffect, useMemo, useRef, useState } from 'react'; -import { Search, Loader2, HardDrive, Download, ChevronRight, ChevronDown, Trophy, Calendar, Users, RefreshCw, HelpCircle } from 'lucide-react'; +import { Search, Loader2, HardDrive, Download, ChevronRight, ChevronDown, Trophy, Calendar, Users, RefreshCw, HelpCircle, X } from 'lucide-react'; import { toast } from 'sonner'; import { humanFileSize, basename, joinPath, putFileContents } from '../webdav'; import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription } from './ui/dialog'; @@ -153,7 +153,7 @@ const AQL_TERMS = [ // ─── Component ──────────────────────────────────────────────────────────────── -export default function SearchAssembly64({ config, setConfig, onClose: _onClose }: SearchAssembly64Props) { +export default function SearchAssembly64({ config, setConfig, onClose }: SearchAssembly64Props) { const scrollRef = useRef(null); const inputRef = useRef(null); const [showAqlHelp, setShowAqlHelp] = useState(false); @@ -322,6 +322,16 @@ export default function SearchAssembly64({ config, setConfig, onClose: _onClose return ( <>
+ {/* Panel header */} +
+
+ + Assembly64 +
+ +
{/* Header */}
{/* Search input */} diff --git a/src/app/components/SearchCSDbNG.tsx b/src/app/components/SearchCSDbNG.tsx index 3720071..4c2a815 100644 --- a/src/app/components/SearchCSDbNG.tsx +++ b/src/app/components/SearchCSDbNG.tsx @@ -1,5 +1,5 @@ import { useEffect, useMemo, useRef, useState } from 'react'; -import { Search, Loader2, HardDrive, Download, ChevronRight } from 'lucide-react'; +import { Search, Loader2, HardDrive, Download, ChevronRight, X, Archive } from 'lucide-react'; import { toast } from 'sonner'; import { basename, joinPath, putFileContents } from '../webdav'; import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription } from './ui/dialog'; @@ -68,7 +68,7 @@ function typeLabel(tags: string): string { // ─── Component ──────────────────────────────────────────────────────────────── -export default function SearchCSDbNG({ config, setConfig, onClose: _onClose }: SearchCSDbNGProps) { +export default function SearchCSDbNG({ config, setConfig, onClose }: SearchCSDbNGProps) { const scrollRef = useRef(null); const [query, setQuery] = useState(() => _store.query); @@ -201,6 +201,16 @@ export default function SearchCSDbNG({ config, setConfig, onClose: _onClose }: S return ( <>
+ {/* Panel header */} +
+
+
+ +
{/* Header */}
@@ -211,7 +221,7 @@ export default function SearchCSDbNG({ config, setConfig, onClose: _onClose }: S value={query} onChange={e => setQuery(e.target.value)} onKeyDown={e => e.key === 'Enter' && !isSearching && doSearch(query)} - placeholder="Search CSDb…" + placeholder="Search CSDb-ng…" className="w-full pl-9 pr-3 py-2.5 bg-neutral-100 border-0 rounded-xl text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:bg-white transition-colors" disabled={isSearching} /> diff --git a/src/app/components/SearchCommoServe.tsx b/src/app/components/SearchCommoServe.tsx index 1f2eed1..ac9532a 100644 --- a/src/app/components/SearchCommoServe.tsx +++ b/src/app/components/SearchCommoServe.tsx @@ -1,5 +1,5 @@ import { useEffect, useMemo, useRef, useState } from 'react'; -import { Search, Loader2, HardDrive, Download, ChevronRight, ChevronDown, Trophy, Calendar, Users, RefreshCw, HelpCircle } from 'lucide-react'; +import { Search, Loader2, HardDrive, Download, ChevronRight, ChevronDown, Trophy, Calendar, Users, RefreshCw, HelpCircle, X } from 'lucide-react'; import { toast } from 'sonner'; import { humanFileSize, basename, joinPath, putFileContents } from '../webdav'; import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription } from './ui/dialog'; @@ -145,7 +145,7 @@ const AQL_TERMS = [ // ─── Component ──────────────────────────────────────────────────────────────── -export default function SearchCommoServe({ config, setConfig, onClose: _onClose }: SearchCommoServeProps) { +export default function SearchCommoServe({ config, setConfig, onClose }: SearchCommoServeProps) { const scrollRef = useRef(null); const inputRef = useRef(null); const [showAqlHelp, setShowAqlHelp] = useState(false); @@ -307,6 +307,16 @@ export default function SearchCommoServe({ config, setConfig, onClose: _onClose return ( <>
+ {/* Panel header */} +
+
+ + CommoServe +
+ +
{/* Header */}
diff --git a/src/app/components/SearchLocal.tsx b/src/app/components/SearchLocal.tsx index 594f51c..11c7a07 100644 --- a/src/app/components/SearchLocal.tsx +++ b/src/app/components/SearchLocal.tsx @@ -339,32 +339,23 @@ export default function SearchLocal({ config, setConfig, onClose, onOpenFolder } return ( <>
- {/* Header */} -
-
+ {/* Panel header */} +
+
+ + Local +
+
{dbPhase === 'ready' && !busy && ( - )} - {hasSearched && ( - )} +
+
- {/* Search input */} + {/* Search input */} +
@@ -439,7 +435,7 @@ export default function SearchLocal({ config, setConfig, onClose, onOpenFolder } {/* Body */}
{ _store.scrollTop = (e.currentTarget as HTMLDivElement).scrollTop; }} > {isScanning && scanPhase && ( @@ -482,7 +478,7 @@ export default function SearchLocal({ config, setConfig, onClose, onOpenFolder } selected={mountedPaths.has(result.path)} nameSlot={ <> -
+
{result.name} {result.system && {result.system}} diff --git a/src/app/components/SearchPane.tsx b/src/app/components/SearchPane.tsx index 79e2ac5..80a95dd 100644 --- a/src/app/components/SearchPane.tsx +++ b/src/app/components/SearchPane.tsx @@ -1,5 +1,5 @@ import { useEffect, useRef, useState } from 'react'; -import { X, Loader2, Database } from 'lucide-react'; +import { Loader2, Database } from 'lucide-react'; import { motion } from 'motion/react'; import SearchLocal from './SearchLocal'; import SearchAssembly64 from './SearchAssembly64'; @@ -21,15 +21,15 @@ interface SearchPaneProps { onOpenFolder: (path: string) => void; } -const TABS = ['Local', 'Assembly64', 'CommoServe', 'CSDb'] as const; - let _lastTab: 0 | 1 | 2 | 3 = (() => { const v = parseInt(localStorage.getItem('search.tab') ?? '0', 10); return (v >= 0 && v <= 3 ? v : 0) as 0 | 1 | 2 | 3; })(); +const PANEL_LABELS = ['Local', 'Assembly64', 'CommoServe', 'CSDb-ng'] as const; + export default function SearchPane({ config, setConfig, initialTab, onClose, onOpenFolder }: SearchPaneProps) { - const panelRef = useRef(null); + const panelRef = useRef(null); const [activeTab, setActiveTab] = useState<0 | 1 | 2 | 3>(initialTab ?? _lastTab); // Subscribe to the locate-database load pipeline so the user can see the @@ -61,9 +61,7 @@ export default function SearchPane({ config, setConfig, initialTab, onClose, onO }, []); // eslint-disable-line react-hooks/exhaustive-deps const scrollToTab = (idx: 0 | 1 | 2 | 3) => { - const el = panelRef.current; - if (!el) return; - el.scrollTo({ left: idx * el.clientWidth, behavior: 'smooth' }); + panelRef.current?.scrollTo({ left: idx * (panelRef.current.clientWidth), behavior: 'smooth' }); }; const handleScroll = () => { @@ -93,31 +91,6 @@ export default function SearchPane({ config, setConfig, initialTab, onClose, onO transition={{ type: 'spring', damping: 28, stiffness: 280 }} className="fixed inset-0 bg-white/80 backdrop-blur-md flex flex-col overflow-hidden" > - {/* Tab bar */} -
- {TABS.map((label, i) => ( - - ))} -
- -
- {/* Locate-database load bar. Visible only while a transfer is in flight; collapses to zero height otherwise so it doesn't take up space when there's nothing to show. */} @@ -200,6 +173,22 @@ export default function SearchPane({ config, setConfig, initialTab, onClose, onO />
+ + {/* Swipe indicator dots */} +
+ {([0, 1, 2, 3] as const).map(i => ( +
); diff --git a/src/vite-env.d.ts b/src/vite-env.d.ts new file mode 100644 index 0000000..11f02fe --- /dev/null +++ b/src/vite-env.d.ts @@ -0,0 +1 @@ +/// diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..7d473f0 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,15 @@ +{ + "compilerOptions": { + "target": "ES2020", + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "module": "ESNext", + "moduleResolution": "bundler", + "jsx": "react-jsx", + "skipLibCheck": true, + "noEmit": true, + "resolveJsonModule": true, + "isolatedModules": true, + "allowImportingTsExtensions": true + }, + "include": ["src"] +}