import { useEffect, useRef, useState } from "preact/hooks";
import type { GraphPayload } from "../../shared/wire";
import { useFocusTrap } from "../shell/FocusTrap";
import { fetchSearch } from "../api";
import type { FetchLike } from "../api";
import type { SearchRowView } from "./search.model";
import { DEBOUNCE_MS, PALETTE_HINT, PALETTE_PLACEHOLDER, PALETTE_TITLE, initialSearchState, paletteModel, resultIdAt, searchKey } from "./search.model";
import type { SearchState } from "./search.model";
export interface SearchPaletteProps {
graph: GraphPayload | null;
onSelect: (id: string) => void;
onClose: () => void;
ports: { fetch: FetchLike };
}
function Row({ row, onPick, onHover }: { row: SearchRowView; onPick: () => void; onHover: () => void }) {
return (
{row.badge}
{row.label}
{row.detail}
);
}
export function SearchPalette(props: SearchPaletteProps) {
const [state, setState] = useState(initialSearchState);
const requestId = useRef(0);
const abort = useRef(null);
const trap = useFocusTrap();
const input = useRef(null);
useEffect(() => void input.current?.focus(), []);
useEffect(() => {
const query = state.query.trim();
let active = true;
if (!query) {
abort.current?.abort();
setState((current) => ({ ...current, hits: [], answered: "", loading: false, failed: false }));
return () => { active = false; };
}
const timer = window.setTimeout(async () => {
const id = ++requestId.current;
abort.current?.abort();
const controller = new AbortController();
abort.current = controller;
setState((current) => ({ ...current, loading: true, failed: false }));
const result = await fetchSearch(props.ports.fetch, query, controller.signal);
if (!active || controller.signal.aborted || id !== requestId.current) return;
setState((current) => result.ok
? { ...current, hits: result.data.hits, answered: query, loading: false, failed: false, cursor: 0 }
: { ...current, loading: false, failed: true });
}, DEBOUNCE_MS);
return () => {
active = false;
window.clearTimeout(timer);
abort.current?.abort();
};
}, [state.query, props.ports.fetch]);
const model = paletteModel(state, props.graph);
const pick = (index: number | null): void => {
const id = resultIdAt(model.rows, index);
if (id !== null) props.onSelect(id);
props.onClose();
};
return (
pick(null)}>
event.stopPropagation()}
onKeyDown={(event) => {
if (trap.onKeyDown(event as unknown as KeyboardEvent)) return;
const next = searchKey(state.cursor, event.key, model.count);
if (!next.handled) return;
event.preventDefault();
setState((current) => ({ ...current, cursor: next.cursor }));
if (next.dismiss) pick(next.activate);
}}
>
setState((current) => ({ ...current, query: event.currentTarget.value, cursor: 0 }))}
/>
{model.status === null ? (
{model.rows.map((row, index) => pick(index)} onHover={() => setState((current) => ({ ...current, cursor: index }))} />)}
) : (
{model.status}
)}
);
}