/** * createListbox - the HEADLESS core behind , in the same spirit as * `createSvGrid` for the grid: a runes-based state machine (roving active index, * single/multi selection, full keyboard) with **prop-getters** you spread onto * YOUR OWN markup. No styles, no DOM assumptions - render it however you like. * * ```svelte * * * ``` * * The styled is just one renderer over this core. */ import { type ListOption } from './list-option'; /** * A `Set` is accepted (and echoed back) in multi-select mode so a caller whose * selection is already set-shaped - a filter checklist whose default state is * "every value selected" - never has to materialize it as an array to drive the * listbox, or to spread it back out again on every click. */ export type ListboxValue = string | number | ReadonlyArray | ReadonlySet | null; /** Reactive inputs are passed as getters so the core tracks live prop changes * (the same controlled pattern Svelte headless libraries use). */ export type ListboxConfig = { options: () => ReadonlyArray; value: () => ListboxValue; onChange?: (value: string | number | Array | Set) => void; multiple?: () => boolean; disabled?: () => boolean; ariaLabel?: () => string | undefined; id?: () => string | undefined; invalid?: () => boolean; required?: () => boolean; error?: () => string | undefined; hint?: () => string | undefined; }; export type OptionProps = { role: 'option'; id: string; 'aria-selected': boolean; 'aria-disabled': boolean | undefined; 'data-idx': number; 'data-active': '' | undefined; 'data-selected': '' | undefined; onclick: () => void; onpointermove: () => void; }; export type ListboxRootProps = { role: 'listbox'; id: string | undefined; 'aria-multiselectable': boolean; 'aria-label': string | undefined; 'aria-disabled': boolean; 'aria-activedescendant': string | undefined; 'aria-invalid': 'true' | undefined; 'aria-required': 'true' | undefined; 'aria-describedby': string | undefined; tabindex: number; onkeydown: (e: KeyboardEvent) => void; }; export type Listbox = { /** Currently highlighted option index (roving focus). */ readonly activeIndex: number; /** Selected values as an array (single mode -> 0 or 1 entry). */ readonly selectedValues: Array; isSelected: (value: string | number) => boolean; isActive: (index: number) => boolean; setActive: (index: number) => void; /** Toggle (multi) / set (single) the option at `index`. */ pick: (index: number) => void; /** Move the active highlight by `delta`, wrapping over enabled options. */ move: (delta: number) => void; first: () => void; last: () => void; onKeydown: (e: KeyboardEvent) => void; /** Spread onto the list container element. */ rootProps: () => ListboxRootProps; /** Spread onto the option element at `index`. */ optionProps: (index: number) => OptionProps; }; /** Normalize a listbox value to an array (pure). */ export declare function toSelectedArray(value: ListboxValue, multiple: boolean): Array; /** * Membership set for a listbox value (pure). A `Set` value is reused as-is, so * `isSelected` stays O(1) without copying - the difference between a linear * scan per rendered row and a hash hit when the selection runs to thousands of * entries. */ export declare function toSelectedSet(value: ListboxValue, multiple: boolean): ReadonlySet; export declare function createListbox(config: ListboxConfig): Listbox;