import { searchThreads, type SearchResult } from "$lib/search"; // Debounced-search state machine shared by the inline (homepage) and modal // search shells, so behavior lives in one place and the shells only differ // in chrome and positioning. export function createSearchState() { let query = $state(""); let results = $state([]); let resultsQuery = $state(""); // The query that produced the current results let searching = $state(false); let activeIndex = $state(-1); let timer: ReturnType | null = null; let seq = 0; const terms = $derived( resultsQuery.split(/\s+/).filter((t) => t.length >= 2), ); function schedule() { const q = query.trim(); if (timer) clearTimeout(timer); activeIndex = -1; if (q.length < 2) { seq++; // Supersede any in-flight query results = []; resultsQuery = ""; searching = false; return; } searching = true; timer = setTimeout(async () => { const id = ++seq; try { const r = await searchThreads(q); if (id !== seq) return; results = r; resultsQuery = q; } finally { if (id === seq) searching = false; } }, 300); } function escapeRe(s: string): string { return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); } // Split into alternating plain/matched segments for rendering function highlight(text: string): { text: string; hit: boolean }[] { if (!text || terms.length === 0) return [{ text, hit: false }]; const alts = terms.map(escapeRe).join("|"); const exact = new RegExp(`^(${alts})$`, "i"); return text .split(new RegExp(`(${alts})`, "gi")) .filter((s) => s !== "") .map((s) => ({ text: s, hit: exact.test(s) })); } // Arrow/Enter handling; returns true when the event was consumed function navigate( e: KeyboardEvent, pick: (r: SearchResult) => void, ): boolean { if (results.length === 0) return false; if (e.key === "ArrowDown") { activeIndex = (activeIndex + 1) % results.length; } else if (e.key === "ArrowUp") { activeIndex = (activeIndex - 1 + results.length) % results.length; } else if (e.key === "Enter" && activeIndex >= 0) { pick(results[activeIndex]); } else { return false; } e.preventDefault(); return true; } return { get query() { return query; }, set query(v: string) { query = v; }, get results() { return results; }, get searching() { return searching; }, get activeIndex() { return activeIndex; }, set activeIndex(v: number) { activeIndex = v; }, // True once a query has produced (or is about to produce) results get active() { return resultsQuery.trim().length >= 2; }, schedule, highlight, navigate, }; } export type SearchState = ReturnType;