feat(SearchComponents): enhance search UI with loading states and clear button functionality
This commit is contained in:
parent
f280ad2ee9
commit
c7ad02dbe4
|
|
@ -365,7 +365,10 @@ export default function SearchAssembly64({ config, setConfig, onClose }: SearchA
|
||||||
<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 items-center gap-1.5 mb-2">
|
<div className="flex items-center gap-1.5 mb-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" />
|
{isSearching
|
||||||
|
? <Loader2 className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-blue-500 animate-spin pointer-events-none" />
|
||||||
|
: <button onClick={handleSearch} className="absolute left-3 top-1/2 -translate-y-1/2 text-neutral-400 hover:text-blue-500 transition-colors" tabIndex={-1} title="Search"><Search className="w-4 h-4" /></button>
|
||||||
|
}
|
||||||
<input
|
<input
|
||||||
ref={inputRef}
|
ref={inputRef}
|
||||||
type="text"
|
type="text"
|
||||||
|
|
@ -378,25 +381,23 @@ export default function SearchAssembly64({ config, setConfig, onClose }: SearchA
|
||||||
className="w-full pl-9 pr-9 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-9 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}
|
||||||
/>
|
/>
|
||||||
<button
|
{query && (
|
||||||
onClick={() => setShowAqlHelp(true)}
|
<button
|
||||||
className="absolute right-2.5 top-1/2 -translate-y-1/2 text-neutral-400 hover:text-neutral-600 transition-colors"
|
onClick={() => { setQuery(''); setResults([]); setHasSearched(false); setSearchError(null); setOffset(0); setHasMore(false); setCategoryFilter(null); }}
|
||||||
title="AQL search help"
|
className="absolute right-2.5 top-1/2 -translate-y-1/2 text-neutral-400 hover:text-neutral-600 transition-colors"
|
||||||
tabIndex={-1}
|
title="Clear"
|
||||||
>
|
tabIndex={-1}
|
||||||
<HelpCircle className="w-4 h-4" />
|
>
|
||||||
</button>
|
<X className="w-4 h-4" />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
onClick={handleSearch}
|
onClick={() => setShowAqlHelp(true)}
|
||||||
disabled={isSearching}
|
className="p-1.5 rounded-lg hover:bg-neutral-100 transition-colors flex-shrink-0 text-neutral-400 hover:text-neutral-600"
|
||||||
className="p-1.5 rounded-lg hover:bg-neutral-100 disabled:opacity-40 transition-colors flex-shrink-0 text-neutral-400 hover:text-blue-600"
|
title="AQL search help"
|
||||||
title="Search"
|
|
||||||
aria-label="Search"
|
|
||||||
>
|
>
|
||||||
{isSearching
|
<HelpCircle className="w-4 h-4" />
|
||||||
? <Loader2 className="w-5 h-5 animate-spin text-blue-500" />
|
|
||||||
: <Search className="w-5 h-5" />}
|
|
||||||
</button>
|
</button>
|
||||||
{hasSearched && (
|
{hasSearched && (
|
||||||
<button
|
<button
|
||||||
|
|
|
||||||
|
|
@ -238,7 +238,10 @@ export default function SearchCSDbNG({ config, setConfig, onClose }: SearchCSDbN
|
||||||
<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 items-center gap-1.5 mb-2">
|
<div className="flex items-center gap-1.5 mb-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" />
|
{isSearching
|
||||||
|
? <Loader2 className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-blue-500 animate-spin pointer-events-none" />
|
||||||
|
: <button onClick={() => doSearch(query)} className="absolute left-3 top-1/2 -translate-y-1/2 text-neutral-400 hover:text-blue-500 transition-colors" tabIndex={-1} title="Search"><Search className="w-4 h-4" /></button>
|
||||||
|
}
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
value={query}
|
value={query}
|
||||||
|
|
@ -247,21 +250,20 @@ export default function SearchCSDbNG({ config, setConfig, onClose }: SearchCSDbN
|
||||||
placeholder="Search CSDb-ng…"
|
placeholder="Search CSDb-ng…"
|
||||||
inputMode="search"
|
inputMode="search"
|
||||||
enterKeyHint="search"
|
enterKeyHint="search"
|
||||||
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-9 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}
|
||||||
/>
|
/>
|
||||||
|
{query && (
|
||||||
|
<button
|
||||||
|
onClick={() => { setQuery(''); setResults([]); setHasSearched(false); setSearchError(null); setTagFilter(null); }}
|
||||||
|
className="absolute right-2.5 top-1/2 -translate-y-1/2 text-neutral-400 hover:text-neutral-600 transition-colors"
|
||||||
|
title="Clear"
|
||||||
|
tabIndex={-1}
|
||||||
|
>
|
||||||
|
<X className="w-4 h-4" />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<button
|
|
||||||
onClick={() => doSearch(query)}
|
|
||||||
disabled={isSearching || !query.trim()}
|
|
||||||
className="p-1.5 rounded-lg hover:bg-neutral-100 disabled:opacity-40 transition-colors flex-shrink-0 text-neutral-400 hover:text-blue-600"
|
|
||||||
title="Search"
|
|
||||||
aria-label="Search"
|
|
||||||
>
|
|
||||||
{isSearching
|
|
||||||
? <Loader2 className="w-5 h-5 animate-spin text-blue-500" />
|
|
||||||
: <Search className="w-5 h-5" />}
|
|
||||||
</button>
|
|
||||||
{hasSearched && (
|
{hasSearched && (
|
||||||
<button
|
<button
|
||||||
onClick={() => setShowFilter(v => !v)}
|
onClick={() => setShowFilter(v => !v)}
|
||||||
|
|
|
||||||
|
|
@ -367,7 +367,10 @@ export default function SearchCommoServe({ config, setConfig, onClose }: SearchC
|
||||||
<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 items-center gap-1.5 mb-2">
|
<div className="flex items-center gap-1.5 mb-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" />
|
{isSearching
|
||||||
|
? <Loader2 className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-blue-500 animate-spin pointer-events-none" />
|
||||||
|
: <button onClick={handleSearch} className="absolute left-3 top-1/2 -translate-y-1/2 text-neutral-400 hover:text-blue-500 transition-colors" tabIndex={-1} title="Search"><Search className="w-4 h-4" /></button>
|
||||||
|
}
|
||||||
<input
|
<input
|
||||||
ref={inputRef}
|
ref={inputRef}
|
||||||
type="text"
|
type="text"
|
||||||
|
|
@ -380,25 +383,23 @@ export default function SearchCommoServe({ config, setConfig, onClose }: SearchC
|
||||||
className="w-full pl-9 pr-9 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-9 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}
|
||||||
/>
|
/>
|
||||||
<button
|
{query && (
|
||||||
onClick={() => setShowAqlHelp(true)}
|
<button
|
||||||
className="absolute right-2.5 top-1/2 -translate-y-1/2 text-neutral-400 hover:text-neutral-600 transition-colors"
|
onClick={() => { setQuery(''); setResults([]); setHasSearched(false); setSearchError(null); setOffset(0); setHasMore(false); }}
|
||||||
title="AQL search help"
|
className="absolute right-2.5 top-1/2 -translate-y-1/2 text-neutral-400 hover:text-neutral-600 transition-colors"
|
||||||
tabIndex={-1}
|
title="Clear"
|
||||||
>
|
tabIndex={-1}
|
||||||
<HelpCircle className="w-4 h-4" />
|
>
|
||||||
</button>
|
<X className="w-4 h-4" />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
onClick={handleSearch}
|
onClick={() => setShowAqlHelp(true)}
|
||||||
disabled={isSearching}
|
className="p-1.5 rounded-lg hover:bg-neutral-100 transition-colors flex-shrink-0 text-neutral-400 hover:text-neutral-600"
|
||||||
className="p-1.5 rounded-lg hover:bg-neutral-100 disabled:opacity-40 transition-colors flex-shrink-0 text-neutral-400 hover:text-blue-600"
|
title="AQL search help"
|
||||||
title="Search"
|
|
||||||
aria-label="Search"
|
|
||||||
>
|
>
|
||||||
{isSearching
|
<HelpCircle className="w-4 h-4" />
|
||||||
? <Loader2 className="w-5 h-5 animate-spin text-blue-500" />
|
|
||||||
: <Search className="w-5 h-5" />}
|
|
||||||
</button>
|
</button>
|
||||||
{hasSearched && (
|
{hasSearched && (
|
||||||
<button
|
<button
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { useEffect, useMemo, useRef, useState } from 'react';
|
import { useEffect, useMemo, useRef, useState } from 'react';
|
||||||
import { flushSync } from 'react-dom';
|
import { flushSync } from 'react-dom';
|
||||||
import { Search, Loader2, RefreshCw, FolderSearch, SlidersHorizontal, HardDrive, FolderOpen, X } from 'lucide-react';
|
import { Search, Loader2, RefreshCw, FolderSearch, SlidersHorizontal, HardDrive, FolderOpen, X, DatabaseZap } from 'lucide-react';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
import { humanFileSize, splitPath } from '../webdav';
|
import { humanFileSize, splitPath } from '../webdav';
|
||||||
import type { EntryInfo } from '../webdav';
|
import type { EntryInfo } from '../webdav';
|
||||||
|
|
@ -346,7 +346,7 @@ export default function SearchLocal({ config, setConfig, onClose, onOpenFolder }
|
||||||
</button>
|
</button>
|
||||||
<div className="flex items-center gap-2 flex-1 py-3 min-w-0">
|
<div className="flex items-center gap-2 flex-1 py-3 min-w-0">
|
||||||
<img src={`${import.meta.env.BASE_URL}favicon.ico`} className="w-5 h-5 flex-shrink-0 object-contain" alt="" aria-hidden="true" />
|
<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>
|
<span className="text-sm font-semibold text-neutral-700">Local Storage</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -354,36 +354,38 @@ export default function SearchLocal({ config, setConfig, onClose, onOpenFolder }
|
||||||
<div className="flex-shrink-0 px-4 pt-3 pb-3 border-b border-neutral-100">
|
<div className="flex-shrink-0 px-4 pt-3 pb-3 border-b border-neutral-100">
|
||||||
<div className="flex items-center gap-1.5">
|
<div className="flex items-center gap-1.5">
|
||||||
<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" />
|
{busy
|
||||||
|
? <Loader2 className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-blue-500 animate-spin pointer-events-none" />
|
||||||
|
: <button onClick={handleSearch} className="absolute left-3 top-1/2 -translate-y-1/2 text-neutral-400 hover:text-blue-500 transition-colors" tabIndex={-1} title="Search"><Search className="w-4 h-4" /></button>
|
||||||
|
}
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
value={query}
|
value={query}
|
||||||
onChange={e => setQuery(e.target.value)}
|
onChange={e => setQuery(e.target.value)}
|
||||||
onKeyDown={e => e.key === 'Enter' && !busy && handleSearch()}
|
onKeyDown={e => e.key === 'Enter' && !busy && handleSearch()}
|
||||||
placeholder="Search… (* any chars, ? one char)"
|
placeholder="Search… (* any chars, ? one char)"
|
||||||
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-9 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"
|
||||||
autoFocus
|
autoFocus
|
||||||
disabled={busy}
|
disabled={busy}
|
||||||
/>
|
/>
|
||||||
|
{query && (
|
||||||
|
<button
|
||||||
|
onClick={() => { setQuery(''); setResults([]); setHasSearched(false); setSearchError(null); }}
|
||||||
|
className="absolute right-2.5 top-1/2 -translate-y-1/2 text-neutral-400 hover:text-neutral-600 transition-colors"
|
||||||
|
title="Clear"
|
||||||
|
tabIndex={-1}
|
||||||
|
>
|
||||||
|
<X className="w-4 h-4" />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<button
|
{/* {dbPhase === 'ready' && !busy && (
|
||||||
onClick={handleSearch}
|
|
||||||
disabled={busy || !query.trim()}
|
|
||||||
className="p-1.5 rounded-lg hover:bg-neutral-100 disabled:opacity-40 transition-colors flex-shrink-0 text-neutral-400 hover:text-blue-600"
|
|
||||||
title="Search"
|
|
||||||
aria-label="Search"
|
|
||||||
>
|
|
||||||
{busy
|
|
||||||
? <Loader2 className="w-5 h-5 animate-spin text-blue-500" />
|
|
||||||
: <Search className="w-5 h-5" />}
|
|
||||||
</button>
|
|
||||||
{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 flex-shrink-0" 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 flex-shrink-0" 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 flex-shrink-0" 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 flex-shrink-0" title="Scan /sd and rebuild database">
|
||||||
<FolderSearch className="w-4 h-4" />
|
<DatabaseZap className="w-4 h-4" />
|
||||||
</button>
|
</button>
|
||||||
{hasSearched && (
|
{hasSearched && (
|
||||||
<button onClick={() => setShowFilter(v => !v)} className={`relative p-1.5 rounded-lg transition-colors flex-shrink-0 ${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 flex-shrink-0 ${showFilter ? 'bg-blue-100 text-blue-600' : 'hover:bg-neutral-100 text-neutral-400 hover:text-neutral-600'}`} title="Filter & sort">
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user