"use client"; import { useEffect, useRef, useState, type ReactNode } from "react"; import { fuzzyScore } from "@/src/core"; /** The model picker's search, arrow-walk, row styling and scrolling machine. * Models and dictation languages supply only their catalog and row content. * The caller owns the shared MenuShell and dismissal behavior. */ export function SearchMenuList({ items, itemKey, searchTerms, compare, isSelected, onPick, renderItem, searchTestid, listTestid, optionTestid, placeholder, ariaLabel, loading, empty, disabled = false, }: { items: readonly T[] | undefined; itemKey: (item: T) => string; searchTerms: (item: T) => string[]; compare?: (a: T, b: T) => number; isSelected: (item: T) => boolean; onPick: (item: T) => void; renderItem: (item: T) => ReactNode; searchTestid: string; listTestid: string; optionTestid: (item: T) => string; placeholder: string; ariaLabel?: string; loading?: string; empty: string; disabled?: boolean; }) { const [query, setQuery] = useState(""); const [menuIdx, setMenuIdx] = useState(null); const searchRef = useRef(null); const listRef = useRef(null); useEffect(() => { searchRef.current?.focus(); }, []); useEffect(() => { if (menuIdx === null) return; listRef.current?.querySelector(`[data-idx="${menuIdx}"]`)?.scrollIntoView({ block: "nearest" }); }, [menuIdx]); const results = (items ?? []) .map((item) => ({ item, score: fuzzyScore(query, ...searchTerms(item)) })) .filter((result) => result.score !== null) .sort((a, b) => b.score! - a.score! || (compare?.(a.item, b.item) ?? 0)) .map(({ item }) => item); return <> { setQuery(event.target.value); setMenuIdx(null); }} onKeyDown={(event) => { if (event.key === "ArrowDown" || event.key === "ArrowUp") { event.preventDefault(); event.stopPropagation(); if (disabled || results.length === 0) return; setMenuIdx((i) => event.key === "ArrowDown" ? Math.min((i ?? -1) + 1, results.length - 1) : Math.max((i ?? results.length) - 1, 0)); } else if (event.key === "Enter") { const chosen = results[menuIdx ?? 0]; if (!disabled && chosen !== undefined) { event.preventDefault(); event.stopPropagation(); onPick(chosen); } } }} placeholder={placeholder} className="lh-input mb-1.5 w-full rounded-lg border bg-background px-2 py-1.5 text-foreground outline-none focus:ring-1 focus:ring-ring" /> {!items && loading &&
{loading}
} {items && results.length === 0 &&
{empty}
}
    {results.map((item, i) =>
  • )}
; }