Desktop: Settings search filters the sections (nav and body), Enter jumps to the first match

This commit is contained in:
CRTkafa 2026-10-23 09:47:29 +08:00
parent 78b1b599a5
commit c86b583830
4 changed files with 27 additions and 1 deletions

View file

@ -79,6 +79,8 @@ export const en = {
'tools.list': "List",
'tools.results': "{count} matching commands",
'tools.noHits': "No command matches. Try a store word (listing, prices, testflight...).",
'set.search': "Search settings",
'set.noHits': "No setting matches",
'palette.gActions': 'Actions',
'palette.openApp': 'Open',

View file

@ -81,6 +81,8 @@ export const tr: Record<keyof typeof en, string> = {
'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...).",
'set.search': "Ayarlarda ara",
'set.noHits': "Eşleşen ayar yok",
'palette.gActions': 'İşlemler',
'palette.openApp': 'Aç',

View file

@ -80,6 +80,21 @@ export function Settings({ onTheme: _onTheme, section }: { onTheme: () => void;
}
} catch (e) { setError(errorText(e)); }
};
// search: sections whose title or text contains the words stay; the rest hide (nav and body)
const [q, setQ] = useState('');
const [hits, setHits] = useState<string[] | null>(null);
useEffect(() => {
const words = q.trim().toLowerCase().split(/\s+/).filter(Boolean);
const out: string[] = [];
for (const [id, , key] of SECTIONS) {
const el = document.getElementById(`set-${id}`);
const text = `${t(key)} ${el?.textContent ?? ''}`.toLowerCase();
const ok = !words.length || words.every(w => text.includes(w));
if (el) el.hidden = !ok;
if (ok) out.push(id);
}
setHits(words.length ? out : null);
}, [q]);
const storeOnly = d.apps.filter(a => !a.project && !a.ignored);
const paired = storeOnly.filter(a => a.ios && a.android);
const singles = storeOnly.filter(a => !(a.ios && a.android));
@ -90,7 +105,10 @@ export function Settings({ onTheme: _onTheme, section }: { onTheme: () => void;
{error && <Notice tone="bad">{error}</Notice>}
<div className="set-layout">
<nav className="set-nav" aria-label={t('set.sections')}>
{SECTIONS.map(([id, icon, key]) => (
<div className="search-input set-search"><Icon name="search" /><input className="input input-sm" type="search" value={q} onChange={e => setQ(e.target.value)} placeholder={t('set.search')} aria-label={t('set.search')}
onKeyDown={e => { if (e.key === 'Escape') setQ(''); if (e.key === 'Enter' && hits?.[0]) jump(hits[0]); }} /></div>
{hits && !hits.length && <p className="small muted set-nohit">{t('set.noHits')}</p>}
{SECTIONS.filter(([id]) => !hits || hits.includes(id)).map(([id, icon, key]) => (
<button key={id} className="nav" aria-current={active === id ? 'true' : undefined} onClick={() => jump(id)}><Icon name={icon} /><span className="grow">{t(key)}</span></button>
))}
</nav>

View file

@ -35,3 +35,7 @@
.scr-settings .danger-card { border-color: color-mix(in srgb, var(--bad-fg, var(--accent-solid)) 35%, var(--ds-stroke)); }
.scr-settings .danger-card .ds-sec-ic { background: color-mix(in srgb, var(--bad-fg, var(--accent-solid)) 16%, transparent); color: var(--bad-fg, var(--accent-solid)); }
@media (max-width: 1100px) { .scr-settings .set-layout { grid-template-columns: 1fr; } }
.scr-settings .set-search { margin-bottom: 6px; }
.scr-settings .set-search .input { width: 100%; }
.scr-settings .set-nohit { padding: 4px 8px; }
.scr-settings .set-body > [hidden] { display: none; }