@ailuracode/alpine-command 0.1.0 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,26 +1,135 @@
1
1
  # @ailuracode/alpine-command
2
2
 
3
- Headless command palette store for Alpine.js — searchable actions, keyboard navigation, groups, and shortcuts. Compose optionally with dialog and toast plugins.
4
-
5
- **[Full documentation →](../../docs/plugins/command.md)**
3
+ Headless command palette (Spotlight-style) store — searchable actions, groups, keyboard navigation, nested pages, async execution, aliases, and ARIA helpers.
6
4
 
7
5
  ## Install
8
6
 
9
7
  ```bash
10
- npm install @ailuracode/alpine-command alpinejs
8
+ pnpm add @ailuracode/alpine-command @ailuracode/alpine-core alpinejs
11
9
  ```
12
10
 
13
- ## Store API
11
+ Active item navigation uses inline helpers — no extra dependency.
12
+
13
+ ## Quick start
14
14
 
15
15
  ```js
16
- $store.command.open();
17
- $store.command.search = "theme";
18
- $store.command.register({
16
+ import Alpine from "alpinejs";
17
+ import { commandPlugin } from "@ailuracode/alpine-command";
18
+
19
+ Alpine.plugin(
20
+ commandPlugin({
21
+ searchStrategy: "substring",
22
+ onRun(item) {
23
+ console.log("Ran", item.id);
24
+ },
25
+ persistence: {
26
+ maxRecent: 8,
27
+ getRecent: () => JSON.parse(localStorage.getItem("recent-commands") ?? "[]"),
28
+ setRecent: (ids) => localStorage.setItem("recent-commands", JSON.stringify(ids)),
29
+ },
30
+ })
31
+ );
32
+ Alpine.start();
33
+ ```
34
+
35
+ ## Store API
36
+
37
+ | Member | Description |
38
+ |--------|-------------|
39
+ | `open()` / `close()` / `toggle()` | Palette visibility |
40
+ | `isOpen` | Whether the palette is open |
41
+ | `search` | Reactive filter string |
42
+ | `activeIndex` | Keyboard-highlighted row |
43
+ | `filteredItems` / `visibleItems` | Visible commands for the current page |
44
+ | `groupedItems` | Filtered items grouped by `group` |
45
+ | `register(item)` | Register an action; returns `unregister()` |
46
+ | `run(id)` / `cancelRun()` | Execute or cancel in-flight async work |
47
+ | `pushPage(page)` / `goBack()` | Nested command pages |
48
+ | `executionState` / `runningId` | Async execution state |
49
+ | `inputProps()` / `listboxProps()` / `optionProps(id)` | Headless ARIA props |
50
+ | `handleKeydown(event)` | Typing, Backspace, Arrow/Home/End/Enter/Escape |
51
+
52
+ ### Command item
53
+
54
+ ```ts
55
+ {
19
56
  id: "toggle-theme",
20
57
  label: "Toggle theme",
21
- group: "Appearance",
22
- shortcut: "⌘K",
58
+ group?: "Appearance",
59
+ shortcut?: "⌘K",
60
+ keywords?: ["dark", "light"],
61
+ aliases?: ["spotlight"],
62
+ disabled?: false | (() => boolean),
63
+ hidden?: false | (() => boolean),
64
+ enabled?: true | (() => boolean),
65
+ pinned?: false,
66
+ page?: "root",
67
+ load?: async () => {},
23
68
  action: () => {},
24
- });
25
- $store.command.run("toggle-theme");
69
+ }
26
70
  ```
71
+
72
+ ### Search
73
+
74
+ - Default strategy: substring ranking on label, aliases, keywords, group, and shortcut
75
+ - `searchStrategy: "fuzzy"` enables lightweight fuzzy matching
76
+ - `rank(item, search)` replaces the deprecated `filter(item, search)` boolean API
77
+
78
+ Disabled commands remain visible unless `hidden` is true. Keyboard navigation and `run()` skip disabled or loading commands.
79
+
80
+ ## Plugin options
81
+
82
+ | Option | Default | Description |
83
+ |--------|---------|-------------|
84
+ | `id` | auto-generated | Controller identifier |
85
+ | `rank` / `searchStrategy` | substring | Ranker for the filter pipeline |
86
+ | `persistence` | — | Recent/pinned hooks (maxRecent, getRecent, setRecent, getPinned, setPinned) |
87
+ | `storeKey` | `'command'` | `$store` key the Alpine plugin registers under |
88
+ | `magicKey` | `'command'` (or `storeKey` when renamed) | `$command` magic key the Alpine plugin registers under |
89
+
90
+ ### Avoiding name collisions
91
+
92
+ If your application already owns a `$store.command` or another toolkit plugin registers on that name, rename the integration surface without touching the controller:
93
+
94
+ ```ts
95
+ Alpine.plugin(commandPlugin({
96
+ storeKey: "palette", // → $store.palette
97
+ // magicKey follows storeKey by default → $palette
98
+ magicKey: "cmd", // explicit override → $cmd
99
+ }));
100
+ ```
101
+
102
+ `storeKey` is the only argument most hosts need. `magicKey` moves independently only when both names must be freed. The exposed constants `DEFAULT_COMMAND_STORE_KEY` and `DEFAULT_COMMAND_MAGIC_KEY` keep the rename discoverable from TypeScript.
103
+
104
+ ## Integration
105
+
106
+ - **Overlay** — optional `overlayId` documents a palette layer id for `$store.overlay.zIndexOf(overlayId, layer)`
107
+ - **Scroll** — pass `scroll: $store.scroll` to lock page scroll while open (enabled by default when provided)
108
+ - **Keyboard** — global open shortcuts remain consumer-owned; compose with `@ailuracode/alpine-keyboard` when needed
109
+ - **Dialog / Toast** — render the palette in a dialog panel or call `$toast()` from `action` / `onRun`
110
+
111
+ Neither overlay, scroll, keyboard, dialog, nor toast is required.
112
+
113
+ ## SSR
114
+
115
+ Register commands on the client. The controller does not touch browser globals during import or construction.
116
+
117
+ ## Standalone usage
118
+
119
+ ```ts
120
+ import { createCommandController } from "@ailuracode/alpine-command";
121
+
122
+ const command = createCommandController();
123
+ command.register({ id: "save", label: "Save", action: () => {} });
124
+ command.open();
125
+ ```
126
+
127
+ ## Migration notes
128
+
129
+ - `register()` now returns an unregister callback; `unregister(id)` remains available
130
+ - `filter` is deprecated in favor of `rank` or `searchStrategy`
131
+ - `filteredItems` now includes disabled commands; use `itemState(id)?.disabled` or `visibleItems` for runtime state
132
+
133
+ ## License
134
+
135
+ MIT
package/dist/global.d.ts CHANGED
@@ -1,47 +1,69 @@
1
1
  /// <reference types="@types/alpinejs" />
2
2
 
