import { useEffect, useMemo, useRef, useState } from "react"; import { Link, useNavigate } from "react-router-dom"; import { search, type SearchHit } from "../lib/docs"; interface Props { autoFocus?: boolean; placeholder?: string; } export function SearchBox({ autoFocus, placeholder }: Props) { const [query, setQuery] = useState(""); const [active, setActive] = useState(0); const [open, setOpen] = useState(false); const inputRef = useRef(null); const navigate = useNavigate(); const hits: SearchHit[] = useMemo(() => search(query, 12), [query]); useEffect(() => { if (autoFocus) inputRef.current?.focus(); }, [autoFocus]); // Global "/" shortcut to focus the search box (only if the user isn't // already typing in some other field). useEffect(() => { const onKey = (e: KeyboardEvent) => { if (e.key !== "/") return; const target = e.target as HTMLElement | null; if (target && /input|textarea|select/i.test(target.tagName)) return; e.preventDefault(); inputRef.current?.focus(); setOpen(true); }; window.addEventListener("keydown", onKey); return () => window.removeEventListener("keydown", onKey); }, []); const onKeyDown = (e: React.KeyboardEvent) => { if (e.key === "ArrowDown") { e.preventDefault(); setActive((a) => Math.min(a + 1, hits.length - 1)); } else if (e.key === "ArrowUp") { e.preventDefault(); setActive((a) => Math.max(a - 1, 0)); } else if (e.key === "Enter" && hits[active]) { e.preventDefault(); navigate(`/docs/${hits[active].doc.slug}`); setOpen(false); } else if (e.key === "Escape") { setOpen(false); inputRef.current?.blur(); } }; return (
🔍 { setQuery(e.target.value); setActive(0); setOpen(true); }} onFocus={() => setOpen(true)} onKeyDown={onKeyDown} autoComplete="off" spellCheck={false} /> {query && ( )}
{open && query.trim() && (
{hits.length === 0 && (
No matches for “{query}”.
)} {hits.map((hit, i) => ( setActive(i)} onClick={() => setOpen(false)} >
{hit.doc.title} {hit.doc.breadcrumbs.join(" / ")}
{hit.snippet}
))}
)}
); }