mirror of
https://github.com/0x5t4l1n/AURHub.git
synced 2026-05-27 19:56:30 +00:00
Not good ui
This commit is contained in:
+423
-30
@@ -2,7 +2,11 @@ import { useState, useEffect } from 'react';
|
||||
import { useSearchParams } from 'react-router-dom';
|
||||
import api from '../api/client';
|
||||
import PackageGrid from '../components/PackageGrid';
|
||||
import { Filter, Search } from 'lucide-react';
|
||||
import LoadingSpinner from '../components/LoadingSpinner';
|
||||
import {
|
||||
Filter, Search, Grid, List, CheckCircle, AlertTriangle, Star,
|
||||
Download, Globe, ExternalLink, Trash2, Shield, Package
|
||||
} from 'lucide-react';
|
||||
|
||||
export default function SearchPage() {
|
||||
const [searchParams] = useSearchParams();
|
||||
@@ -11,78 +15,467 @@ export default function SearchPage() {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [source, setSource] = useState('all');
|
||||
const [error, setError] = useState(null);
|
||||
const [layoutMode, setLayoutMode] = useState('grid');
|
||||
const [sortBy, setSortBy] = useState('relevance');
|
||||
const [filters, setFilters] = useState({ installed: false, outOfDate: false, hasVotes: false });
|
||||
|
||||
// Selected package details for right preview panel
|
||||
const [selectedPkgName, setSelectedPkgName] = useState(null);
|
||||
const [pkgDetail, setPkgDetail] = useState(null);
|
||||
const [detailLoading, setDetailLoading] = useState(false);
|
||||
const [scanResult, setScanResult] = useState(null);
|
||||
const [scanning, setScanning] = useState(false);
|
||||
const [actionLog, setActionLog] = useState('');
|
||||
const [acting, setActing] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (query.trim()) performSearch(query, source);
|
||||
else setResults([]);
|
||||
if (query.trim()) {
|
||||
performSearch(query, source);
|
||||
} else {
|
||||
setResults([]);
|
||||
setSelectedPkgName(null);
|
||||
setPkgDetail(null);
|
||||
}
|
||||
}, [query, source]);
|
||||
|
||||
useEffect(() => {
|
||||
if (selectedPkgName) {
|
||||
loadDetails(selectedPkgName);
|
||||
} else {
|
||||
setPkgDetail(null);
|
||||
setScanResult(null);
|
||||
}
|
||||
}, [selectedPkgName]);
|
||||
|
||||
async function performSearch(q, s) {
|
||||
setLoading(true); setError(null);
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
try {
|
||||
const data = await api.searchPackages(q, s);
|
||||
setResults(data.results || []);
|
||||
} catch (err) { setError(err.message); setResults([]); }
|
||||
finally { setLoading(false); }
|
||||
const resList = data.results || [];
|
||||
setResults(resList);
|
||||
if (resList.length > 0) {
|
||||
setSelectedPkgName(resList[0].name);
|
||||
} else {
|
||||
setSelectedPkgName(null);
|
||||
}
|
||||
} catch (err) {
|
||||
setError(err.message);
|
||||
setResults([]);
|
||||
setSelectedPkgName(null);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
async function loadDetails(name) {
|
||||
setDetailLoading(true);
|
||||
setScanResult(null);
|
||||
setActionLog('');
|
||||
try {
|
||||
const detail = await api.getPackageInfo(name);
|
||||
setPkgDetail(detail);
|
||||
if (detail.source === 'aur') {
|
||||
doScan(detail.name);
|
||||
}
|
||||
} catch {
|
||||
// silent fallback
|
||||
} finally {
|
||||
setDetailLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
async function doScan(name) {
|
||||
setScanning(true);
|
||||
try {
|
||||
const scan = await api.scanPackage(name);
|
||||
setScanResult(scan);
|
||||
} catch {
|
||||
// silent fallback
|
||||
} finally {
|
||||
setScanning(false);
|
||||
}
|
||||
}
|
||||
|
||||
async function handleInstall() {
|
||||
if (!pkgDetail) return;
|
||||
setActing(true);
|
||||
setActionLog('Retrieving database structures...\nInitializing builder pipeline...\n');
|
||||
try {
|
||||
const res = await api.installPackage(pkgDetail.name);
|
||||
if (res.success) {
|
||||
setActionLog(prev => prev + '\n✓ Build completed successfully!\n' + res.message);
|
||||
loadDetails(pkgDetail.name);
|
||||
} else {
|
||||
setError('Installation failed: ' + res.message);
|
||||
}
|
||||
} catch (e) {
|
||||
setError(e.message);
|
||||
} finally {
|
||||
setActing(false);
|
||||
}
|
||||
}
|
||||
|
||||
async function handleUninstall() {
|
||||
if (!pkgDetail || !confirm(`Remove ${pkgDetail.name}?`)) return;
|
||||
setActing(true);
|
||||
setActionLog('Resolving package graph...\nRemoving target dependencies...\n');
|
||||
try {
|
||||
const res = await api.removePackage(pkgDetail.name);
|
||||
if (res.success) {
|
||||
setActionLog(prev => prev + '\n✓ Uninstallation complete!\n' + res.message);
|
||||
loadDetails(pkgDetail.name);
|
||||
} else {
|
||||
setError('Removal failed: ' + res.message);
|
||||
}
|
||||
} catch (e) {
|
||||
setError(e.message);
|
||||
} finally {
|
||||
setActing(false);
|
||||
}
|
||||
}
|
||||
|
||||
const tabs = [
|
||||
{ id: 'all', label: 'All' },
|
||||
{ id: 'all', label: 'All Packages' },
|
||||
{ id: 'pacman', label: 'Official' },
|
||||
{ id: 'aur', label: 'AUR' },
|
||||
{ id: 'aur', label: 'AUR Repos' },
|
||||
];
|
||||
|
||||
const filteredResults = results
|
||||
.filter((pkg) => !filters.installed || pkg.installed)
|
||||
.filter((pkg) => !filters.outOfDate || pkg.out_of_date)
|
||||
.filter((pkg) => !filters.hasVotes || (pkg.votes || 0) > 0);
|
||||
|
||||
const sortedResults = [...filteredResults].sort((a, b) => {
|
||||
if (sortBy === 'name') return a.name.localeCompare(b.name);
|
||||
if (sortBy === 'votes') return (b.votes || 0) - (a.votes || 0);
|
||||
if (sortBy === 'popularity') return (b.popularity || 0) - (a.popularity || 0);
|
||||
return 0;
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="animate-slide-up flex flex-col gap-4">
|
||||
{/* Header */}
|
||||
<div className="flex flex-col sm:flex-row sm:items-end justify-between gap-3">
|
||||
<div className="animate-slide-up flex flex-col gap-6 w-full">
|
||||
{/* Search page sub-header */}
|
||||
<div className="flex flex-col xl:flex-row xl:items-center justify-between gap-4">
|
||||
<div>
|
||||
<h1 className="page-title">Search Results</h1>
|
||||
<h1 className="page-title text-white">Advanced Search</h1>
|
||||
<p className="page-subtitle">
|
||||
{query ? <>Results for <strong style={{ color: 'var(--text-primary)' }}>"{query}"</strong></> : 'Enter a query to search'}
|
||||
{query ? <>Queried results for <strong style={{ color: 'var(--accent)' }}>"{query}"</strong></> : 'Query pacman databases and AUR repositories'}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{query && (
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="flex p-0.5 rounded-lg" style={{ background: 'var(--bg-secondary)', border: '1px solid var(--border-primary)' }}>
|
||||
<div className="flex flex-wrap items-center gap-3">
|
||||
<div className="flex p-1 rounded-lg bg-[var(--bg-secondary)] border border-[var(--border-primary)]">
|
||||
{tabs.map((tab) => (
|
||||
<button
|
||||
key={tab.id}
|
||||
onClick={() => setSource(tab.id)}
|
||||
className="px-3 py-1 text-[11px] font-semibold rounded-md transition-all"
|
||||
className="px-4 py-1.5 text-xs font-semibold rounded-md transition-all cursor-pointer"
|
||||
style={{
|
||||
background: source === tab.id ? 'var(--accent)' : 'transparent',
|
||||
color: source === tab.id ? '#fff' : 'var(--text-tertiary)',
|
||||
color: source === tab.id ? '#fff' : 'var(--text-secondary)',
|
||||
}}
|
||||
>
|
||||
{tab.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
<span className="flex items-center gap-1.5 text-[11px]" style={{ color: 'var(--text-tertiary)' }}>
|
||||
<Filter size={11} /> {results.length} pkg{results.length !== 1 ? 's' : ''}
|
||||
<span className="flex items-center gap-1.5 text-xs font-semibold" style={{ color: 'var(--text-tertiary)' }}>
|
||||
<Filter size={13} /> {sortedResults.length} results
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
<div className="rounded-lg p-3 text-xs font-medium"
|
||||
style={{ background: 'var(--red-muted)', color: 'var(--red)', border: '1px solid rgba(248,113,113,0.15)' }}>
|
||||
<div className="rounded-xl p-4 text-xs font-semibold"
|
||||
style={{ background: 'var(--red-muted)', color: 'var(--red)', border: '1px solid rgba(239, 68, 68, 0.2)' }}>
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{query ? (
|
||||
<PackageGrid packages={results} loading={loading} />
|
||||
) : (
|
||||
<div className="flex flex-col items-center justify-center py-14" style={{ color: 'var(--text-tertiary)' }}>
|
||||
<div className="w-10 h-10 rounded-xl flex items-center justify-center mb-3"
|
||||
style={{ background: 'var(--bg-secondary)', border: '1px solid var(--border-primary)' }}>
|
||||
<Search size={18} />
|
||||
/* Triple Column Split Layout */
|
||||
<div className="grid grid-cols-1 xl:grid-cols-12 gap-6 items-start w-full">
|
||||
{/* Results Panel */}
|
||||
<div className="xl:col-span-8 flex flex-col gap-4 min-w-0">
|
||||
<div className="flex flex-wrap items-center gap-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<label className="flex items-center gap-2 text-xs" style={{ color: 'var(--text-secondary)' }}>
|
||||
<input type="checkbox" checked={filters.installed} onChange={() => setFilters((prev) => ({ ...prev, installed: !prev.installed }))} />
|
||||
Installed
|
||||
</label>
|
||||
<label className="flex items-center gap-2 text-xs" style={{ color: 'var(--text-secondary)' }}>
|
||||
<input type="checkbox" checked={filters.outOfDate} onChange={() => setFilters((prev) => ({ ...prev, outOfDate: !prev.outOfDate }))} />
|
||||
Out of date
|
||||
</label>
|
||||
<label className="flex items-center gap-2 text-xs" style={{ color: 'var(--text-secondary)' }}>
|
||||
<input type="checkbox" checked={filters.hasVotes} onChange={() => setFilters((prev) => ({ ...prev, hasVotes: !prev.hasVotes }))} />
|
||||
Has votes
|
||||
</label>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 ml-auto">
|
||||
<span className="text-xs" style={{ color: 'var(--text-tertiary)' }}>Sort</span>
|
||||
<select
|
||||
value={sortBy}
|
||||
onChange={(e) => setSortBy(e.target.value)}
|
||||
className="input !py-1.5 !px-2 text-xs"
|
||||
style={{ width: 140 }}
|
||||
>
|
||||
{['relevance', 'name', 'votes', 'popularity'].map((opt) => (
|
||||
<option key={opt} value={opt}>{opt}</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-wrap items-center justify-between gap-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="pill">
|
||||
<CheckCircle size={12} /> {results.filter(r => r.installed).length} installed
|
||||
</span>
|
||||
<span className="pill">
|
||||
<AlertTriangle size={12} /> {results.filter(r => r.out_of_date).length} out of date
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
className="btn-ghost !p-2 rounded-lg"
|
||||
style={{ color: layoutMode === 'grid' ? 'var(--accent)' : 'var(--text-tertiary)' }}
|
||||
onClick={() => setLayoutMode('grid')}
|
||||
>
|
||||
<Grid size={15} />
|
||||
</button>
|
||||
<button
|
||||
className="btn-ghost !p-2 rounded-lg"
|
||||
style={{ color: layoutMode === 'list' ? 'var(--accent)' : 'var(--text-tertiary)' }}
|
||||
onClick={() => setLayoutMode('list')}
|
||||
>
|
||||
<List size={15} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{loading ? (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
{Array.from({ length: 6 }).map((_, i) => (
|
||||
<div key={i} className="card p-4 flex flex-col gap-2">
|
||||
<div className="shimmer h-4 w-32"></div>
|
||||
<div className="shimmer h-3 w-16 mb-2"></div>
|
||||
<div className="shimmer h-3 w-full"></div>
|
||||
<div className="shimmer h-3 w-3/4"></div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : sortedResults.length === 0 ? (
|
||||
<div className="card p-12 text-center flex flex-col items-center justify-center gap-3">
|
||||
<Package size={36} className="text-slate-500" />
|
||||
<h3 className="font-bold text-base text-slate-200">No packages match the query</h3>
|
||||
<p className="text-xs text-slate-400">Refine the query or check for alternative repository scopes.</p>
|
||||
</div>
|
||||
) : layoutMode === 'grid' ? (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
{sortedResults.map((pkg) => (
|
||||
<div
|
||||
key={`${pkg.source}-${pkg.name}`}
|
||||
className={`card card-interactive p-4 flex flex-col justify-between min-h-[150px] ${selectedPkgName === pkg.name ? 'border-[var(--border-secondary)]' : ''}`}
|
||||
onClick={() => setSelectedPkgName(pkg.name)}
|
||||
>
|
||||
<div>
|
||||
<div className="flex items-center justify-between gap-2 mb-2">
|
||||
<span className="font-extrabold text-sm text-slate-100 truncate flex items-center gap-1.5">
|
||||
{pkg.name}
|
||||
{pkg.installed && <CheckCircle size={12} className="text-[var(--green)]" />}
|
||||
{pkg.out_of_date && <AlertTriangle size={12} className="text-[var(--amber)]" />}
|
||||
</span>
|
||||
<span className={`badge ${pkg.source === 'aur' ? 'badge-aur' : 'badge-pacman'} text-[9px]`}>
|
||||
{pkg.source === 'aur' ? 'AUR' : (pkg.repository || 'pacman')}
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-xs text-slate-300 line-clamp-2 min-h-[2.6em] leading-relaxed">
|
||||
{pkg.description || 'No description available.'}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between pt-3 mt-3 border-t border-[var(--border-primary)] text-[11px]" style={{ color: 'var(--text-tertiary)' }}>
|
||||
<span className="font-mono text-slate-400 font-semibold">{pkg.version || '—'}</span>
|
||||
{pkg.installed ? (
|
||||
<span className="text-[var(--green)] font-bold">Installed</span>
|
||||
) : (
|
||||
<span className="text-[var(--accent)] font-semibold flex items-center gap-1">
|
||||
<Download size={11} /> Preview
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div className="data-table">
|
||||
<div className="table-head">
|
||||
<span>Name</span>
|
||||
<span>Version</span>
|
||||
<span>Source</span>
|
||||
<span>Description</span>
|
||||
<span>Status</span>
|
||||
</div>
|
||||
{sortedResults.map((pkg) => (
|
||||
<div
|
||||
key={`${pkg.source}-${pkg.name}`}
|
||||
className={`table-row ${selectedPkgName === pkg.name ? 'ring-1 ring-[var(--accent-glow)]' : ''}`}
|
||||
onClick={() => setSelectedPkgName(pkg.name)}
|
||||
>
|
||||
<span className="font-semibold text-slate-100 truncate">{pkg.name}</span>
|
||||
<span className="font-mono text-xs text-slate-400 truncate">{pkg.version || '—'}</span>
|
||||
<span className={`badge ${pkg.source === 'aur' ? 'badge-aur' : 'badge-pacman'} text-[9px] w-fit`}>
|
||||
{pkg.source === 'aur' ? 'AUR' : (pkg.repository || 'pacman')}
|
||||
</span>
|
||||
<span className="text-xs text-slate-400 truncate">{pkg.description || 'No description available.'}</span>
|
||||
<span className="text-xs font-semibold" style={{ color: pkg.installed ? 'var(--green)' : 'var(--text-tertiary)' }}>
|
||||
{pkg.installed ? 'Installed' : 'Available'}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<p className="text-sm font-medium mb-0.5" style={{ color: 'var(--text-secondary)' }}>Start searching</p>
|
||||
<p className="text-xs">Type a package name above</p>
|
||||
|
||||
{/* Right Panel: detailed specification drawer */}
|
||||
<div className="xl:col-span-4 card p-5 flex flex-col gap-5 sticky top-24 max-h-[calc(100vh-130px)] overflow-y-auto min-w-0">
|
||||
{detailLoading ? (
|
||||
<LoadingSpinner text="Fetching full package schema..." />
|
||||
) : pkgDetail ? (
|
||||
<div className="flex flex-col gap-4">
|
||||
|
||||
{/* Header info */}
|
||||
<div>
|
||||
<div className="flex flex-wrap items-center gap-2 mb-2">
|
||||
<span className="text-base font-extrabold text-white">{pkgDetail.name}</span>
|
||||
<span className={`badge ${pkgDetail.source === 'aur' ? 'badge-aur' : 'badge-pacman'}`}>
|
||||
{pkgDetail.source === 'aur' ? 'AUR' : 'pacman'}
|
||||
</span>
|
||||
{pkgDetail.installed && <span className="badge badge-installed">Active</span>}
|
||||
</div>
|
||||
<p className="text-xs text-slate-300 leading-relaxed">
|
||||
{pkgDetail.description || 'No description specs available for this system library.'}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Console build output */}
|
||||
{acting && (
|
||||
<div className="bg-slate-950 border border-[var(--border-glow)] rounded-xl p-3">
|
||||
<span className="text-[10px] font-bold text-slate-400 block mb-1">Process output console</span>
|
||||
<pre className="font-mono text-[9px] text-[var(--accent)] whitespace-pre-wrap max-h-32 overflow-y-auto leading-normal">
|
||||
{actionLog}
|
||||
</pre>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Actions Row */}
|
||||
<div className="grid grid-cols-2 gap-3 pb-3 border-b border-[var(--border-primary)]">
|
||||
{pkgDetail.installed ? (
|
||||
<button onClick={handleUninstall} disabled={acting} className="btn btn-danger py-2">
|
||||
<Trash2 size={13} /> Remove pkg
|
||||
</button>
|
||||
) : (
|
||||
<button onClick={handleInstall} disabled={acting} className="btn btn-primary py-2">
|
||||
<Download size={13} /> Install package
|
||||
</button>
|
||||
)}
|
||||
|
||||
{pkgDetail.url ? (
|
||||
<a href={pkgDetail.url} target="_blank" rel="noopener noreferrer"
|
||||
className="btn btn-secondary py-2 no-underline text-center justify-center flex items-center gap-1.5">
|
||||
<Globe size={13} /> Website <ExternalLink size={10} />
|
||||
</a>
|
||||
) : (
|
||||
<button className="btn btn-secondary opacity-50 cursor-not-allowed">No specs URL</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Static scan widget (AUR only) */}
|
||||
{pkgDetail.source === 'aur' && (
|
||||
<div className="bg-slate-900/40 border border-[var(--border-primary)] rounded-xl p-3.5 flex flex-col gap-2.5">
|
||||
<h4 className="text-xs font-bold text-slate-100 flex items-center gap-1.5">
|
||||
<Shield size={13} style={{ color: 'var(--accent)' }} /> PKGBUILD Security Scan
|
||||
</h4>
|
||||
{scanning ? (
|
||||
<span className="text-[10px] text-slate-400 animate-pulse">Running static scan routines...</span>
|
||||
) : scanResult ? (
|
||||
<div className="flex flex-col gap-2 text-[11px]">
|
||||
<div className="flex justify-between items-center bg-slate-950 p-2 rounded border border-[var(--border-primary)]">
|
||||
<span className="font-bold text-slate-300">Risk Assessment:</span>
|
||||
<span className="font-extrabold"
|
||||
style={{
|
||||
color: scanResult.risk_score >= 70 ? 'var(--red)'
|
||||
: scanResult.risk_score >= 40 ? 'var(--amber)'
|
||||
: 'var(--green)'
|
||||
}}>
|
||||
{scanResult.risk_score} / 100 ({scanResult.risk_level})
|
||||
</span>
|
||||
</div>
|
||||
{scanResult.findings.length > 0 ? (
|
||||
<div className="text-[10px] text-red-400 bg-red-950/20 border border-red-500/10 p-2 rounded">
|
||||
Static analyzer detected script mutations or potentially unsafe downloads. Verify manual details in PKGBUILD.
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-[10px] text-green-400 bg-green-950/20 border border-green-500/10 p-2 rounded">
|
||||
Verified PKGBUILD check completed. No suspicious mutations found.
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<span className="text-[10px] text-slate-400">Scan details not loaded.</span>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Grid Metadata details */}
|
||||
<div className="flex flex-col gap-2 text-[11px] bg-slate-950/50 p-3.5 rounded-xl border border-[var(--border-primary)]">
|
||||
<div className="flex justify-between py-1 border-b border-[var(--border-primary)]/40">
|
||||
<span style={{ color: 'var(--text-tertiary)' }}>Package version</span>
|
||||
<span className="font-mono text-slate-200">{pkgDetail.version}</span>
|
||||
</div>
|
||||
{pkgDetail.votes !== undefined && (
|
||||
<div className="flex justify-between py-1 border-b border-[var(--border-primary)]/40">
|
||||
<span style={{ color: 'var(--text-tertiary)' }}>AUR Vote Weight</span>
|
||||
<span className="font-semibold text-slate-200 flex items-center gap-0.5">
|
||||
<Star size={11} className="text-amber-500 fill-amber-500" /> {pkgDetail.votes}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
{pkgDetail.maintainer && (
|
||||
<div className="flex justify-between py-1">
|
||||
<span style={{ color: 'var(--text-tertiary)' }}>AUR Maintainer</span>
|
||||
<span className="font-bold text-slate-200">{pkgDetail.maintainer}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-center py-20 text-slate-400 flex flex-col items-center gap-3">
|
||||
<Shield size={32} className="text-slate-500" />
|
||||
<p className="text-xs font-semibold text-slate-300">Select a Package for Details</p>
|
||||
<p className="text-[10px] max-w-[200px] leading-relaxed">
|
||||
Click on any searched package to review security scans, versions, votes, and maintainer specifications instantly.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
) : (
|
||||
/* Empty State Illustration */
|
||||
<div className="card p-16 text-center flex flex-col items-center justify-center gap-4 max-w-2xl mx-auto mt-8">
|
||||
<div className="w-14 h-14 rounded-2xl flex items-center justify-center bg-slate-900 border border-[var(--border-glow)]"
|
||||
style={{ boxShadow: 'var(--shadow-glow)' }}>
|
||||
<Search size={28} className="text-[var(--accent)]" />
|
||||
</div>
|
||||
<h2 className="text-lg font-extrabold text-white">Find Arch Software Instantly</h2>
|
||||
<p className="text-xs text-slate-400 max-w-sm leading-relaxed">
|
||||
Search across official core, extra, multilib repositories, and the community-driven AUR. Use the top navigation bar to initialize queries.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user