/** * @fileoverview Saasflare MultiSelect — searchable, chip-rendering multi-select. * @module packages/ui/components/ui/multi-select * @package ui * @layer core * * Data-driven (`options` array), self-contained multi-select built on the SAME * stack as the single-select Combobox (Radix Popover + cmdk) with zero new * runtime dependencies. It owns its own selection / query / open state * (controlled OR uncontrolled), so consumers get select-all, max, clearable, * and chips out of the box instead of hand-wiring Combobox + Badge + useState * (the gap the Combobox JSDoc explicitly calls out as "not built in"). * * Async option loading is supported and DOCUMENTED rather than a new prop * surface: pass `loading` + an updated `options` array and debounce your fetch * off `onSearchChange`. The presence of `onSearchChange` flips cmdk to * `shouldFilter={false}` so the server is the filter; omit it for built-in * client-side fuzzy search. * * Select-all scope: operates on the CURRENTLY-FILTERED, non-disabled options * (and respects `max`), toggling between "select all" and "Clear". * * @example * import { MultiSelect, type MultiSelectOption } from "@saasflare/ui" * * const options: MultiSelectOption[] = [ * { value: "react", label: "React" }, * { value: "vue", label: "Vue" }, * { value: "svelte", label: "Svelte" }, * ] * * const [value, setValue] = React.useState([]) * */ import * as React from "react"; import { type SaasflareComponentProps } from "../../providers"; /** A selectable option in {@link MultiSelect}. */ export interface MultiSelectOption { /** Stable unique key + the value stored in `value[]`. */ value: string; /** Visible label (also the chip text + the search match target). */ label: string; /** Optional group heading; options sharing a `group` render under one cmdk group. */ group?: string; /** Disable selecting/deselecting this option. */ disabled?: boolean; /** Optional leading node (icon/avatar) rendered in the list row. */ icon?: React.ReactNode; } /** Motion/HTML event keys that collide with React's — stripped from the root div. */ type MultiSelectDomConflicts = "onDrag" | "onDragStart" | "onDragEnd" | "onAnimationStart" | "onAnimationEnd" | "onChange" | "value" | "defaultValue"; /** Props for {@link MultiSelect}. Extends the 4-axis Saasflare contract. */ export interface MultiSelectProps extends Omit, MultiSelectDomConflicts | keyof SaasflareComponentProps>, SaasflareComponentProps { /** Options to choose from. Update this array (+ `loading`) for async loading. */ options: MultiSelectOption[]; /** Controlled selected values (array of `option.value`). */ value?: string[]; /** Uncontrolled initial selection. @default [] */ defaultValue?: string[]; /** Fires on every selection change with the next value array. */ onValueChange?: (value: string[]) => void; /** Trigger placeholder when nothing is selected. @default "Select…" */ placeholder?: string; /** Search input placeholder. @default "Search…" */ searchPlaceholder?: string; /** Node shown when the query matches no option. @default "No results." */ emptyMessage?: React.ReactNode; /** Max number of selectable values. Over-limit options become non-interactive (data-disabled) and a "Max N selected" hint shows. */ max?: number; /** Show a clear-all (×) affordance on the trigger when selection is non-empty. @default true */ clearable?: boolean; /** Show a "Select all / Clear" header row (operates on the CURRENTLY FILTERED, non-disabled options). @default false */ selectAll?: boolean; /** Label for the select-all row. @default "Select all" */ selectAllLabel?: string; /** Close the popover after each pick. @default false (multi-pick stays open) */ closeOnSelect?: boolean; /** Max chip ROWS shown collapsed on the trigger before overflowing to "+N more" (HeroUI isMultiline analog). @default 1 */ maxRows?: number; /** Async: render a spinner row + aria-busy; pair with `onSearchChange` for server filtering. @default false */ loading?: boolean; /** Controlled search query (optional). */ searchValue?: string; /** Fires on query change. PRESENCE of this prop flips cmdk to `shouldFilter={false}` (server-side filtering); omit it for built-in client fuzzy search. */ onSearchChange?: (query: string) => void; /** Disable the whole control. */ disabled?: boolean; /** Forwarded to the cmdk list for ARIA. */ "aria-label"?: string; /** className on the trigger button (root data-axes div wraps it). */ className?: string; /** className on the popover content. */ contentClassName?: string; } /** * Searchable, chip-rendering multi-select on Radix Popover + cmdk. * * Resolves the four orthogonal axes (`surface` / `radius` / `animated` / * `iconWeight`) via {@link useSaasflareProps} and emits `data-surface` / * `data-radius` / `data-animated` on the root; the axes are forwarded to the * PORTALLED popover content so `surface="glass"` theming carries into the * dropdown. * * Selection is controlled via `value` or uncontrolled via `defaultValue` * (identical guard to tag-input). `onValueChange` always fires with the next * array. Select-all is CURRENT-FILTER scope and respects `max`. * * @component * @layer core * * @param {MultiSelectOption[]} options - Options to choose from. * @param {string[]} value - Controlled selected values. * @param {string[]} defaultValue - Uncontrolled initial selection. * @param {(value: string[]) => void} onValueChange - Fires on every selection change. * @param {number} max - Max selectable values; over-limit options become non-interactive. * @param {boolean} selectAll - Show a select-all / clear header row. * @param {boolean} loading - Async spinner row + aria-busy. * @param {(query: string) => void} onSearchChange - Lifts the query AND flips cmdk to server-side filtering. * @param {string} surface - Surface style override (inherits from provider when omitted). * @param {string} radius - Radius preset override (inherits from provider when omitted). * @param {string} iconWeight - Phosphor icon weight override (inherits from provider when omitted). * @param {boolean} animated - Gate motion effects (inherits from provider when omitted). * * @example * // Controlled multi-select with chips + search * const [value, setValue] = React.useState(["react"]) * * * @example * // Select-all header + max limit + collapse to "+N more" * * * @example * // Async server filtering: presence of onSearchChange disables client filter * debouncedFetch(q)} * /> */ export declare function MultiSelect({ options, value, defaultValue, onValueChange, placeholder, searchPlaceholder, emptyMessage, max, clearable, selectAll, selectAllLabel, closeOnSelect, maxRows, loading, searchValue, onSearchChange, disabled, surface, radius, animated, iconWeight, className, contentClassName, "aria-label": ariaLabel, ...props }: MultiSelectProps): React.ReactElement; export {};