RegistrySearch
uses document
The header's search (React Aria's Autocomplete, as the forge's finder is): names, docs or :types across the libraries, with results as you type, by library: each library, then what in it matches. ⌘K or / goes to it, arrows choose, Enter opens the one chosen (or, with none chosen, the search page). On the search page it holds what was searched for.
import { navigate, useLocation } from "#codebase/tcmts/react"; import { registrySearchFor } from "#codebase/tcmts/registry"; import { at } from "#codebase/tcmts/share"; import { useEffect, useRef, useState } from "react"; import { Autocomplete, Collection, Input, Keyboard, ListBox, ListBoxItem, ListBoxSection, Popover, SearchField, } from "react-aria-components"; export function RegistrySearch() { const { path, query } = useLocation(); const here = `${path}?${query}`; const searched = path === "/search" ? (query.get("q") ?? "") : ""; const [text, setText] = useState(searched); const [open, setOpen] = useState(false); // (another page: what it searched for, if it's the search page, and the results put away) const [was, setWas] = useState(here); if (was !== here) { setWas(here); setText(searched); setOpen(false); } const [found, setFound] = useState<{ for: string; libraries: Awaited<ReturnType<typeof registrySearchFor>>; }>({ for: "", libraries: [] }); const field = useRef<HTMLDivElement>(null); const input = useRef<HTMLInputElement>(null); useEffect(() => { const key = (e: KeyboardEvent) => { const typing = /INPUT|TEXTAREA|SELECT/.test(document.activeElement?.tagName ?? ""); if ((e.key === "k" && (e.metaKey || e.ctrlKey)) || (e.key === "/" && !typing)) { e.preventDefault(); input.current?.focus(); input.current?.select(); } }; document.addEventListener("keydown", key); return () => document.removeEventListener("keydown", key); }, []); const t = text.trim(); useEffect(() => { if (!t) return; let live = true; const timer = setTimeout(() => { registrySearchFor(t).then( (r) => live && setFound({ for: t, libraries: r }), () => live && setFound({ for: t, libraries: [] }), ); }, 150); return () => { live = false; clearTimeout(timer); }; }, [t]); const libraries = t ? found.libraries : []; return ( <div className="search" ref={field}> <Autocomplete inputValue={text} onInputChange={(v) => (setText(v), setOpen(true))}> <SearchField aria-label="Search libraries" className="searchfield" onSubmit={(v) => { // (with a result chosen, Enter opens it) if (input.current?.getAttribute("aria-activedescendant") || !v.trim()) return; setOpen(false); navigate(at(`/search?q=${encodeURIComponent(v.trim())}`)); }} onClear={() => setOpen(false)} > <Input ref={input} className="field" placeholder="Search names, docs or :types" onFocus={() => setOpen(true)} /> <Keyboard className="kbd">⌘K</Keyboard> </SearchField> <Popover triggerRef={field} isOpen={open && !!t} onOpenChange={setOpen} isNonModal placement="bottom end" offset={6} className="results" > <ListBox aria-label="Results" className="result-list" items={libraries} renderEmptyState={() => ( <div className="none">{found.for === t ? "Nothing found" : "Searching…"}</div> )} > {(l) => ( <ListBoxSection id={l.id} aria-label={l.library} className="result-sec"> <ListBoxItem id={`library ${l.id}`} textValue={l.library} href={l.href} className="result" > <b className="mono">{l.library}</b> <span className="muted">{l.version}</span> {l.description && <span className="desc">{l.description}</span>} </ListBoxItem> <Collection items={l.hits}> {(h) => ( <ListBoxItem id={h.id} textValue={h.name} href={h.href} className="result hit"> <span className="mono">{h.name}</span> <span className="sig">{h.signature}</span> </ListBoxItem> )} </Collection> </ListBoxSection> )} </ListBox> </Popover> </Autocomplete> </div> ); }