Compare commits
No commits in common. "c3af4406bfc68b332af051fede45e7367a2aecc3" and "48f75a1acca4da2938bf8fd1de328eca2e5d0254" have entirely different histories.
c3af4406bf
...
48f75a1acc
Binary file not shown.
|
Before Width: | Height: | Size: 2.0 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 701 B |
|
|
@ -1,6 +1,5 @@
|
||||||
import { lazy, Suspense, useEffect, useState } from 'react';
|
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 { 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 { Toaster, toast } from 'sonner';
|
||||||
import StatusPage from './components/StatusPage';
|
import StatusPage from './components/StatusPage';
|
||||||
import DevicesPage from './components/DevicesPage';
|
import DevicesPage from './components/DevicesPage';
|
||||||
|
|
@ -55,7 +54,8 @@ type AppId =
|
||||||
export default function App() {
|
export default function App() {
|
||||||
const [currentPage, setCurrentPage] = useState<Page>('status');
|
const [currentPage, setCurrentPage] = useState<Page>('status');
|
||||||
const { config, setConfig, saveStatus, pendingCount, flushNow, reload } = useSettings();
|
const { config, setConfig, saveStatus, pendingCount, flushNow, reload } = useSettings();
|
||||||
const [searchPanel, setSearchPanel] = useState<'local' | 'assembly64' | 'commoserve' | 'csdb' | 'last' | null>(null);
|
const [showSearch, setShowSearch] = useState(false);
|
||||||
|
const [searchInitialTab, setSearchInitialTab] = useState<0 | 1 | 2 | 3 | undefined>(undefined);
|
||||||
const [devicesOpenId, setDevicesOpenId] = useState<string | null>(null);
|
const [devicesOpenId, setDevicesOpenId] = useState<string | null>(null);
|
||||||
const [isFullscreen, setIsFullscreen] = useState(false);
|
const [isFullscreen, setIsFullscreen] = useState(false);
|
||||||
const [fileManagerInitialPath, setFileManagerInitialPath] = useState<string | undefined>(undefined);
|
const [fileManagerInitialPath, setFileManagerInitialPath] = useState<string | undefined>(undefined);
|
||||||
|
|
@ -118,9 +118,9 @@ export default function App() {
|
||||||
<h2 className="text-lg font-semibold mb-4 text-blue-700">Management</h2>
|
<h2 className="text-lg font-semibold mb-4 text-blue-700">Management</h2>
|
||||||
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-4">
|
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-4">
|
||||||
<AppCard icon={<Folder className="w-7 h-7" />} label="Media Manager" onClick={() => { setFileManagerInitialPath(undefined); setCurrentPage('file-manager'); }} />
|
<AppCard icon={<Folder className="w-7 h-7" />} label="Media Manager" onClick={() => { setFileManagerInitialPath(undefined); setCurrentPage('file-manager'); }} />
|
||||||
<AppCard icon={<Database className="w-7 h-7" />} label="Assembly64" onClick={() => setSearchPanel('assembly64')} />
|
<AppCard icon={<Database className="w-7 h-7" />} label="Assembly64" onClick={() => { setSearchInitialTab(1); setShowSearch(true); }} />
|
||||||
<AppCard icon={<Database className="w-7 h-7" />} label="CommoServe" onClick={() => setSearchPanel('commoserve')} />
|
<AppCard icon={<Database className="w-7 h-7" />} label="CommoServe" onClick={() => { setSearchInitialTab(2); setShowSearch(true); }} />
|
||||||
<AppCard icon={<Database className="w-7 h-7" />} label="CSDb" onClick={() => setSearchPanel('csdb')} />
|
<AppCard icon={<Database className="w-7 h-7" />} label="CSDb" onClick={() => { setSearchInitialTab(3); setShowSearch(true); }} />
|
||||||
<AppCard icon={<Printer className="w-7 h-7" />} label="Print Manager" onClick={() => setCurrentPage('print-manager')} />
|
<AppCard icon={<Printer className="w-7 h-7" />} label="Print Manager" onClick={() => setCurrentPage('print-manager')} />
|
||||||
<AppCard icon={<Terminal className="w-7 h-7" />} label="Serial Console" onClick={() => setCurrentPage('serial-console')} />
|
<AppCard icon={<Terminal className="w-7 h-7" />} label="Serial Console" onClick={() => setCurrentPage('serial-console')} />
|
||||||
<AppCard icon={<Link className="w-7 h-7" />} label="Short Codes" onClick={() => setCurrentPage('serial-console')} />
|
<AppCard icon={<Link className="w-7 h-7" />} label="Short Codes" onClick={() => setCurrentPage('serial-console')} />
|
||||||
|
|
@ -262,7 +262,7 @@ function AppPage({ title, onBack }: { title: string; onBack: () => void }) {
|
||||||
{isFullscreen ? <Minimize2 className="w-5 h-5 text-white" /> : <Maximize2 className="w-5 h-5 text-white" />}
|
{isFullscreen ? <Minimize2 className="w-5 h-5 text-white" /> : <Maximize2 className="w-5 h-5 text-white" />}
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={() => setSearchPanel('last')}
|
onClick={() => { setSearchInitialTab(undefined); setShowSearch(true); }}
|
||||||
className="p-2 hover:bg-[#5e5e5e] rounded-lg"
|
className="p-2 hover:bg-[#5e5e5e] rounded-lg"
|
||||||
>
|
>
|
||||||
<Search className="w-5 h-5 text-white" />
|
<Search className="w-5 h-5 text-white" />
|
||||||
|
|
@ -316,18 +316,20 @@ function AppPage({ title, onBack }: { title: string; onBack: () => void }) {
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
<AnimatePresence>
|
{showSearch && (
|
||||||
{searchPanel && (
|
<SearchPane
|
||||||
<SearchPane
|
config={config}
|
||||||
key="search"
|
setConfig={setConfig}
|
||||||
config={config}
|
initialTab={searchInitialTab}
|
||||||
setConfig={setConfig}
|
onClose={() => setShowSearch(false)}
|
||||||
initialTab={({ local: 0, assembly64: 1, commoserve: 2, csdb: 3, last: undefined } as const)[searchPanel]}
|
onOpenFolder={(path: string) => {
|
||||||
onClose={() => setSearchPanel(null)}
|
setFileManagerInitialPath(path);
|
||||||
onOpenFolder={(path: string) => { setSearchPanel(null); setFileManagerInitialPath(path); setFileManagerReturnPage('status'); setCurrentPage('file-manager'); }}
|
setFileManagerReturnPage('status');
|
||||||
/>
|
setShowSearch(false);
|
||||||
)}
|
setCurrentPage('file-manager');
|
||||||
</AnimatePresence>
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</WsProvider>
|
</WsProvider>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -455,23 +455,6 @@ export default function DeviceDetailOverlay({
|
||||||
))}
|
))}
|
||||||
</Swiper>
|
</Swiper>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* ── Swipe indicator dots ── */}
|
|
||||||
{devices.length > 1 && (
|
|
||||||
<div className="flex-shrink-0 flex items-center justify-center gap-2.5 py-2 border-t border-neutral-200/50">
|
|
||||||
{devices.map((_, i) => (
|
|
||||||
<button
|
|
||||||
key={i}
|
|
||||||
onClick={() => swiperRef.current?.slideTo(i)}
|
|
||||||
className={`rounded-full transition-all duration-200 ${
|
|
||||||
activeIndex === i
|
|
||||||
? 'w-4 h-2 bg-blue-500'
|
|
||||||
: 'w-2 h-2 bg-neutral-300 hover:bg-neutral-400'
|
|
||||||
}`}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</motion.div>
|
</motion.div>
|
||||||
</div>
|
</div>
|
||||||
</AnimatePresence>
|
</AnimatePresence>
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { useEffect, useMemo, useRef, useState } from 'react';
|
import { useEffect, useMemo, useRef, useState } from 'react';
|
||||||
import { Search, Loader2, HardDrive, Download, ChevronRight, ChevronDown, Trophy, Calendar, Users, RefreshCw, HelpCircle, X } from 'lucide-react';
|
import { Search, Loader2, HardDrive, Download, ChevronRight, ChevronDown, Trophy, Calendar, Users, RefreshCw, HelpCircle } from 'lucide-react';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
import { humanFileSize, basename, joinPath, putFileContents } from '../webdav';
|
import { humanFileSize, basename, joinPath, putFileContents } from '../webdav';
|
||||||
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription } from './ui/dialog';
|
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription } from './ui/dialog';
|
||||||
|
|
@ -153,7 +153,7 @@ const AQL_TERMS = [
|
||||||
|
|
||||||
// ─── Component ────────────────────────────────────────────────────────────────
|
// ─── Component ────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
export default function SearchAssembly64({ config, setConfig, onClose }: SearchAssembly64Props) {
|
export default function SearchAssembly64({ config, setConfig, onClose: _onClose }: SearchAssembly64Props) {
|
||||||
const scrollRef = useRef<HTMLDivElement>(null);
|
const scrollRef = useRef<HTMLDivElement>(null);
|
||||||
const inputRef = useRef<HTMLInputElement>(null);
|
const inputRef = useRef<HTMLInputElement>(null);
|
||||||
const [showAqlHelp, setShowAqlHelp] = useState(false);
|
const [showAqlHelp, setShowAqlHelp] = useState(false);
|
||||||
|
|
@ -322,16 +322,6 @@ export default function SearchAssembly64({ config, setConfig, onClose }: SearchA
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="flex flex-col h-full overflow-hidden">
|
<div className="flex flex-col h-full overflow-hidden">
|
||||||
{/* Panel header */}
|
|
||||||
<div className="flex-shrink-0 flex items-center border-b border-neutral-200/70 px-4">
|
|
||||||
<div className="flex items-center gap-2 flex-1 py-3 min-w-0">
|
|
||||||
<img src={`${import.meta.env.BASE_URL}assets/favicon.a64.png`} className="w-5 h-5 flex-shrink-0 object-contain" alt="" aria-hidden="true" />
|
|
||||||
<span className="text-sm font-semibold text-neutral-700">Assembly64</span>
|
|
||||||
</div>
|
|
||||||
<button onClick={onClose} className="p-1.5 my-1.5 rounded-lg hover:bg-neutral-100 text-neutral-500 transition-colors" aria-label="Close search">
|
|
||||||
<X className="w-5 h-5" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div className="flex-shrink-0 px-4 pt-3 pb-3 border-b border-neutral-200/70">
|
<div className="flex-shrink-0 px-4 pt-3 pb-3 border-b border-neutral-200/70">
|
||||||
{/* Search input */}
|
{/* Search input */}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { useEffect, useMemo, useRef, useState } from 'react';
|
import { useEffect, useMemo, useRef, useState } from 'react';
|
||||||
import { Search, Loader2, HardDrive, Download, ChevronRight, X, Archive } from 'lucide-react';
|
import { Search, Loader2, HardDrive, Download, ChevronRight } from 'lucide-react';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
import { basename, joinPath, putFileContents } from '../webdav';
|
import { basename, joinPath, putFileContents } from '../webdav';
|
||||||
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription } from './ui/dialog';
|
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription } from './ui/dialog';
|
||||||
|
|
@ -68,7 +68,7 @@ function typeLabel(tags: string): string {
|
||||||
|
|
||||||
// ─── Component ────────────────────────────────────────────────────────────────
|
// ─── Component ────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
export default function SearchCSDbNG({ config, setConfig, onClose }: SearchCSDbNGProps) {
|
export default function SearchCSDbNG({ config, setConfig, onClose: _onClose }: SearchCSDbNGProps) {
|
||||||
const scrollRef = useRef<HTMLDivElement>(null);
|
const scrollRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
const [query, setQuery] = useState(() => _store.query);
|
const [query, setQuery] = useState(() => _store.query);
|
||||||
|
|
@ -201,16 +201,6 @@ export default function SearchCSDbNG({ config, setConfig, onClose }: SearchCSDbN
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="flex flex-col h-full overflow-hidden">
|
<div className="flex flex-col h-full overflow-hidden">
|
||||||
{/* Panel header */}
|
|
||||||
<div className="flex-shrink-0 flex items-center border-b border-neutral-200/70 px-4">
|
|
||||||
<div className="flex items-center gap-2 flex-1 py-3 min-w-0">
|
|
||||||
<Archive className="w-5 h-5 flex-shrink-0 text-neutral-500" aria-hidden="true" />
|
|
||||||
<span className="text-sm font-semibold text-neutral-700">CSDb-ng</span>
|
|
||||||
</div>
|
|
||||||
<button onClick={onClose} className="p-1.5 my-1.5 rounded-lg hover:bg-neutral-100 text-neutral-500 transition-colors" aria-label="Close search">
|
|
||||||
<X className="w-5 h-5" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div className="flex-shrink-0 px-4 pt-3 pb-3 border-b border-neutral-200/70">
|
<div className="flex-shrink-0 px-4 pt-3 pb-3 border-b border-neutral-200/70">
|
||||||
<div className="flex gap-2 mb-2">
|
<div className="flex gap-2 mb-2">
|
||||||
|
|
@ -221,7 +211,7 @@ export default function SearchCSDbNG({ config, setConfig, onClose }: SearchCSDbN
|
||||||
value={query}
|
value={query}
|
||||||
onChange={e => setQuery(e.target.value)}
|
onChange={e => setQuery(e.target.value)}
|
||||||
onKeyDown={e => e.key === 'Enter' && !isSearching && doSearch(query)}
|
onKeyDown={e => e.key === 'Enter' && !isSearching && doSearch(query)}
|
||||||
placeholder="Search CSDb-ng…"
|
placeholder="Search CSDb…"
|
||||||
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"
|
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}
|
disabled={isSearching}
|
||||||
/>
|
/>
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { useEffect, useMemo, useRef, useState } from 'react';
|
import { useEffect, useMemo, useRef, useState } from 'react';
|
||||||
import { Search, Loader2, HardDrive, Download, ChevronRight, ChevronDown, Trophy, Calendar, Users, RefreshCw, HelpCircle, X } from 'lucide-react';
|
import { Search, Loader2, HardDrive, Download, ChevronRight, ChevronDown, Trophy, Calendar, Users, RefreshCw, HelpCircle } from 'lucide-react';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
import { humanFileSize, basename, joinPath, putFileContents } from '../webdav';
|
import { humanFileSize, basename, joinPath, putFileContents } from '../webdav';
|
||||||
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription } from './ui/dialog';
|
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription } from './ui/dialog';
|
||||||
|
|
@ -145,7 +145,7 @@ const AQL_TERMS = [
|
||||||
|
|
||||||
// ─── Component ────────────────────────────────────────────────────────────────
|
// ─── Component ────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
export default function SearchCommoServe({ config, setConfig, onClose }: SearchCommoServeProps) {
|
export default function SearchCommoServe({ config, setConfig, onClose: _onClose }: SearchCommoServeProps) {
|
||||||
const scrollRef = useRef<HTMLDivElement>(null);
|
const scrollRef = useRef<HTMLDivElement>(null);
|
||||||
const inputRef = useRef<HTMLInputElement>(null);
|
const inputRef = useRef<HTMLInputElement>(null);
|
||||||
const [showAqlHelp, setShowAqlHelp] = useState(false);
|
const [showAqlHelp, setShowAqlHelp] = useState(false);
|
||||||
|
|
@ -307,16 +307,6 @@ export default function SearchCommoServe({ config, setConfig, onClose }: SearchC
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="flex flex-col h-full overflow-hidden">
|
<div className="flex flex-col h-full overflow-hidden">
|
||||||
{/* Panel header */}
|
|
||||||
<div className="flex-shrink-0 flex items-center border-b border-neutral-200/70 px-4">
|
|
||||||
<div className="flex items-center gap-2 flex-1 py-3 min-w-0">
|
|
||||||
<img src={`${import.meta.env.BASE_URL}assets/favicon.cbm.png`} className="w-5 h-5 flex-shrink-0 object-contain" alt="" aria-hidden="true" />
|
|
||||||
<span className="text-sm font-semibold text-neutral-700">CommoServe</span>
|
|
||||||
</div>
|
|
||||||
<button onClick={onClose} className="p-1.5 my-1.5 rounded-lg hover:bg-neutral-100 text-neutral-500 transition-colors" aria-label="Close search">
|
|
||||||
<X className="w-5 h-5" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div className="flex-shrink-0 px-4 pt-3 pb-3 border-b border-neutral-200/70">
|
<div className="flex-shrink-0 px-4 pt-3 pb-3 border-b border-neutral-200/70">
|
||||||
<div className="flex gap-2 mb-2">
|
<div className="flex gap-2 mb-2">
|
||||||
|
|
|
||||||
|
|
@ -339,23 +339,32 @@ export default function SearchLocal({ config, setConfig, onClose, onOpenFolder }
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="flex flex-col h-full overflow-hidden">
|
<div className="flex flex-col h-full overflow-hidden">
|
||||||
{/* Panel header */}
|
{/* Header */}
|
||||||
<div className="flex-shrink-0 flex items-center border-b border-neutral-200/70 px-4">
|
<div className="flex-shrink-0 px-4 pt-3 pb-3 border-b border-neutral-100">
|
||||||
<div className="flex items-center gap-2 flex-1 py-3 min-w-0">
|
<div className="flex items-center justify-end mb-3 gap-1">
|
||||||
<img src={`${import.meta.env.BASE_URL}favicon.ico`} className="w-5 h-5 flex-shrink-0 object-contain" alt="" aria-hidden="true" />
|
|
||||||
<span className="text-sm font-semibold text-neutral-700">Local</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center gap-0.5">
|
|
||||||
{dbPhase === 'ready' && !busy && (
|
{dbPhase === 'ready' && !busy && (
|
||||||
<button onClick={handleRefreshDb} className="p-1.5 rounded-lg hover:bg-neutral-100 text-neutral-400 hover:text-neutral-600 transition-colors" title="Reload database">
|
<button
|
||||||
|
onClick={handleRefreshDb}
|
||||||
|
className="p-1.5 rounded-lg hover:bg-neutral-100 text-neutral-400 hover:text-neutral-600 transition-colors"
|
||||||
|
title="Reload database"
|
||||||
|
>
|
||||||
<RefreshCw className="w-4 h-4" />
|
<RefreshCw className="w-4 h-4" />
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
<button onClick={() => setShowScanConfirm(true)} disabled={busy} className="p-1.5 rounded-lg hover:bg-neutral-100 text-neutral-400 hover:text-neutral-600 disabled:opacity-40 transition-colors" title="Scan /sd and rebuild database">
|
<button
|
||||||
|
onClick={() => setShowScanConfirm(true)}
|
||||||
|
disabled={busy}
|
||||||
|
className="p-1.5 rounded-lg hover:bg-neutral-100 text-neutral-400 hover:text-neutral-600 disabled:opacity-40 transition-colors"
|
||||||
|
title="Scan /sd and rebuild database"
|
||||||
|
>
|
||||||
<FolderSearch className="w-4 h-4" />
|
<FolderSearch className="w-4 h-4" />
|
||||||
</button>
|
</button>
|
||||||
{hasSearched && (
|
{hasSearched && (
|
||||||
<button onClick={() => setShowFilter(v => !v)} className={`relative p-1.5 rounded-lg transition-colors ${showFilter ? 'bg-blue-100 text-blue-600' : 'hover:bg-neutral-100 text-neutral-400 hover:text-neutral-600'}`} title="Filter & sort">
|
<button
|
||||||
|
onClick={() => setShowFilter(v => !v)}
|
||||||
|
className={`relative p-1.5 rounded-lg transition-colors ${showFilter ? 'bg-blue-100 text-blue-600' : 'hover:bg-neutral-100 text-neutral-400 hover:text-neutral-600'}`}
|
||||||
|
title="Filter & sort"
|
||||||
|
>
|
||||||
<SlidersHorizontal className="w-4 h-4" />
|
<SlidersHorizontal className="w-4 h-4" />
|
||||||
{activeFilters > 0 && (
|
{activeFilters > 0 && (
|
||||||
<span className="absolute -top-0.5 -right-0.5 w-3.5 h-3.5 rounded-full bg-blue-600 text-white text-[9px] flex items-center justify-center font-bold">
|
<span className="absolute -top-0.5 -right-0.5 w-3.5 h-3.5 rounded-full bg-blue-600 text-white text-[9px] flex items-center justify-center font-bold">
|
||||||
|
|
@ -364,14 +373,9 @@ export default function SearchLocal({ config, setConfig, onClose, onOpenFolder }
|
||||||
)}
|
)}
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
<button onClick={onClose} className="p-1.5 ml-1 rounded-lg hover:bg-neutral-100 text-neutral-500 transition-colors" aria-label="Close search">
|
|
||||||
<X className="w-5 h-5" />
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Search input */}
|
{/* Search input */}
|
||||||
<div className="flex-shrink-0 px-4 pt-3 pb-3 border-b border-neutral-100">
|
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
<div className="relative flex-1">
|
<div className="relative flex-1">
|
||||||
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-neutral-400 pointer-events-none" />
|
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-neutral-400 pointer-events-none" />
|
||||||
|
|
@ -435,7 +439,7 @@ export default function SearchLocal({ config, setConfig, onClose, onOpenFolder }
|
||||||
{/* Body */}
|
{/* Body */}
|
||||||
<div
|
<div
|
||||||
ref={scrollRef}
|
ref={scrollRef}
|
||||||
className="flex-1 overflow-y-auto overflow-x-hidden"
|
className="flex-1 overflow-y-auto"
|
||||||
onScroll={e => { _store.scrollTop = (e.currentTarget as HTMLDivElement).scrollTop; }}
|
onScroll={e => { _store.scrollTop = (e.currentTarget as HTMLDivElement).scrollTop; }}
|
||||||
>
|
>
|
||||||
{isScanning && scanPhase && (
|
{isScanning && scanPhase && (
|
||||||
|
|
@ -478,7 +482,7 @@ export default function SearchLocal({ config, setConfig, onClose, onOpenFolder }
|
||||||
selected={mountedPaths.has(result.path)}
|
selected={mountedPaths.has(result.path)}
|
||||||
nameSlot={
|
nameSlot={
|
||||||
<>
|
<>
|
||||||
<div className="flex items-center gap-1.5 flex-wrap min-w-0">
|
<div className="flex items-center gap-1.5 flex-wrap">
|
||||||
<span className="text-neutral-900 text-sm">{result.name}</span>
|
<span className="text-neutral-900 text-sm">{result.name}</span>
|
||||||
<TypeBadge type={result.type} isDir={result.isDir} />
|
<TypeBadge type={result.type} isDir={result.isDir} />
|
||||||
{result.system && <span className="text-xs px-1.5 py-0.5 rounded bg-indigo-100 text-indigo-700 font-mono">{result.system}</span>}
|
{result.system && <span className="text-xs px-1.5 py-0.5 rounded bg-indigo-100 text-indigo-700 font-mono">{result.system}</span>}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { useEffect, useRef, useState } from 'react';
|
import { useEffect, useRef, useState } from 'react';
|
||||||
import { Loader2, Database } from 'lucide-react';
|
import { X, Loader2, Database } from 'lucide-react';
|
||||||
import { motion } from 'motion/react';
|
import { motion } from 'motion/react';
|
||||||
import SearchLocal from './SearchLocal';
|
import SearchLocal from './SearchLocal';
|
||||||
import SearchAssembly64 from './SearchAssembly64';
|
import SearchAssembly64 from './SearchAssembly64';
|
||||||
|
|
@ -21,15 +21,15 @@ interface SearchPaneProps {
|
||||||
onOpenFolder: (path: string) => void;
|
onOpenFolder: (path: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const TABS = ['Local', 'Assembly64', 'CommoServe', 'CSDb'] as const;
|
||||||
|
|
||||||
let _lastTab: 0 | 1 | 2 | 3 = (() => {
|
let _lastTab: 0 | 1 | 2 | 3 = (() => {
|
||||||
const v = parseInt(localStorage.getItem('search.tab') ?? '0', 10);
|
const v = parseInt(localStorage.getItem('search.tab') ?? '0', 10);
|
||||||
return (v >= 0 && v <= 3 ? v : 0) as 0 | 1 | 2 | 3;
|
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) {
|
export default function SearchPane({ config, setConfig, initialTab, onClose, onOpenFolder }: SearchPaneProps) {
|
||||||
const panelRef = useRef<HTMLDivElement>(null);
|
const panelRef = useRef<HTMLDivElement>(null);
|
||||||
const [activeTab, setActiveTab] = useState<0 | 1 | 2 | 3>(initialTab ?? _lastTab);
|
const [activeTab, setActiveTab] = useState<0 | 1 | 2 | 3>(initialTab ?? _lastTab);
|
||||||
|
|
||||||
// Subscribe to the locate-database load pipeline so the user can see the
|
// Subscribe to the locate-database load pipeline so the user can see the
|
||||||
|
|
@ -61,7 +61,9 @@ export default function SearchPane({ config, setConfig, initialTab, onClose, onO
|
||||||
}, []); // eslint-disable-line react-hooks/exhaustive-deps
|
}, []); // eslint-disable-line react-hooks/exhaustive-deps
|
||||||
|
|
||||||
const scrollToTab = (idx: 0 | 1 | 2 | 3) => {
|
const scrollToTab = (idx: 0 | 1 | 2 | 3) => {
|
||||||
panelRef.current?.scrollTo({ left: idx * (panelRef.current.clientWidth), behavior: 'smooth' });
|
const el = panelRef.current;
|
||||||
|
if (!el) return;
|
||||||
|
el.scrollTo({ left: idx * el.clientWidth, behavior: 'smooth' });
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleScroll = () => {
|
const handleScroll = () => {
|
||||||
|
|
@ -91,6 +93,31 @@ export default function SearchPane({ config, setConfig, initialTab, onClose, onO
|
||||||
transition={{ type: 'spring', damping: 28, stiffness: 280 }}
|
transition={{ type: 'spring', damping: 28, stiffness: 280 }}
|
||||||
className="fixed inset-0 bg-white/80 backdrop-blur-md flex flex-col overflow-hidden"
|
className="fixed inset-0 bg-white/80 backdrop-blur-md flex flex-col overflow-hidden"
|
||||||
>
|
>
|
||||||
|
{/* Tab bar */}
|
||||||
|
<div className="flex-shrink-0 flex items-center border-b border-neutral-200/70 px-1">
|
||||||
|
{TABS.map((label, i) => (
|
||||||
|
<button
|
||||||
|
key={label}
|
||||||
|
onClick={() => scrollToTab(i as 0 | 1 | 2 | 3)}
|
||||||
|
className={`px-4 py-3 text-sm font-medium transition-colors border-b-2 -mb-px ${
|
||||||
|
activeTab === i
|
||||||
|
? 'text-blue-600 border-blue-600'
|
||||||
|
: 'text-neutral-500 border-transparent hover:text-neutral-700'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{label}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
<div className="flex-1" />
|
||||||
|
<button
|
||||||
|
onClick={onClose}
|
||||||
|
className="p-1.5 mr-2 rounded-lg hover:bg-neutral-100 text-neutral-500 transition-colors"
|
||||||
|
aria-label="Close search"
|
||||||
|
>
|
||||||
|
<X className="w-5 h-5" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Locate-database load bar. Visible only while a transfer is in
|
{/* Locate-database load bar. Visible only while a transfer is in
|
||||||
flight; collapses to zero height otherwise so it doesn't take
|
flight; collapses to zero height otherwise so it doesn't take
|
||||||
up space when there's nothing to show. */}
|
up space when there's nothing to show. */}
|
||||||
|
|
@ -173,22 +200,6 @@ export default function SearchPane({ config, setConfig, initialTab, onClose, onO
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Swipe indicator dots */}
|
|
||||||
<div className="flex-shrink-0 flex items-center justify-center gap-2.5 py-2 border-t border-neutral-200/50">
|
|
||||||
{([0, 1, 2, 3] as const).map(i => (
|
|
||||||
<button
|
|
||||||
key={i}
|
|
||||||
onClick={() => scrollToTab(i)}
|
|
||||||
aria-label={PANEL_LABELS[i]}
|
|
||||||
className={`rounded-full transition-all duration-200 ${
|
|
||||||
activeTab === i
|
|
||||||
? 'w-4 h-2 bg-blue-500'
|
|
||||||
: 'w-2 h-2 bg-neutral-300 hover:bg-neutral-400'
|
|
||||||
}`}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</motion.div>
|
</motion.div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
1
src/vite-env.d.ts
vendored
1
src/vite-env.d.ts
vendored
|
|
@ -1 +0,0 @@
|
||||||
/// <reference types="vite/client" />
|
|
||||||
|
|
@ -1,15 +0,0 @@
|
||||||
{
|
|
||||||
"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"]
|
|
||||||
}
|
|
||||||
Loading…
Reference in New Issue
Block a user