SearchPalette.tsx
Language TypeScript2.7 KB82 lines
import { useEffect, useRef, useState } from 'react'import type { SearchHit } from '../ipc/gen/SearchHit'import { relativeDay, snippetParts } from '../lib/format'import { useStore } from '../state/store'export function SearchPalette() {const [q, setQ] = useState('')const [hits, setHits] = useState<SearchHit[]>([])const [cursor, setCursor] = useState(0)const client = useStore((s) => s.client)const open = useStore((s) => s.open)const setPanel = useStore((s) => s.setPanel)const input = useRef<HTMLInputElement>(null)useEffect(() => input.current?.focus(), [])useEffect(() => {if (!client) returnconst t = setTimeout(() =>void client.search(q).then((h) => {setHits(h)setCursor(0)}),120,)return () => clearTimeout(t)}, [q, client])return (<div className="overlay" onMouseDown={(e) => e.target === e.currentTarget && setPanel(null)}><divclassName="palette glass-strong"role="dialog"aria-label="Search chats"onKeyDown={(e) => {if (e.key === 'ArrowDown') {e.preventDefault()setCursor((c) => Math.min(c + 1, hits.length - 1))}if (e.key === 'ArrowUp') {e.preventDefault()setCursor((c) => Math.max(c - 1, 0))}if (e.key === 'Enter' && hits[cursor]) void open(hits[cursor].conversation_id)}}><inputref={input}className="search-input big"placeholder="Search every chat"value={q}onChange={(e) => setQ(e.target.value)}aria-label="Search every chat"data-testid="search-input"/><ul className="hits" role="listbox">{hits.map((h, i) => (<likey={`${h.conversation_id}-${h.message_id ?? 't'}`}role="option"aria-selected={i === cursor}className={i === cursor ? 'cursor' : ''}onMouseEnter={() => setCursor(i)}onClick={() => void open(h.conversation_id)}><div className="hit-head"><strong>{h.title}</strong><span className="muted small">{relativeDay(h.created_at)}</span></div><div className="hit-snippet">{snippetParts(h.snippet).map((p, j) =>p.hit ? <mark key={j}>{p.text}</mark> : <span key={j}>{p.text}</span>,)}</div></li>))}{q.trim() && hits.length === 0 && <li className="empty-row">Nothing found.</li>}</ul></div></div>)}