kmenu
Version:
The perfect ⌘K menu
215 lines (212 loc) • 6.89 kB
text/typescript
/**
* Describes an actionable or navigable option in the command menu.
*/
interface CommandOption<T = any> {
id?: string;
label: string;
keywords?: string[];
disabled?: boolean;
group?: string;
data?: T;
children?: CommandOption<T>[];
parent?: string;
action?: () => void | Promise<void> | CommandOption<T>[];
}
/**
* Breadcrumb segment representing the current submenu path.
*/
interface Breadcrumb {
id: string;
label: string;
}
/**
* Complete immutable snapshot of the command system state.
*/
interface CommandState<T = any> {
open: boolean;
input: string;
activeId?: string;
activeIndex: number;
filtered: CommandOption<T>[];
options: CommandOption<T>[];
groups: Map<string, CommandOption<T>[]>;
menuStack: string[];
currentLevel: number;
breadcrumbs: Breadcrumb[];
allOptions: CommandOption<T>[];
currentOptions: CommandOption<T>[];
}
/**
* Discriminated union of events emitted by the command core.
*/
type CommandEvent<T = any> = {
type: "open";
} | {
type: "close";
} | {
type: "change";
input: string;
} | {
type: "select";
option: CommandOption<T>;
} | {
type: "navigate";
activeId: string | undefined;
activeIndex: number;
} | {
type: "submenu";
option: CommandOption<T>;
level: number;
} | {
type: "back";
level: number;
};
/** Handler for a specific command event type. */
type EventHandler<T = any> = (event: CommandEvent<T>) => void;
/** Function that filters `options` using a text `query`. */
type FilterFunction<T = any> = (options: CommandOption<T>[], query: string) => CommandOption<T>[];
/** Callback to unsubscribe an event handler. */
type Unsubscribe = () => void;
/**
* Configuration for the `CommandCore` behavior and callbacks.
*/
interface CommandCoreConfig<T = any> {
filter?: FilterFunction<T>;
onOpen?: () => void;
onClose?: () => void;
onChange?: (input: string) => void;
onSelect?: (option: CommandOption<T>) => void;
}
/**
* Headless command menu engine with filtering, navigation, and submenu support.
* Provides ARIA props and emits events for UI integration.
*/
declare class CommandCore<T = any> {
private stateMachine;
private state;
private listeners;
private filter;
private keydownHandler?;
private globalKeyHandler?;
private inputElement?;
private listElement?;
private optionElements;
private destroyed;
private lastScrollTime;
constructor(config?: CommandCoreConfig<T>);
private setupStateMachine;
private setupGlobalKeyboardShortcut;
private defaultFilter;
/** Open the command menu if not destroyed. */
open(): void;
/** Close the command menu if not destroyed. */
close(): void;
/** Toggle the command menu open/closed state. */
toggle(): void;
/**
* Set the input query and update the filtered options.
* Emits a `change` event with the latest input.
*/
setInput(value: string): void;
/**
* Replace the registered options and rebuild all derived state.
*/
registerOptions(options: CommandOption<T>[]): void;
private processOptionsWithIds;
private ensureUniqueId;
private rebuildGroups;
private reorderByGroups;
private flattenOptions;
/**
* Set the active option by filtered index. Optionally provide the
* navigation direction to optimize scroll behavior.
*/
setActiveByIndex(index: number, direction?: "up" | "down"): void;
private scrollActiveIntoView;
/** Set the active option by option id. */
setActiveById(id: string): void;
/** Move the active selection to the previous enabled option. */
navigateUp(): void;
/** Move the active selection to the next enabled option. */
navigateDown(): void;
/** Subscribe to a command event. Returns an unsubscribe function. */
on(event: CommandEvent<T>["type"], handler: EventHandler<T>): Unsubscribe;
/** Get a shallow-copied snapshot of the current state. */
getState(): CommandState<T>;
private emit;
private updateFiltered;
private selectFirstAvailableOption;
/** Enter a submenu using the given parent `option` with children. */
enterSubmenu(option: CommandOption<T>): void;
/** Go back one submenu level. Returns `false` if already at root. */
goBack(): boolean;
private findOptionById;
private updateAriaActiveDescendant;
/**
* Return ARIA attributes for the combobox container element.
*/
getComboboxProps(): {
role: "combobox";
"aria-expanded": boolean;
"aria-haspopup": "listbox";
"aria-controls": string;
};
/**
* Return ARIA attributes and event handlers for the input element.
*/
getInputProps(): {
ref: (el: HTMLInputElement | null) => void;
role: "combobox";
"aria-autocomplete": "list";
"aria-expanded": boolean;
"aria-controls": string;
"aria-activedescendant": string | undefined;
value: string;
onInput: (e: Event) => void;
onKeyDown: (e: KeyboardEvent) => void;
};
/** Return ARIA attributes for the listbox element. */
getListboxProps(): {
ref: (el: HTMLElement | null) => void;
id: string;
role: "listbox";
"aria-label": string;
tabIndex: number;
};
/**
* Return ARIA attributes and event handlers for an option element by id.
*/
getOptionProps(id: string): {
ref: (el: HTMLElement | null) => void;
id: string;
role: "option";
"aria-selected": boolean;
"aria-disabled": boolean | undefined;
tabIndex: number;
onClick: () => void;
onMouseEnter: () => void;
};
private handleKeyDown;
/** Select the active option or enter its submenu if it has children. */
selectActive(): void;
/** Cleanup event listeners and internal references. Safe to call multiple times. */
destroy(): void;
}
/**
* Case-insensitive contains match across label and keywords.
*/
declare const simpleFilter: FilterFunction;
/**
* Case-insensitive starts-with match across label and keywords.
*/
declare const startsWithFilter: FilterFunction;
/**
* Simple fuzzy matcher that rewards consecutive and word-boundary matches.
*/
declare const fuzzyFilter: FilterFunction;
/**
* Build a filter using a regex pattern where `{{query}}` is replaced
* by the user's query at evaluation time.
*/
declare function createRegexFilter(pattern: string, flags?: string): FilterFunction;
export { CommandCore as C, type EventHandler as E, type FilterFunction as F, type Unsubscribe as U, type CommandOption as a, type CommandState as b, type CommandEvent as c, type CommandCoreConfig as d, startsWithFilter as e, fuzzyFilter as f, createRegexFilter as g, simpleFilter as s };