Desktop: More tools gets a command search across groups and a compact list layout

This commit is contained in:
CRTkafa 2026-10-23 09:46:55 +08:00
parent e32d0772e9
commit 78b1b599a5
4 changed files with 41 additions and 5 deletions

View file

@ -9,8 +9,8 @@ import { api, errorText, type MethodDescriptor, newJob, type Project, type ToolR
import { type Key, t } from '../../i18n';
import { useData, useWrite } from '../../store';
import { Icon } from '../../ui/icons';
import { ErrorNotice, Skeleton } from '../../ui/kit';
import { Section, StatusPill } from '../../ui/ds';
import { ErrorNotice, Seg, Skeleton } from '../../ui/kit';
import { Section, StatusPill, Toolbar } from '../../ui/ds';
import '../../styles/tools.css';
import { WriteButton } from '../../ui/WriteButton';
@ -111,6 +111,9 @@ export function ToolsTab({ project }: { project: Project }) {
const [methods, setMethods] = useState<MethodDescriptor[] | null>(null);
const [err, setErr] = useState<string | null>(null);
const [open, setOpen] = useState<string>(TOOLBOX[0]!.id);
const [q, setQ] = useState('');
const [dense, setDenseState] = useState<boolean>(() => { try { return localStorage.getItem('sb.tools.dense') === '1'; } catch { return false; } });
const setDense = (v: boolean) => { setDenseState(v); try { localStorage.setItem('sb.tools.dense', v ? '1' : '0'); } catch { /* private mode */ } };
useEffect(() => {
api.call<{ methods: MethodDescriptor[] }>('system.methods').then(r => setMethods(r.methods), e => setErr(errorText(e)));
}, []);
@ -118,12 +121,26 @@ export function ToolsTab({ project }: { project: Project }) {
const copy = (c: string) => { navigator.clipboard?.writeText(c).then(() => d.toast({ kind: 'ok', title: t('tools.copied') }), () => {}); };
const groups = TOOLBOX.map(g => ({ g, ms: g.methods.map(x => by.get(x)).filter((x): x is MethodDescriptor => !!x) }));
const cur = groups.find(x => x.g.id === open);
const needle = q.trim().toLowerCase();
const hits = needle ? groups.flatMap(x => x.ms).filter(m => [m.method, m.title, m.description ?? ''].some(x => x.toLowerCase().includes(needle))) : [];
return (
<div className="scr-tools">
<div className={`scr-tools${dense ? ' dense' : ''}`}>
<p className="tl-lead"><Icon name="info" />{t('tools.lead')}</p>
{methods && (
<Toolbar label={t('tools.filter')}>
<div className="search-input"><Icon name="search" /><input className="input" value={q} onChange={e => setQ(e.target.value)} placeholder={t('tools.search', { count: groups.reduce((a, x) => a + x.ms.length, 0) })} aria-label={t('tools.searchAria')} onKeyDown={e => { if (e.key === 'Escape') setQ(''); }} /></div>
<span className="grow" />
<Seg value={dense ? 'list' : 'cards'} onChange={v => setDense(v === 'list')} label={t('tools.density')} options={[['cards', t('pf.cards'), 'grid'], ['list', t('tools.list'), 'layers']]} />
</Toolbar>
)}
{needle && (
<Section title={t('tools.results', { count: hits.length })} icon="search">
{hits.length ? <div className="tl-cmds">{hits.map(m => <Tool key={m.method} m={m} project={project} />)}</div> : <p className="muted">{t('tools.noHits')}</p>}
</Section>
)}
{err && <ErrorNotice error={err} />}
{!methods && !err && <Skeleton lines={4} />}
{methods && (
{methods && !needle && (
<div className="tl-groups" role="group" aria-label={t('tab.tools')}>
{groups.map(({ g, ms }) => {
const isOpen = open === g.id;
@ -138,7 +155,7 @@ export function ToolsTab({ project }: { project: Project }) {
})}
</div>
)}
{methods && cur && (
{methods && cur && !needle && (
<Section id="tools-open" title={t(`tools.g.${cur.g.id}` as Key)} icon="sliders" sub={t('tools.count', { count: cur.ms.length })}>
<div className="tl-cmds">{cur.ms.map(m => <Tool key={m.method} m={m} project={project} />)}</div>
</Section>