/** value's shape must match isMultiple (array vs single object) — a mismatch normalizes to no selection, not an error. */ /** Prefer `Select` with `native={false}` for new work — not yet a drop-in; see COMPONENT-GOTCHAS.md ("Which select component"). */ export type SearchSelectProps = { /** Base DOM id of the trigger; listbox and option ids derive from it. Auto-generated when omitted. */ id?: string; /** Visible label above the field; omit it and set `aria-label` for a labelless field. */ label?: string; /** Accessible name for a labelless field; falls back to the visible label when both are set. */ "aria-label"?: string; /** Filtered against option.label only — a custom `view` node's visible text isn't searchable. */ options: Options[]; /** Canonical change callback; required for selections to stick (fully controlled). Wins over `onSelect`. */ onChange?: (options: Options | Options[] | undefined) => void; /** @deprecated Use `onChange` — same payload; renamed to match every other picker. */ onSelect?: (options: Options | Options[] | undefined) => void; /** Fires 1s after typing stops; local filtering against `options` already runs synchronously regardless. */ onSearch?: (value: string) => void; /** Shows `loadingText` in place of options when the list is empty, else as a trailing row. */ isOptionLoading?: boolean; /** Overrides rightIcon — replaces it with a spinner in the trailing slot whenever true. */ isLoading?: boolean; /** Text of the option-list loading row. Defaults to "Loading...". */ loadingText?: string; /** Input placeholder, hidden once a value is selected and the menu is closed. Defaults to "Select". */ placeholder?: string; /** Fully controlled selection; pass back what `onChange` emits or nothing sticks. */ value?: Options[] | Options; /** Only seeds the initial input text at mount — later prop changes don't resync the input. */ searchQuery?: string; /** Multi mode: chips, toggling rows, menu stays open; `value`/`onChange` become arrays. */ isMultiple?: boolean; /** Visual-only — shows the required asterisk on the label; doesn't block selection or submission. */ isRequired?: boolean; /** Marks the field invalid with the error border and ring; `errorMessage` text shows only while true. */ error?: boolean; /** Only rendered when `error` is true — setting this alone shows nothing. */ errorMessage?: string; /** Hidden whenever `error` is true — mutually exclusive with errorMessage. */ helperText?: string; /** Width of the whole field; the menu matches it. Defaults to "100%". */ width?: string | number; /** Only sets the insideSelect segment's height, not the main field's — that's driven by `size`. */ height?: string | number; /** Height on the field control scale (xs 32 · sm 38 · md 40 · lg 48px). Defaults to `sm`. */ size?: "xs" | "sm" | "md" | "lg"; /** Blocks opening, typing, chip removal and clearing; renders the shared disabled field look. */ isDisabled?: boolean; /** Tuning for selected-value chips; only applies when `isMultiple` is true. */ chip?: { /** Chips shown before a "+N" chip; `0` shows only the count. */ maxChips?: number; /** Truncates each chip label to this many characters, adding an ellipsis. */ maxText?: number; /** Never called — its presence alone enables each chip's remove (X) icon, which fires `onChange`. */ onClick?: (options: Options | Options[]) => void; }; /** Legacy nested form of `isInformation`/`informationMessage`; the flat props win when both are set. */ information?: { isInformation?: boolean; informationMessage?: string; }; /** Wins over `information.isInformation` when both are set; both this and informationMessage must be truthy to show the icon. */ isInformation?: boolean; /** Tooltip text on the label info icon; needs `isInformation` and a `label` to appear. */ informationMessage?: string; /** Replaces the trailing toggle chevron with a custom icon; ignored while `isLoading` shows the spinner. */ rightIcon?: { /** Must be a rendered element (``); anything else fails isValidElement and falls back to the toggle chevron. */ icon?: React.ReactNode; /** Fires on custom-icon click; the click also bubbles to the field and opens the menu. */ onClick?: () => void; }; /** Adds a footer "add new" row below the options; nothing renders unless `enabled` is true. */ addNew?: { /** Must be explicitly true — icon/text/onClick alone render nothing. */ enabled: boolean; /** Defaults to a plus icon. */ icon?: React.ReactNode; /** Defaults to "Add New". */ text?: string; /** Fires on the footer row click; the menu stays open. */ onClick?: () => void; }; /** Shows a clear-all (X) button while anything is selected; emits `[]` or `undefined`. */ isRemoveAllIcon?: boolean; /** Ignored entirely unless `isMultiple` is also true. */ isSelectAll?: boolean; /** Infinite-scroll paging: `onScroll` fires near the list end while more rows are expected. */ pagination?: { /** startIndex is 1-based (current options.length + 1), not a 0-based offset. */ onScroll?: (startIndex: number, limit: number, query: string) => void; /** Also used to guess whether more pages exist (options.length % limit === 0, refined once a page returns no new rows). Defaults to 10 when omitted. */ limit?: number; /** Set this synchronously while a page request is in flight, or fast scrolling can re-fire onScroll for the same page. */ scrollLoading?: boolean; /** Explicit stop signal; overrides the limit-based heuristic when set. */ hasMore?: boolean; /** Total row count; when set, hasMore is derived as options.length < totalRecords instead of the limit heuristic. */ totalRecords?: number; }; /** @deprecated Not implemented — the option list never virtualizes; page large datasets with `pagination` instead. */ virtualization?: boolean; /** Renders a second nested picker inside the field (e.g. a country code) beside the main input. */ insideSelect?: { /** Fires with the picked nested option; the nested picker is controlled via `insideSelect.value`. */ onSelect?: (options: Options) => void; /** In isMultiple mode, its `id` is stamped onto every newly selected main option as `insideSelectId`. */ value?: Options; /** The nested picker's own option list — distinct from the top-level `options`, singular name despite being an array. */ option?: Options[]; /** Nested trigger width; label truncates at 6rem when unset. */ width?: string; /** Shown when no value is selected. Defaults to "Select". */ placeholder?: string; }; }; /** One selectable option; view, when set, fully replaces the default label rendering for that option's row. */ /** Prefer `Select` with `native={false}` for new work — not yet a drop-in; see COMPONENT-GOTCHAS.md ("Which select component"). */ export type Options = { /** Unique key; selected state and chip removal are matched by it. */ id: string | number; /** Text shown in the row and chip, and the only text the search box filters on. */ label: string; /** Custom row content replacing the label text; search still uses `label`. */ view?: React.ReactNode; };