3
- export type CommandAction = () => void | Promise<void>;
3
+ import type {
4
+ CommandExecutionState,
5
+ CommandItem,
6
+ CommandItemState,
7
+ CommandPage,
8
+ CommandStoreConfig,
9
+ } from "./types";
4
10
 
5
- export type CommandItem = {
6
- id: string;
7
- label: string;
8
- group?: string;
9
- shortcut?: string;
10
- keywords?: string[];
11
- disabled?: boolean;
12
- action: CommandAction;
13
- };
11
+ export type {
12
+ CommandAction,
13
+ CommandExecutionState,
14
+ CommandFilterFn,
15
+ CommandItem,
16
+ CommandItemState,
17
+ CommandLoader,
18
+ CommandPage,
19
+ CommandPersistence,
20
+ CommandPredicate,
21
+ CommandRankFn,
22
+ CommandSearchStrategy,
23
+ CommandStoreConfig,
24
+ } from "./types";
14
25
 
15
26
  export interface CommandStore {
16
27
  search: string;
17
28
  activeIndex: number;
18
29
  visible: boolean;
19
30
  items: Record<string, CommandItem>;
20
- readonly isOpen: boolean;
31
+ isOpen: boolean;
32
+ executionState: CommandExecutionState;
33
+ runningId: string | null;
34
+ currentPageId: string;
35
+ pageStack: string[];
36
+ pages: Record<string, CommandPage>;
37
+ loadingIds: string[];
38
+ pinnedIds: string[];
39
+ recentIds: string[];
21
40
  open(): void;
22
41
  close(): void;
23
42
  toggle(): void;
24
- register(item: CommandItem): void;
43
+ register(item: CommandItem): () => void;
25
44
  unregister(id: string): void;
26
45
  run(id: string): Promise<void>;
46
+ cancelRun(): void;
27
47
  handleKeydown(event: KeyboardEvent): void;
28
- readonly filteredItems: CommandItem[];
29
- readonly groupedItems: Record<string, CommandItem[]>;
48
+ pushPage(page: CommandPage): Promise<void>;
49
+ popPage(): void;
50
+ goBack(): void;
51
+ itemState(id: string): CommandItemState | null;
52
+ inputProps(): Record<string, string | boolean | undefined>;
53
+ listboxProps(): Record<string, string | boolean | undefined>;
54
+ optionProps(id: string): Record<string, string | number | boolean | undefined>;
55
+ filteredItems: CommandItem[];
56
+ visibleItems: CommandItemState[];
57
+ groupedItems: Record<string, CommandItem[]>;
30
58
  destroy(): void;
31
59
  }
32
60
 
33
- export interface CommandPluginOptions {
34
- onOpen?: () => void;
35
- onClose?: () => void;
36
- onRun?: (item: CommandItem) => void;
37
- filter?: (item: CommandItem, search: string) => boolean;
38
- }
39
-
40
- export function commandOptions<const T extends CommandPluginOptions>(options: T): T;
41
- export function createCommandStore(options?: CommandPluginOptions): CommandStore;
61
+ export function createCommandController(
62
+ config?: CommandStoreConfig
63
+ ): import("./controller").CommandController;
42
64
 
43
65
  export default function commandPlugin(
44
- options?: CommandPluginOptions
66
+ options?: import("./types").CommandPluginOptions
45
67
  ): import("alpinejs").PluginCallback;
46
68
 
