import { h } from 'preact'; import { useState, useMemo } from 'preact/hooks'; // A single fuzzy-finder input for picking a signal as one "ds:name" reference, // replacing the old stacked "Data source" + "Signal" select pair. The caller // supplies a flattened option list (every ds:name it knows about) and, via // onOpen, a hook to lazily load signals for all data sources when the dropdown // is first opened. When allowFreeform is set, typing a value that matches no // option (e.g. a new srv:/local: variable) can still be committed. export interface SignalOption { ds: string; name: string; } export default function SignalPicker({ value, options, onChange, onOpen, placeholder = 'Search signals…', allowFreeform = false, }: { value: string; // current "ds:name" (or "") options: SignalOption[]; onChange: (ref: string) => void; onOpen?: () => void; placeholder?: string; allowFreeform?: boolean; }) { const [open, setOpen] = useState(false); const [filter, setFilter] = useState(''); const filtered = useMemo(() => { const tokens = filter.toLowerCase().split(/\s+/).filter(Boolean); const scored = options .map(o => ({ o, label: `${o.ds}:${o.name}` })) .filter(({ label }) => { const l = label.toLowerCase(); return tokens.every(t => l.includes(t)); }); // Prefer matches where the signal name (not the ds prefix) leads. const f = filter.toLowerCase(); scored.sort((a, b) => { const an = a.o.name.toLowerCase().startsWith(f) ? 0 : 1; const bn = b.o.name.toLowerCase().startsWith(f) ? 0 : 1; if (an !== bn) return an - bn; return a.label.localeCompare(b.label); }); return scored.slice(0, 200); }, [options, filter]); function openDropdown() { setOpen(true); setFilter(''); onOpen?.(); } function commit(ref: string) { onChange(ref); setOpen(false); setFilter(''); } // A freeform entry is offered when the typed text looks like a usable // reference and is not already an exact option. const freeform = allowFreeform && filter.trim() !== '' && !filtered.some(({ label }) => label === filter.trim()); return (