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

@ -72,6 +72,13 @@ export const en = {
'tabs.more': "{count} more tabs",
'tabs.moreShort': "More",
'reviews.keysHint': "Keyboard: J / K next and previous, R reply, C copy, O open in the console",
'tools.filter': "Find a command",
'tools.search': "Search {count} commands (name, method, what it does)",
'tools.searchAria': "Search commands",
'tools.density': "Layout",
'tools.list': "List",
'tools.results': "{count} matching commands",
'tools.noHits': "No command matches. Try a store word (listing, prices, testflight...).",
'palette.gActions': 'Actions',
'palette.openApp': 'Open',

View file

@ -74,6 +74,13 @@ export const tr: Record<keyof typeof en, string> = {
'tabs.more': "{count} sekme daha",
'tabs.moreShort': "Daha fazla",
'reviews.keysHint': "Klavye: J / K sonraki ve önceki, R yanıtla, C kopyala, O konsolda aç",
'tools.filter': "Komut bul",
'tools.search': "{count} komutta ara (ad, yöntem, ne yaptığı)",
'tools.searchAria': "Komut ara",
'tools.density': "Görünüm",
'tools.list': "Liste",
'tools.results': "{count} eşleşen komut",
'tools.noHits': "Eşleşen komut yok. Bir mağaza kelimesi deneyin (listing, prices, testflight...).",
'palette.gActions': 'İşlemler',
'palette.openApp': 'Aç',

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>

View file

@ -46,3 +46,8 @@
.scr-tools .tl-step { display: flex; align-items: center; gap: 12px; padding: 12px 14px; border-radius: var(--ds-r); background: var(--ds-inset); border: 1px solid var(--ds-stroke); min-width: 0; }
.scr-tools .tl-step-t { display: grid; gap: 4px; flex: 1; min-width: 0; color: var(--ink); font-weight: 600; }
.scr-tools .tl-step-t code { font-size: var(--ds-t-micro); color: var(--muted); font-weight: 500; overflow-wrap: anywhere; }
/* list layout: smaller group tiles, one command per row */
.scr-tools.dense .tl-groups { grid-template-columns: repeat(auto-fill, minmax(190px, 1fr)); gap: 8px; }
.scr-tools.dense .tl-group { padding-top: 8px; padding-bottom: 8px; }
.scr-tools.dense .tl-group-ic { display: none; }
.scr-tools.dense .tl-cmds { grid-template-columns: minmax(0, 1fr); gap: 8px; }