import { createEffect, createSignal, untrack, Show } from 'solid-js'; import { defineWebComponent } from './define'; import { Input } from '../ui/input'; import { Kbd } from '../ui/kbd'; import { Loader } from '../components/loader'; import { renderIcon } from '../ui/icon'; interface Props extends Record { /** Controlled query — settable and reflected to the `value` attribute. Read * `el.value` for live state. */ value?: string; /** Placeholder. Defaults to `Search…`. */ placeholder?: string; /** Leading icon-NAME string (a curated name, URL, or text), resolved to a glyph * the same way `kai-button`'s `icon` is. Defaults to `search`. */ icon?: string; /** Debounce window for `kai-search`, in ms. Defaults to `200`. */ debounce?: number; /** Show a spinner in place of the leading icon while results load. */ loading?: boolean; /** Optional shortcut hint shown (as a `kai-kbd`) while the field is empty, * e.g. `Mod+K`. Display only; it does not bind the key. */ shortcut?: string; } /** Events fired by ``. */ interface Events { /** The query changed (debounced live, and on clear). */ 'kai-search': { value: string }; /** Enter was pressed. */ 'kai-submit': { value: string }; /** The field committed (blur). */ 'kai-change': { value: string }; } /** * `` — a debounced filter field built on `kai-input`. Type to fire a * debounced `kai-search`, press Enter for `kai-submit`. A clear (×) button shows * once there's a value; an optional `shortcut` renders a `kai-kbd` hint while the * field is empty. * * ```html * * * ``` * * Drive/read the query with the `value` property (reflected to the attribute). * Restyle via `::part(field)`, `::part(input)`, and `::part(clear)`. */ defineWebComponent('kai-search', { value: undefined, placeholder: 'Search…', icon: 'search', debounce: 200, loading: false, shortcut: undefined, }, (props, ctx) => { const { element, dispatch, flag, expose } = ctx; // Lift the query into the facade and drive Input CONTROLLED so the host can read // it (`el.value` / `:host([value])`) and set it after mount. Mirrors kai-segmented. const [value, setValue] = createSignal( (props.value as string | undefined) ?? element.getAttribute('value') ?? '', ); const coerce = (v: unknown): string => v == null ? (element.getAttribute('value') ?? '') : String(v); // Reads return LIVE state; host writes (`el.value = …`) drive it WITHOUT firing // events. The equality guard kills the reflect write-back loop. Object.defineProperty(element, 'value', { get: () => value(), set: (v: unknown) => { const next = coerce(v); if (untrack(value) !== next) setValue(next); }, configurable: true, enumerable: true, }); // Reflect internal value → `[value]` host attribute (for `:host([value])`). createEffect(() => { const v = value(); if (v) { if (element.getAttribute('value') !== v) element.setAttribute('value', v); } else if (element.hasAttribute('value')) { element.removeAttribute('value'); } }); // Debounced kai-search: each keystroke resets the timer; it fires once after the // window. Clearing fires immediately (timer cancelled). let timer: ReturnType | undefined; const debounceMs = () => { const ms = Number(props.debounce ?? 200); return Number.isFinite(ms) && ms >= 0 ? ms : 200; }; const scheduleSearch = (next: string) => { if (timer) clearTimeout(timer); timer = setTimeout(() => dispatch('kai-search', { value: next }), debounceMs()); }; const getInput = (): HTMLInputElement | null => element.shadowRoot?.querySelector('input') ?? null; const onInput = (next: string) => { setValue(next); scheduleSearch(next); }; // Empty the field, fire kai-search '' immediately (cancelling any pending), refocus. const clear = () => { if (timer) clearTimeout(timer); setValue(''); dispatch('kai-search', { value: '' }); getInput()?.focus(); }; const onKeyDown = (e: KeyboardEvent) => { if (e.key === 'Enter') dispatch('kai-submit', { value: untrack(value) }); }; expose({ focus: () => getInput()?.focus(), clear, }); // Leading: a spinner while loading, otherwise the resolved icon glyph. const leading = ( ); // Trailing: a clear button once there's a value, else a shortcut hint if set. const trailing = ( <> ); return ( <> dispatch('kai-change', { value: v })} onKeyDown={onKeyDown} /> ); });