47
69
  declare global {
package/dist/index.d.ts CHANGED
@@ -1,58 +1,281 @@
1
- import AlpineType from 'alpinejs';
1
+ import { Alpine, PluginCallback, BaseController } from '@ailuracode/alpine-core';
2
+ export { Unsubscribe } from '@ailuracode/alpine-core';
3
+ import { ScrollStore } from '@ailuracode/alpine-scroll';
4
+ import { Alpine as Alpine$1 } from 'alpinejs';
2
5
 
6
+ /**
7
+ * Public type contracts for `@ailuracode/alpine-command`.
8
+ */
9
+
10
+ /** Action callback invoked when a command is executed. */
3
11
  type CommandAction = () => void | Promise<void>;
12
+ /** Static or reactive predicate for command state. */
13
+ type CommandPredicate = boolean | (() => boolean);
14
+ /** Async loader for lazy command registration. */
15
+ type CommandLoader = () => void | Promise<void>;
16
+ /** Search ranking strategy. */
17
+ type CommandSearchStrategy = "substring" | "fuzzy";
18
+ /** Rank function — higher scores surface earlier; `null` excludes the item. */
19
+ type CommandRankFn = (item: CommandItem, search: string) => number | null;
20
+ /**
21
+ * @deprecated Use {@link CommandRankFn} via `rank` or `searchStrategy` instead.
22
+ * Legacy boolean filter kept for backward compatibility.
23
+ */
24
+ type CommandFilterFn = (item: CommandItem, search: string) => boolean;
25
+ /** Optional recent/pinned persistence hooks. */
26
+ type CommandPersistence = {
27
+ readonly maxRecent?: number;
28
+ readonly getRecent?: () => readonly string[] | Promise<readonly string[]>;
29
+ readonly setRecent?: (ids: readonly string[]) => void | Promise<void>;
30
+ readonly getPinned?: () => readonly string[] | Promise<readonly string[]>;
31
+ readonly setPinned?: (ids: readonly string[]) => void | Promise<void>;
32
+ };
33
+ /** Nested command page metadata. */
34
+ type CommandPage = {
35
+ readonly id: string;
36
+ readonly title: string;
37
+ readonly parentId?: string;
38
+ readonly load?: CommandLoader;
39
+ };
40
+ /** A registered command item. */
4
41
  type CommandItem = {
5
- id: string;
6
- label: string;
7
- group?: string;
8
- shortcut?: string;
9
- keywords?: string[];
10
- disabled?: boolean;
11
- action: CommandAction;
42
+ readonly id: string;
43
+ readonly label: string;
44
+ readonly group?: string;
45
+ readonly shortcut?: string;
46
+ readonly keywords?: string[];
47
+ readonly aliases?: string[];
48
+ readonly disabled?: CommandPredicate;
49
+ readonly hidden?: CommandPredicate;
50
+ readonly enabled?: CommandPredicate;
51
+ readonly pinned?: boolean;
52
+ readonly page?: string;
53
+ readonly load?: CommandLoader;
54
+ readonly action: CommandAction;
55
+ };
56
+ /** Resolved runtime state for a visible command row. */
57
+ type CommandItemState = {
58
+ readonly id: string;
59
+ readonly item: CommandItem;
60
+ readonly disabled: boolean;
61
+ readonly hidden: boolean;
62
+ readonly loading: boolean;
63
+ readonly pinned: boolean;
64
+ readonly recent: boolean;
65
+ readonly rank: number;
66
+ readonly selectable: boolean;
12
67
  };
13
- type CommandStore = {
68
+ /** Explicit async lifecycle state for the palette. */
69
+ type CommandExecutionState = "idle" | "loading" | "running";
70
+ /** Config callbacks and behavior for the command palette. */
71
+ type CommandStoreConfig = {
72
+ readonly onOpen?: () => void;
73
+ readonly onClose?: () => void;
74
+ readonly onRun?: (item: CommandItem) => void;
75
+ /** @deprecated Use `rank` or `searchStrategy` instead. */
76
+ readonly filter?: CommandFilterFn;
77
+ readonly rank?: CommandRankFn;
78
+ readonly searchStrategy?: CommandSearchStrategy;
79
+ readonly persistence?: CommandPersistence;
80
+ readonly overlayId?: string;
81
+ readonly editableSelector?: string;
82
+ readonly idPrefix?: string;
83
+ readonly closeOnRun?: boolean;
84
+ readonly scroll?: ScrollStore;
85
+ readonly scrollLock?: boolean;
86
+ };
87
+ /** Normalized options used internally by the controller. */
88
+ type NormalizedCommandOptions = {
89
+ readonly id?: string;
90
+ readonly onOpen?: () => void;
91
+ readonly onClose?: () => void;
92
+ readonly onRun?: (item: CommandItem) => void;
93
+ readonly rank: CommandRankFn;
94
+ readonly searchStrategy: CommandSearchStrategy;
95
+ readonly persistence: Required<Pick<CommandPersistence, "maxRecent">> & Omit<CommandPersistence, "maxRecent">;
96
+ readonly overlayId?: string;
97
+ readonly editableSelector: string;
98
+ readonly idPrefix: string;
99
+ readonly closeOnRun: boolean;
100
+ readonly scroll?: ScrollStore;
101
+ readonly scrollLock: boolean;
102
+ };
103
+ /** Options accepted by the command plugin factory. */
104
+ interface CommandPluginOptions extends CommandStoreConfig {
105
+ readonly id?: string;
106
+ /**
107
+ * `$store` key the Alpine plugin registers under. Defaults to
108
+ * {@link DEFAULT_COMMAND_STORE_KEY}. Set when the host already owns
109
+ * a `command` store or another toolkit plugin would collide on that
110
+ * name — the rename avoids the collision without touching the
111
+ * controller. Ignored by the standalone `createCommandStore` factory.
112
+ */
113
+ readonly storeKey?: string;
114
+ /**
115
+ * `$command` magic key the Alpine plugin registers under. Defaults
116
+ * to {@link DEFAULT_COMMAND_MAGIC_KEY}, or to `storeKey` when that
117
+ * is renamed (the magic follows the store so consumers only rename
118
+ * one). Ignored by the standalone factory.
119
+ */
120
+ readonly magicKey?: string;
121
+ }
122
+ /** Default `$store` key registered by {@link commandPlugin}. */
123
+ declare const DEFAULT_COMMAND_STORE_KEY = "command";
124
+ /** Default `$command` magic key registered by {@link commandPlugin}. */
125
+ declare const DEFAULT_COMMAND_MAGIC_KEY = "command";
126
+ /** Alpine-facing store surface. */
127
+ interface CommandStore {
14
128
  search: string;
15
129
  activeIndex: number;
16
- /** Whether the palette is visible. */
17
130
  visible: boolean;
18
- /** Registered command items. */
19
131
  items: Record<string, CommandItem>;
20
- readonly isOpen: boolean;
132
+ isOpen: boolean;
133
+ executionState: CommandExecutionState;
134
+ runningId: string | null;
135
+ currentPageId: string;
136
+ pageStack: string[];
137
+ pages: Record<string, CommandPage>;
138
+ loadingIds: string[];
139
+ pinnedIds: string[];
140
+ recentIds: string[];
21
141
  open(): void;
22
142
  close(): void;
23
143
  toggle(): void;
24
- register(item: CommandItem): void;
144
+ register(item: CommandItem): () => void;
25
145
  unregister(id: string): void;
26
146
  run(id: string): Promise<void>;
147
+ cancelRun(): void;
27
148
  handleKeydown(event: KeyboardEvent): void;
28
- readonly filteredItems: CommandItem[];
29
- readonly groupedItems: Record<string, CommandItem[]>;
149
+ pushPage(page: CommandPage): Promise<void>;
150
+ popPage(): void;
151
+ goBack(): void;
152
+ itemState(id: string): CommandItemState | null;
153
+ inputProps(): Record<string, string | boolean | undefined>;
154
+ listboxProps(): Record<string, string | boolean | undefined>;
155
+ optionProps(id: string): Record<string, string | number | boolean | undefined>;
156
+ filteredItems: CommandItem[];
157
+ visibleItems: CommandItemState[];
158
+ groupedItems: Record<string, CommandItem[]>;
30
159
  destroy(): void;
160
+ }
161
+ /** Typed view of `Alpine` the command plugin uses internally. */
162
+ type CommandAlpine = Alpine<{
163
+ command: CommandStore;
164
+ }> & {
165
+ cleanup?(callback: () => void): void;
31
166
  };
32
- type CommandStoreConfig = {
33
- onOpen?: () => void;
34
- onClose?: () => void;
35
- onRun?: (item: CommandItem) => void;
36
- filter?: (item: CommandItem, search: string) => boolean;
37
- };
38
- /** Creates the headless command palette store. */
167
+ /** `Alpine.plugin()` callback signature. */
168
+ type CommandPluginCallback = PluginCallback<Alpine$1>;
169
+
170
+ /**
171
+ * Strongly-typed event map for the command controller.
172
+ */
173
+
174
+ /**
175
+ * Event map for command palette state changes.
176
+ * `open` — palette opened.
177
+ * `close` — palette closed.
178
+ * `run` — a command was executed.
179
+ * `change` — reactive state changed (search, selection, pages, loading).
180
+ */
181
+ interface CommandEvents extends Record<string, unknown> {
182
+ open: undefined;
183
+ close: undefined;
184
+ run: CommandItem;
185
+ change: undefined;
186
+ }
187
+
188
+ /**
189
+ * Command palette controller — the framework-agnostic core of
190
+ * `@ailuracode/alpine-command`.
191
+ */
192
+
193
+ /**
194
+ * Headless command palette controller with search ranking, nested pages,
195
+ * async loading/execution, and ARIA helpers.
196
+ */
197
+ declare class CommandController extends BaseController<CommandEvents> {
198
+ #private;
199
+ constructor(id?: string, config?: CommandStoreConfig);
200
+ get items(): Readonly<Record<string, CommandItem>>;
201
+ get search(): string;
202
+ set search(value: string);
203
+ get activeIndex(): number;
204
+ set activeIndex(value: number);
205
+ get visible(): boolean;
206
+ get isOpen(): boolean;
207
+ get executionState(): CommandExecutionState;
208
+ get runningId(): string | null;
209
+ get currentPageId(): string;
210
+ get pageStack(): readonly string[];
211
+ get pages(): Readonly<Record<string, CommandPage>>;
212
+ get loadingIds(): readonly string[];
213
+ get pinnedIds(): readonly string[];
214
+ get recentIds(): readonly string[];
215
+ get visibleItems(): readonly CommandItemState[];
216
+ get filteredItems(): CommandItem[];
217
+ get groupedItems(): Record<string, CommandItem[]>;
218
+ open(): void;
219
+ close(): void;
220
+ toggle(): void;
221
+ register(item: CommandItem): () => void;
222
+ unregister(id: string): void;
223
+ pushPage(page: CommandPage): Promise<void>;
224
+ popPage(): void;
225
+ goBack(): void;
226
+ itemState(id: string): CommandItemState | null;
227
+ inputProps(): Record<string, string | boolean | undefined>;
228
+ listboxProps(): Record<string, string | boolean | undefined>;
229
+ optionProps(id: string): Record<string, string | number | boolean | undefined>;
230
+ cancelRun(): void;
231
+ run(id: string): Promise<void>;
232
+ handleKeydown(event: KeyboardEvent): void;
233
+ toStore(): CommandStore;
234
+ destroy(): void;
235
+ }
236
+ /** Creates a CommandController instance. */
237
+ declare function createCommandController(config?: CommandStoreConfig): CommandController;
238
+ /** Creates a CommandStore (store-shaped object) directly. */
39
239
  declare function createCommandStore(config?: CommandStoreConfig): CommandStore;
40
240
 
41
- interface CommandPluginOptions extends CommandStoreConfig {
241
+ /**
242
+ * Alpine adapter for the command controller.
243
+ *
244
+ * Fields are direct properties (not getters) so the plugin can mirror
245
+ * controller state onto Alpine's reactive store proxy.
246
+ */
247
+
248
+ /** Mirrors controller state into a plain store object for Alpine. */
249
+ declare function syncCommandStore(store: CommandStore, controller: CommandController): void;
250
+ /** Builds the {@link CommandStore} installed at `$store.command`. */
251
+ declare function createCommandAlpineStore(controller: CommandController): CommandStore;
252
+
253
+ type CommandErrorCode = "COMMAND_DESTROYED" | "COMMAND_DUPLICATE_ID" | "COMMAND_UNKNOWN_ITEM" | "COMMAND_UNKNOWN_PAGE" | "COMMAND_ALREADY_RUNNING";
254
+ declare class CommandError extends Error {
255
+ readonly code: CommandErrorCode;
256
+ readonly cause?: unknown;
257
+ constructor(message: string, code: CommandErrorCode, options?: {
258
+ cause?: unknown;
259
+ });
42
260
  }
261
+
262
+ declare const ROOT_PAGE_ID = "root";
263
+ /** Normalizes command controller and plugin options once at construction. */
264
+ declare function normalizeCommandOptions(options?: CommandPluginOptions): NormalizedCommandOptions;
265
+
266
+ /**
267
+ * Alpine.js integration for `@ailuracode/alpine-command`.
268
+ */
269
+
270
+ /**
271
+ * Plugin factory — returns the `Alpine.plugin()` callback. Pass
272
+ * {@link CommandPluginOptions} to configure the controller,
273
+ * or `{}` for the package defaults.
274
+ */
275
+ declare function commandPlugin(options?: CommandPluginOptions): CommandPluginCallback;
43
276
  /** Builds typed command plugin options. */
44
277
  declare function commandOptions<const T extends CommandPluginOptions>(options: T): T;
45
- /** Alpine.js command palette plugin. Registers `$store.command`. */
46
- declare function commandPlugin(options?: CommandPluginOptions): AlpineType.PluginCallback;
47
- declare global {
48
- namespace Alpine {
49
- interface Stores {
50
- command: CommandStore;
51
- }
52
- interface Magics<T> {
53
- $command: CommandStore;
54
- }
55
- }
56
- }
278
+ /** @deprecated Use {@link createCommandAlpineStore} via the plugin adapter. */
279
+ declare function createCommandStoreFromController(controller: CommandController): CommandStore;
57
280
 
58
- export { type CommandAction, type CommandItem, type CommandPluginOptions, type CommandStore, type CommandStoreConfig, commandOptions, createCommandStore, commandPlugin as default };
281
+ export { type CommandAction, type CommandAlpine, CommandController, CommandError, type CommandErrorCode, type CommandEvents, type CommandExecutionState, type CommandFilterFn, type CommandItem, type CommandItemState, type CommandLoader, type CommandPage, type CommandPersistence, type CommandPluginCallback, type CommandPluginOptions, type CommandPredicate, type CommandRankFn, type CommandSearchStrategy, type CommandStore, type CommandStoreConfig, DEFAULT_COMMAND_MAGIC_KEY, DEFAULT_COMMAND_STORE_KEY, type NormalizedCommandOptions, ROOT_PAGE_ID, commandOptions, commandPlugin, createCommandAlpineStore, createCommandController, createCommandStore, createCommandStoreFromController, commandPlugin as default, normalizeCommandOptions, syncCommandStore };
package/dist/index.js CHANGED
@@ -1,173 +1 @@
1
- // src/store.ts
2
- function defaultFilter(item, search) {
3
- if (!search.trim()) {
4
- return true;
5
- }
6
- const query = search.trim().toLowerCase();
7
- const haystack = [item.label, item.group ?? "", item.shortcut ?? "", ...item.keywords ?? []].join(" ").toLowerCase();
8
- return haystack.includes(query);
9
- }
10
- function isEditableTarget(target) {
11
- if (!(target instanceof HTMLElement)) {
12
- return false;
13
- }
14
- const tag = target.tagName;
15
- return tag === "INPUT" || tag === "TEXTAREA" || target.isContentEditable;
16
- }
17
- function isTypingKey(event) {
18
- return event.key.length === 1 && !event.metaKey && !event.ctrlKey && !event.altKey && !event.isComposing;
19
- }
20
- function handlePaletteTyping(store, event) {
21
- if (isEditableTarget(event.target)) {
22
- return false;
23
- }
24
- if (event.key === "Backspace") {
25
- event.preventDefault();
26
- store.search = store.search.slice(0, -1);
27
- store.activeIndex = 0;
28
- return true;
29
- }
30
- if (isTypingKey(event)) {
31
- event.preventDefault();
32
- store.search += event.key;
33
- store.activeIndex = 0;
34
- return true;
35
- }
36
- return false;
37
- }
38
- function createCommandStore(config = {}) {
39
- const filter = config.filter ?? defaultFilter;
40
- const store = {
41
- search: "",
42
- activeIndex: 0,
43
- visible: false,
44
- items: {},
45
- get isOpen() {
46
- return this.visible;
47
- },
48
- get filteredItems() {
49
- const list = Object.values(this.items).filter(
50
- (item) => !item.disabled && filter(item, this.search)
51
- );
52
- if (this.activeIndex >= list.length) {
53
- this.activeIndex = Math.max(list.length - 1, 0);
54
- }
55
- return list;
56
- },
57
- get groupedItems() {
58
- const groups = {};
59
- for (const item of this.filteredItems) {
60
- const group = item.group ?? "General";
61
- groups[group] ??= [];
62
- groups[group].push(item);
63
- }
64
- return groups;
65
- },
66
- open() {
67
- if (this.visible) {
68
- return;
69
- }
70
- this.visible = true;
71
- this.search = "";
72
- this.activeIndex = 0;
73
- config.onOpen?.();
74
- },
75
- close() {
76
- if (!this.visible) {
77
- return;
78
- }
79
- this.visible = false;
80
- this.search = "";
81
- this.activeIndex = 0;
82
- config.onClose?.();
83
- },
84
- toggle() {
85
- if (this.visible) {
86
- this.close();
87
- } else {
88
- this.open();
89
- }
90
- },
91
- register(item) {
92
- this.items[item.id] = item;
93
- },
94
- unregister(id) {
95
- delete this.items[id];
96
- },
97
- async run(id) {
98
- const item = this.items[id];
99
- if (!item || item.disabled) {
100
- return;
101
- }
102
- await item.action();
103
- config.onRun?.(item);
104
- this.close();
105
- },
106
- handleKeydown(event) {
107
- if (!this.visible) {
108
- return;
109
- }
110
- const list = this.filteredItems;
111
- if (handlePaletteTyping(this, event)) {
112
- return;
113
- }
114
- switch (event.key) {
115
- case "ArrowDown":
116
- event.preventDefault();
117
- this.activeIndex = list.length === 0 ? 0 : (this.activeIndex + 1) % list.length;
118
- break;
119
- case "ArrowUp":
120
- event.preventDefault();
121
- this.activeIndex = list.length === 0 ? 0 : (this.activeIndex - 1 + list.length) % list.length;
122
- break;
123
- case "Home":
124
- event.preventDefault();
125
- this.activeIndex = 0;
126
- break;
127
- case "End":
128
- event.preventDefault();
129
- this.activeIndex = Math.max(list.length - 1, 0);
130
- break;
131
- case "Enter": {
132
- event.preventDefault();
133
- const active = list[this.activeIndex];
134
- if (active) {
135
- void this.run(active.id);
136
- }
137
- break;
138
- }
139
- case "Escape":
140
- event.preventDefault();
141
- this.close();
142
- break;
143
- default:
144
- break;
145
- }
146
- },
147
- destroy() {
148
- this.items = {};
149
- this.visible = false;
150
- this.search = "";
151
- this.activeIndex = 0;
152
- }
153
- };
154
- return store;
155
- }
156
-
157
- // src/index.ts
158
- function commandOptions(options) {
159
- return options;
160
- }
161
- function commandPlugin(options = {}) {
162
- return function registerCommand(Alpine) {
163
- const store = createCommandStore(options);
164
- Alpine.store("command", store);
165
- Alpine.magic("command", () => Alpine.store("command"));
166
- };
167
- }
168
- export {
169
- commandOptions,
170
- createCommandStore,
171
- commandPlugin as default
172
- };
173
- //# sourceMappingURL=index.js.map
1
+ function c(t,e){t.search=e.search,t.activeIndex=e.activeIndex,t.visible=e.visible,t.isOpen=e.isOpen,t.items={...e.items},t.executionState=e.executionState,t.runningId=e.runningId,t.currentPageId=e.currentPageId,Object.assign(t,{pageStack:[...e.pageStack],pages:{...e.pages},loadingIds:[...e.loadingIds],pinnedIds:[...e.pinnedIds],recentIds:[...e.recentIds],visibleItems:[...e.visibleItems],filteredItems:[...e.filteredItems],groupedItems:{...e.groupedItems}})}function m(t){return{search:t.search,activeIndex:t.activeIndex,visible:t.visible,isOpen:t.isOpen,items:{...t.items},executionState:t.executionState,runningId:t.runningId,currentPageId:t.currentPageId,pageStack:[...t.pageStack],pages:{...t.pages},loadingIds:[...t.loadingIds],pinnedIds:[...t.pinnedIds],recentIds:[...t.recentIds],visibleItems:[...t.visibleItems],filteredItems:[...t.filteredItems],groupedItems:{...t.groupedItems},open:()=>t.open(),close:()=>t.close(),toggle:()=>t.toggle(),register:n=>t.register(n),unregister:n=>t.unregister(n),run:n=>t.run(n),cancelRun:()=>t.cancelRun(),handleKeydown:n=>t.handleKeydown(n),pushPage:n=>t.pushPage(n),popPage:()=>t.popPage(),goBack:()=>t.goBack(),itemState:n=>t.itemState(n),inputProps:()=>t.inputProps(),listboxProps:()=>t.listboxProps(),optionProps:n=>t.optionProps(n),destroy:()=>t.destroy()}}import{BaseController as F,generateId as j}from"@ailuracode/alpine-core";function S(t,e){let n=t.target;return n instanceof Element?n.closest(e)?!0:n instanceof HTMLElement&&n.isContentEditable:!1}function v(t){return t.key.length===1&&!t.metaKey&&!t.ctrlKey&&!t.altKey&&!t.isComposing}var l=class extends Error{code;cause;constructor(e,n,i={}){super(e,{cause:i.cause}),this.name="CommandError",this.code=n,this.cause=i.cause}};function T(t){return[t.label,t.group??"",t.shortcut??"",...t.keywords??[],...t.aliases??[]]}function k(t,e){let n=e.trim().toLowerCase();if(!n)return 0;let i=t.label.toLowerCase();if(i.startsWith(n))return 100;if(i.includes(n))return 80;for(let s of t.aliases??[]){let r=s.toLowerCase();if(r.startsWith(n))return 70;if(r.includes(n))return 60}return T(t).join(" ").toLowerCase().includes(n)?50:null}function N(t,e){let n=e.trim().toLowerCase();if(!n)return 0;let i=t.label.toLowerCase(),o=0,s=0;for(let r=0;r<i.length&&s<n.length;r++)i[r]===n[s]&&(o+=10,s++);return s===n.length?o+(i.startsWith(n)?20:0):k(t,e)}function R(t,e){return e||(t==="fuzzy"?N:k)}function O(t){return(e,n)=>t(e,n)?1:null}var K='input, textarea, select, [contenteditable=""], [contenteditable="true"], [contenteditable="plaintext-only"]',C=10,a="root";function f(t={}){let e=t.searchStrategy??"substring",n=t.rank??(t.filter?O(t.filter):R(e));return{id:t.id,onOpen:t.onOpen,onClose:t.onClose,onRun:t.onRun,rank:n,searchStrategy:e,persistence:{maxRecent:t.persistence?.maxRecent??C,getRecent:t.persistence?.getRecent,setRecent:t.persistence?.setRecent,getPinned:t.persistence?.getPinned,setPinned:t.persistence?.setPinned},overlayId:t.overlayId,editableSelector:t.editableSelector??K,idPrefix:t.idPrefix??"command",closeOnRun:t.closeOnRun!==!1,scroll:t.scroll,scrollLock:t.scrollLock!==!1}}function g(t,e){return t===void 0?e:typeof t=="function"?t():t}function E(t,e,n){let i=n.length;if(i===0)return 0;let o=t;for(let s=0;s<i;s++)if(o=(o+e+i)%i,n[o])return o;return t}function z(t){let e=t.findIndex(Boolean);return e===-1?0:e}function U(t){for(let e=t.length-1;e>=0;e--)if(t[e])return e;return 0}function A(t,e){return(t.page??a)===e}function y(t){return g(t.hidden,!1)}function D(t){return y(t)?!0:t.enabled!==void 0?!g(t.enabled,!0):g(t.disabled,!1)}var d=class extends F{#n;#d={};#h={[a]:{id:a,title:"Commands"}};#i=new Set;#l=new Set;#c=new Set;#r=[];#o="";#t=0;#s=!1;#a=[a];#u="idle";#f=null;#y=0;#p=0;#x=!1;#g=0;#m=null;#C;#I;constructor(e,n={}){super(e??j("command")),this.#n=f(n),this.#C=this.#n.scroll,this.#I=this.#n.scrollLock}get items(){return this.#d}get search(){return this.#o}set search(e){this.#o!==e&&(this.#o=e,this.#t=0,this.#e())}get activeIndex(){return this.#t}set activeIndex(e){let n=Math.max(0,e);this.#t!==n&&(this.#t=n,this.#e())}get visible(){return this.#s}get isOpen(){return this.#s}get executionState(){return this.#u}get runningId(){return this.#f}get currentPageId(){return this.#a[this.#a.length-1]??a}get pageStack(){return this.#a}get pages(){return this.#h}get loadingIds(){return[...this.#i]}get pinnedIds(){return[...this.#c]}get recentIds(){return[...this.#r]}get visibleItems(){return this.#v()}get filteredItems(){return this.visibleItems.map(e=>e.item)}get groupedItems(){let e={};for(let n of this.visibleItems){let i=n.item.group??"General";e[i]??=[],e[i].push(n.item)}return e}open(){this.isDestroyed||this.#s||(this.#s=!0,this.#o="",this.#t=0,this.#I&&this.#S(!0),this.#R(),this.#O(),this.#n.onOpen?.(),this.emit("open",void 0),this.#e())}close(){this.isDestroyed||!this.#s||(this.#s=!1,this.#o="",this.#t=0,this.#a=[a],this.#I&&this.#S(!1),this.cancelRun(),this.#n.onClose?.(),this.emit("close",void 0),this.#e())}toggle(){this.#s?this.close():this.open()}register(e){if(this.isDestroyed)throw new l("Cannot register command after destroy()","COMMAND_DESTROYED");if(this.#d[e.id])throw new l(`Command id "${e.id}" is already registered`,"COMMAND_DUPLICATE_ID");return this.#d[e.id]=e,e.pinned&&this.#c.add(e.id),this.#b(),this.#e(),()=>{this.unregister(e.id)}}unregister(e){if(this.isDestroyed)return;delete this.#d[e],this.#i.delete(e),this.#l.delete(e),this.#c.delete(e);let n=this.#r.indexOf(e);n!==-1&&this.#r.splice(n,1),this.#b(),this.#e()}async pushPage(e){this.isDestroyed||(this.#h[e.id]=e,this.#a.push(e.id),this.#o="",this.#t=0,await this.#P(e),this.#e())}popPage(){this.isDestroyed||this.#a.length<=1||(this.#a.pop(),this.#o="",this.#t=0,this.#b(),this.#e())}goBack(){this.popPage()}itemState(e){return this.visibleItems.find(n=>n.id===e)??null}inputProps(){let e=`${this.#n.idPrefix}-listbox`,n=this.visibleItems[this.#t];return{role:"combobox","aria-expanded":this.#s,"aria-controls":e,"aria-activedescendant":n?`${this.#n.idPrefix}-option-${n.id}`:void 0,"aria-autocomplete":"list"}}listboxProps(){return{role:"listbox",id:`${this.#n.idPrefix}-listbox`,"aria-label":this.#h[this.currentPageId]?.title??"Commands"}}optionProps(e){let n=this.itemState(e),i=this.visibleItems.findIndex(o=>o.id===e);return{role:"option",id:`${this.#n.idPrefix}-option-${e}`,"aria-selected":i===this.#t,"aria-disabled":n?.disabled??!1,"data-disabled":n?.disabled??!1,"data-loading":n?.loading??!1}}cancelRun(){this.#y++,this.#f=null,this.#u="idle",this.#e()}async run(e){if(this.isDestroyed)return;let n=this.itemState(e)??this.#k(e);if(!n||n.disabled||n.loading)return;let i=++this.#y;this.#f=e,this.#u="running",this.#e();try{if(await n.item.action(),i!==this.#y||this.isDestroyed)return;if(this.#n.onRun?.(n.item),this.emit("run",n.item),await this.#A(e),this.#n.closeOnRun){this.close();return}}finally{i===this.#y&&(this.#f=null,this.#u="idle",this.#e())}}handleKeydown(e){if(this.isDestroyed||!this.#s)return;if(e.key==="Backspace"&&this.#a.length>1&&this.#o.length===0){e.preventDefault(),this.popPage();return}if(!S(e,this.#n.editableSelector)){if(e.key==="Backspace"){e.preventDefault(),this.#o=this.#o.slice(0,-1),this.#t=0,this.#e();return}if(v(e)){e.preventDefault(),this.#o+=e.key,this.#t=0,this.#e();return}}let n=this.visibleItems,i=n.map(o=>o.selectable);switch(e.key){case"ArrowDown":e.preventDefault(),this.#t=E(this.#t,1,i),this.#e();break;case"ArrowUp":e.preventDefault(),this.#t=E(this.#t,-1,i),this.#e();break;case"Home":e.preventDefault(),this.#t=z(i),this.#e();break;case"End":e.preventDefault(),this.#t=U(i),this.#e();break;case"Enter":{e.preventDefault();let o=n[this.#t];o?.selectable&&this.run(o.id);break}case"Escape":e.preventDefault(),this.#a.length>1?this.popPage():this.close();break;default:break}}toStore(){let e=this,n=m(e),i=()=>{c(n,e)};return e.on("open",i),e.on("close",i),e.on("change",i),Object.defineProperty(n,"search",{get(){return e.search},set(o){e.search=o},enumerable:!0,configurable:!0}),Object.defineProperty(n,"activeIndex",{get(){return e.activeIndex},set(o){e.activeIndex=o},enumerable:!0,configurable:!0}),i(),n}destroy(){if(!this.isDestroyed){this.cancelRun(),this.#p++,this.#m!==null&&(this.#C?.unlock(this.#m),this.#m=null),this.#g=0;for(let e of Object.keys(this.#d))delete this.#d[e];for(let e of Object.keys(this.#h))e!==a&&delete this.#h[e];this.#a=[a],this.#i.clear(),this.#l.clear(),this.#c.clear(),this.#r.length=0,this.#s=!1,this.#o="",this.#t=0,super.destroy()}}#v(){let e=this.currentPageId,n=[];for(let i of Object.values(this.#d)){if(!A(i,e)||y(i))continue;let o=this.#n.rank(i,this.#o);if(o===null)continue;let s=D(i),r=i.pinned===!0||this.#c.has(i.id),u=this.#r.includes(i.id);n.push({id:i.id,item:i,disabled:s,hidden:!1,loading:this.#i.has(i.id),pinned:r,recent:u,rank:o+(r?1e3:0)+(u?100:0),selectable:!(s||this.#i.has(i.id))}),i.load&&!this.#l.has(i.id)&&!this.#i.has(i.id)&&this.#E(i)}return n.sort((i,o)=>o.rank!==i.rank?o.rank-i.rank:i.item.label.localeCompare(o.item.label)),n}#k(e){let n=this.#d[e];if(!(n&&A(n,this.currentPageId))||y(n))return null;let i=D(n);return{id:e,item:n,disabled:i,hidden:!1,loading:this.#i.has(e),pinned:n.pinned===!0||this.#c.has(e),recent:this.#r.includes(e),rank:0,selectable:!(i||this.#i.has(e))}}async#R(){if(this.#x)return;this.#x=!0;let e=await this.#n.persistence.getPinned?.();if(e)for(let i of e)this.#c.add(i);let n=await this.#n.persistence.getRecent?.();n&&this.#r.splice(0,this.#r.length,...n),this.#e()}async#O(){let e=this.#h[this.currentPageId];e&&await this.#P(e)}async#P(e){if(!e.load||this.#l.has(e.id))return;let n=++this.#p;this.#i.add(e.id),this.#u="loading",this.#e();try{if(await e.load(),n!==this.#p||this.isDestroyed)return;this.#l.add(e.id)}finally{this.#i.delete(e.id),this.#i.size===0&&this.#u==="loading"&&(this.#u="idle"),this.#e()}}async#E(e){if(!e.load||this.#l.has(e.id))return;let n=++this.#p;this.#i.add(e.id),this.#e();try{if(await e.load(),n!==this.#p||this.isDestroyed)return;this.#l.add(e.id)}finally{this.#i.delete(e.id),this.#e()}}async#A(e){let n=this.#n.persistence.maxRecent??C,i=[e,...this.#r.filter(o=>o!==e)].slice(0,n);this.#r.splice(0,this.#r.length,...i),await this.#n.persistence.setRecent?.(i),this.#e()}#b(){let e=this.visibleItems.length;this.#t>=e&&(this.#t=Math.max(e-1,0))}#e(){this.isDestroyed||(this.#b(),this.emit("change",void 0))}#S(e){if(e){this.#g===0&&this.#C&&this.#m===null&&(this.#m=this.#C.lock("command")),this.#g++;return}this.#g!==0&&(this.#g--,this.#g===0&&this.#m!==null&&(this.#C?.unlock(this.#m),this.#m=null))}};function B(t={}){return new d(void 0,t)}function G(t={}){return new d(void 0,t).toStore()}import{bridgeControllerStore as Y}from"@ailuracode/alpine-core";var b="command",I="command";function _(t={}){let e=t.storeKey??b,n=t.magicKey??t.storeKey??I;return function(o){let s=o,r=new d(t.id,t),u=m(r);Y({alpine:s,storeKey:e,magicKey:n,store:u,controller:r,packageName:"command",subscribe:x=>{let p=!1,h=()=>{p=!0,c(x,r),p=!1},w=r.on("open",h),L=r.on("close",h),M=r.on("change",h);return o.effect(()=>{if(p)return;let P=x.search;r.search!==P&&(r.search=P)}),h(),()=>{w(),L(),M()}}})}}function $(t){return t}function H(t){return m(t)}export{d as CommandController,l as CommandError,I as DEFAULT_COMMAND_MAGIC_KEY,b as DEFAULT_COMMAND_STORE_KEY,a as ROOT_PAGE_ID,$ as commandOptions,_ as commandPlugin,m as createCommandAlpineStore,B as createCommandController,G as createCommandStore,H as createCommandStoreFromController,_ as default,f as normalizeCommandOptions,c as syncCommandStore};
package/package.json CHANGED
@@ -1,10 +1,11 @@
1
1
  {
2
2
  "name": "@ailuracode/alpine-command",
3
- "version": "0.1.0",
3
+ "version": "2.0.0",
4
4
  "description": "Alpine.js headless command palette store",
5
5
  "type": "module",
6
6
  "license": "MIT",
7
7
  "author": "ailuracode",
8
+ "sideEffects": false,
8
9
  "publishConfig": {
9
10
  "access": "public"
10
11
  },
@@ -31,11 +32,22 @@
31
32
  }
32
33
  },
33
34
  "types": "./dist/global.d.ts",
35
+ "devDependencies": {
36
+ "@types/alpinejs": "^3.13.11",
37
+ "alpinejs": "^3.15.12",
38
+ "@ailuracode/alpine-scroll": "3.0.0",
39
+ "@ailuracode/alpine-core": "0.3.0"
40
+ },
34
41
  "peerDependencies": {
42
+ "@ailuracode/alpine-core": "^0.3.0",
43
+ "@ailuracode/alpine-scroll": "^3.0.0",
35
44
  "alpinejs": "^3.0.0",
36
45
  "@types/alpinejs": "^3.13.11"
37
46
  },
38
47
  "peerDependenciesMeta": {
48
+ "@ailuracode/alpine-scroll": {
49
+ "optional": true
50
+ },
39
51
  "@types/alpinejs": {
40
52
  "optional": true
41
53
  }
@@ -49,8 +61,30 @@
49
61
  "spotlight",
50
62
  "headless"
51
63
  ],
64
+ "toolkit": {
65
+ "bundleBudget": {
66
+ "category": "complex-feature"
67
+ }
68
+ },
69
+ "size-limit": [
70
+ {
71
+ "name": "full surface",
72
+ "path": "dist/index.js",
73
+ "import": "*",
74
+ "ignore": [
75
+ "alpinejs",
76
+ "@ailuracode/alpine-core",
77
+ "@types/alpinejs"
78
+ ],
79
+ "gzip": true,
80
+ "brotli": true,
81
+ "limit": "4 kB"
82
+ }
83
+ ],
52
84
  "scripts": {
53
- "build": "tsup src/index.ts --format esm --dts --clean --sourcemap --out-dir dist && cp src/global.d.ts dist/global.d.ts",
54
- "test": "vitest run --config ../../vitest.config.ts test"
85
+ "build": "tsup src/index.ts --format esm --dts --clean --out-dir dist --minify && cp src/global.d.ts dist/global.d.ts",
86
+ "size": "size-limit",
87
+ "test": "vitest run --config ../../vitest.config.ts packages/command",
88
+ "test:e2e": "playwright test --config playwright.config.ts"
55
89
  }
56
90
  }
package/dist/index.js.map DELETED
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/store.ts","../src/index.ts"],"sourcesContent":["export type CommandAction = () => void | Promise<void>;\n\nexport type CommandItem = {\n id: string;\n label: string;\n group?: string;\n shortcut?: string;\n keywords?: string[];\n disabled?: boolean;\n action: CommandAction;\n};\n\nexport type CommandStore = {\n search: string;\n activeIndex: number;\n /** Whether the palette is visible. */\n visible: boolean;\n /** Registered command items. */\n items: Record<string, CommandItem>;\n readonly isOpen: boolean;\n open(): void;\n close(): void;\n toggle(): void;\n register(item: CommandItem): void;\n unregister(id: string): void;\n run(id: string): Promise<void>;\n handleKeydown(event: KeyboardEvent): void;\n readonly filteredItems: CommandItem[];\n readonly groupedItems: Record<string, CommandItem[]>;\n destroy(): void;\n};\n\nexport type CommandStoreConfig = {\n onOpen?: () => void;\n onClose?: () => void;\n onRun?: (item: CommandItem) => void;\n filter?: (item: CommandItem, search: string) => boolean;\n};\n\nfunction defaultFilter(item: CommandItem, search: string): boolean {\n if (!search.trim()) {\n return true;\n }\n\n const query = search.trim().toLowerCase();\n const haystack = [item.label, item.group ?? \"\", item.shortcut ?? \"\", ...(item.keywords ?? [])]\n .join(\" \")\n .toLowerCase();\n\n return haystack.includes(query);\n}\n\nfunction isEditableTarget(target: EventTarget | null): boolean {\n if (!(target instanceof HTMLElement)) {\n return false;\n }\n\n const tag = target.tagName;\n return tag === \"INPUT\" || tag === \"TEXTAREA\" || target.isContentEditable;\n}\n\nfunction isTypingKey(event: KeyboardEvent): boolean {\n return (\n event.key.length === 1 &&\n !event.metaKey &&\n !event.ctrlKey &&\n !event.altKey &&\n !event.isComposing\n );\n}\n\nfunction handlePaletteTyping(\n store: Pick<CommandStore, \"search\" | \"activeIndex\">,\n event: KeyboardEvent\n): boolean {\n if (isEditableTarget(event.target)) {\n return false;\n }\n\n if (event.key === \"Backspace\") {\n event.preventDefault();\n store.search = store.search.slice(0, -1);\n store.activeIndex = 0;\n return true;\n }\n\n if (isTypingKey(event)) {\n event.preventDefault();\n store.search += event.key;\n store.activeIndex = 0;\n return true;\n }\n\n return false;\n}\n\n/** Creates the headless command palette store. */\nexport function createCommandStore(config: CommandStoreConfig = {}): CommandStore {\n const filter = config.filter ?? defaultFilter;\n\n const store: CommandStore = {\n search: \"\",\n activeIndex: 0,\n visible: false,\n items: {},\n\n get isOpen() {\n return this.visible;\n },\n\n get filteredItems() {\n const list = Object.values(this.items).filter(\n (item) => !item.disabled && filter(item, this.search)\n );\n if (this.activeIndex >= list.length) {\n this.activeIndex = Math.max(list.length - 1, 0);\n }\n return list;\n },\n\n get groupedItems() {\n const groups: Record<string, CommandItem[]> = {};\n for (const item of this.filteredItems) {\n const group = item.group ?? \"General\";\n groups[group] ??= [];\n groups[group].push(item);\n }\n return groups;\n },\n\n open() {\n if (this.visible) {\n return;\n }\n this.visible = true;\n this.search = \"\";\n this.activeIndex = 0;\n config.onOpen?.();\n },\n\n close() {\n if (!this.visible) {\n return;\n }\n this.visible = false;\n this.search = \"\";\n this.activeIndex = 0;\n config.onClose?.();\n },\n\n toggle() {\n if (this.visible) {\n this.close();\n } else {\n this.open();\n }\n },\n\n register(item) {\n this.items[item.id] = item;\n },\n\n unregister(id) {\n delete this.items[id];\n },\n\n async run(id) {\n const item = this.items[id];\n if (!item || item.disabled) {\n return;\n }\n\n await item.action();\n config.onRun?.(item);\n this.close();\n },\n\n handleKeydown(event) {\n if (!this.visible) {\n return;\n }\n\n const list = this.filteredItems;\n\n if (handlePaletteTyping(this, event)) {\n return;\n }\n\n switch (event.key) {\n case \"ArrowDown\":\n event.preventDefault();\n this.activeIndex = list.length === 0 ? 0 : (this.activeIndex + 1) % list.length;\n break;\n case \"ArrowUp\":\n event.preventDefault();\n this.activeIndex =\n list.length === 0 ? 0 : (this.activeIndex - 1 + list.length) % list.length;\n break;\n case \"Home\":\n event.preventDefault();\n this.activeIndex = 0;\n break;\n case \"End\":\n event.preventDefault();\n this.activeIndex = Math.max(list.length - 1, 0);\n break;\n case \"Enter\": {\n event.preventDefault();\n const active = list[this.activeIndex];\n if (active) {\n void this.run(active.id);\n }\n break;\n }\n case \"Escape\":\n event.preventDefault();\n this.close();\n break;\n default:\n break;\n }\n },\n\n destroy() {\n this.items = {};\n this.visible = false;\n this.search = \"\";\n this.activeIndex = 0;\n },\n };\n\n return store;\n}\n","import type AlpineType from \"alpinejs\";\nimport { type CommandStore, type CommandStoreConfig, createCommandStore } from \"./store.js\";\n\nexport {\n type CommandAction,\n type CommandItem,\n type CommandStore,\n type CommandStoreConfig,\n createCommandStore,\n} from \"./store.js\";\n\nexport interface CommandPluginOptions extends CommandStoreConfig {}\n\n/** Builds typed command plugin options. */\nexport function commandOptions<const T extends CommandPluginOptions>(options: T): T {\n return options;\n}\n\n/** Alpine.js command palette plugin. Registers `$store.command`. */\nexport default function commandPlugin(\n options: CommandPluginOptions = {}\n): AlpineType.PluginCallback {\n return function registerCommand(Alpine) {\n const store = createCommandStore(options);\n Alpine.store(\"command\", store);\n Alpine.magic(\"command\", () => Alpine.store(\"command\"));\n };\n}\n\ndeclare global {\n namespace Alpine {\n interface Stores {\n command: CommandStore;\n }\n interface Magics<T> {\n $command: CommandStore;\n }\n }\n}\n"],"mappings":"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","names":[]}