@ailuracode/alpine-command 1.0.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 +120 -11
- package/dist/global.d.ts +44 -8
- package/dist/index.d.ts +180 -46
- package/dist/index.js +1 -304
- package/package.json +35 -5
package/README.md
CHANGED
|
@@ -1,26 +1,135 @@
|
|
|
1
1
|
# @ailuracode/alpine-command
|
|
2
2
|
|
|
3
|
-
Headless command palette store
|
|
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
|
-
pnpm add @ailuracode/alpine-command alpinejs
|
|
8
|
+
pnpm add @ailuracode/alpine-command @ailuracode/alpine-core alpinejs
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Active item navigation uses inline helpers — no extra dependency.
|
|
12
|
+
|
|
13
|
+
## Quick start
|
|
14
|
+
|
|
15
|
+
```js
|
|
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();
|
|
11
33
|
```
|
|
12
34
|
|
|
13
35
|
## Store API
|
|
14
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
|
+
|
|
15
54
|
```ts
|
|
16
|
-
|
|
17
|
-
$store.command.search = "theme";
|
|
18
|
-
$store.command.register({
|
|
55
|
+
{
|
|
19
56
|
id: "toggle-theme",
|
|
20
57
|
label: "Toggle theme",
|
|
21
|
-
group
|
|
22
|
-
shortcut
|
|
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,30 +1,66 @@
|
|
|
1
1
|
/// <reference types="@types/alpinejs" />
|
|
2
2
|
|
|
3
|
-
import type {
|
|
3
|
+
import type {
|
|
4
|
+
CommandExecutionState,
|
|
5
|
+
CommandItem,
|
|
6
|
+
CommandItemState,
|
|
7
|
+
CommandPage,
|
|
8
|
+
CommandStoreConfig,
|
|
9
|
+
} from "./types";
|
|
4
10
|
|
|
5
|
-
export type {
|
|
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";
|
|
6
25
|
|
|
7
26
|
export interface CommandStore {
|
|
8
27
|
search: string;
|
|
9
28
|
activeIndex: number;
|
|
10
29
|
visible: boolean;
|
|
11
30
|
items: Record<string, CommandItem>;
|
|
12
|
-
|
|
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[];
|
|
13
40
|
open(): void;
|
|
14
41
|
close(): void;
|
|
15
42
|
toggle(): void;
|
|
16
|
-
register(item: CommandItem): void;
|
|
43
|
+
register(item: CommandItem): () => void;
|
|
17
44
|
unregister(id: string): void;
|
|
18
45
|
run(id: string): Promise<void>;
|
|
46
|
+
cancelRun(): void;
|
|
19
47
|
handleKeydown(event: KeyboardEvent): void;
|
|
20
|
-
|
|
21
|
-
|
|
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[]>;
|
|
22
58
|
destroy(): void;
|
|
23
59
|
}
|
|
24
60
|
|
|
25
61
|
export function createCommandController(
|
|
26
|
-
config?:
|
|
27
|
-
): import("./
|
|
62
|
+
config?: CommandStoreConfig
|
|
63
|
+
): import("./controller").CommandController;
|
|
28
64
|
|
|
29
65
|
export default function commandPlugin(
|
|
30
66
|
options?: import("./types").CommandPluginOptions
|
package/dist/index.d.ts
CHANGED
|
@@ -1,55 +1,161 @@
|
|
|
1
1
|
import { Alpine, PluginCallback, BaseController } from '@ailuracode/alpine-core';
|
|
2
2
|
export { Unsubscribe } from '@ailuracode/alpine-core';
|
|
3
|
+
import { ScrollStore } from '@ailuracode/alpine-scroll';
|
|
3
4
|
import { Alpine as Alpine$1 } from 'alpinejs';
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
7
|
* Public type contracts for `@ailuracode/alpine-command`.
|
|
7
|
-
*
|
|
8
|
-
* Every public type lives in a `types.ts` module so consumers can import
|
|
9
|
-
* them without pulling the implementation. The shape IS the contract.
|
|
10
8
|
*/
|
|
11
9
|
|
|
12
10
|
/** Action callback invoked when a command is executed. */
|
|
13
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
|
+
};
|
|
14
40
|
/** A registered command item. */
|
|
15
41
|
type CommandItem = {
|
|
16
|
-
id: string;
|
|
17
|
-
label: string;
|
|
18
|
-
group?: string;
|
|
19
|
-
shortcut?: string;
|
|
20
|
-
keywords?: string[];
|
|
21
|
-
|
|
22
|
-
|
|
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;
|
|
23
67
|
};
|
|
24
|
-
/**
|
|
68
|
+
/** Explicit async lifecycle state for the palette. */
|
|
69
|
+
type CommandExecutionState = "idle" | "loading" | "running";
|
|
70
|
+
/** Config callbacks and behavior for the command palette. */
|
|
25
71
|
type CommandStoreConfig = {
|
|
26
|
-
onOpen?: () => void;
|
|
27
|
-
onClose?: () => void;
|
|
28
|
-
onRun?: (item: CommandItem) => void;
|
|
29
|
-
|
|
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;
|
|
30
102
|
};
|
|
31
103
|
/** Options accepted by the command plugin factory. */
|
|
32
104
|
interface CommandPluginOptions extends CommandStoreConfig {
|
|
33
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;
|
|
34
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";
|
|
35
126
|
/** Alpine-facing store surface. */
|
|
36
127
|
interface CommandStore {
|
|
37
128
|
search: string;
|
|
38
129
|
activeIndex: number;
|
|
39
|
-
/** Whether the palette is visible. */
|
|
40
130
|
visible: boolean;
|
|
41
|
-
/** Registered command items. */
|
|
42
131
|
items: Record<string, CommandItem>;
|
|
43
|
-
|
|
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[];
|
|
44
141
|
open(): void;
|
|
45
142
|
close(): void;
|
|
46
143
|
toggle(): void;
|
|
47
|
-
register(item: CommandItem): void;
|
|
144
|
+
register(item: CommandItem): () => void;
|
|
48
145
|
unregister(id: string): void;
|
|
49
146
|
run(id: string): Promise<void>;
|
|
147
|
+
cancelRun(): void;
|
|
50
148
|
handleKeydown(event: KeyboardEvent): void;
|
|
51
|
-
|
|
52
|
-
|
|
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[]>;
|
|
53
159
|
destroy(): void;
|
|
54
160
|
}
|
|
55
161
|
/** Typed view of `Alpine` the command plugin uses internally. */
|
|
@@ -70,27 +176,23 @@ type CommandPluginCallback = PluginCallback<Alpine$1>;
|
|
|
70
176
|
* `open` — palette opened.
|
|
71
177
|
* `close` — palette closed.
|
|
72
178
|
* `run` — a command was executed.
|
|
179
|
+
* `change` — reactive state changed (search, selection, pages, loading).
|
|
73
180
|
*/
|
|
74
181
|
interface CommandEvents extends Record<string, unknown> {
|
|
75
182
|
open: undefined;
|
|
76
183
|
close: undefined;
|
|
77
184
|
run: CommandItem;
|
|
185
|
+
change: undefined;
|
|
78
186
|
}
|
|
79
187
|
|
|
80
188
|
/**
|
|
81
189
|
* Command palette controller — the framework-agnostic core of
|
|
82
|
-
* `@ailuracode/alpine-command`.
|
|
83
|
-
* with search, item registry, open/close/toggle, keyboard navigation,
|
|
84
|
-
* and filtered/grouped item views.
|
|
85
|
-
*
|
|
86
|
-
* Emits typed `open`, `close`, and `run` events so consumers can
|
|
87
|
-
* react programmatically.
|
|
190
|
+
* `@ailuracode/alpine-command`.
|
|
88
191
|
*/
|
|
89
192
|
|
|
90
193
|
/**
|
|
91
|
-
* Headless command palette controller
|
|
92
|
-
*
|
|
93
|
-
* and filtered/grouped item views.
|
|
194
|
+
* Headless command palette controller with search ranking, nested pages,
|
|
195
|
+
* async loading/execution, and ARIA helpers.
|
|
94
196
|
*/
|
|
95
197
|
declare class CommandController extends BaseController<CommandEvents> {
|
|
96
198
|
#private;
|
|
@@ -102,37 +204,67 @@ declare class CommandController extends BaseController<CommandEvents> {
|
|
|
102
204
|
set activeIndex(value: number);
|
|
103
205
|
get visible(): boolean;
|
|
104
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[];
|
|
105
216
|
get filteredItems(): CommandItem[];
|
|
106
217
|
get groupedItems(): Record<string, CommandItem[]>;
|
|
107
218
|
open(): void;
|
|
108
219
|
close(): void;
|
|
109
220
|
toggle(): void;
|
|
110
|
-
register(item: CommandItem): void;
|
|
221
|
+
register(item: CommandItem): () => void;
|
|
111
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;
|
|
112
231
|
run(id: string): Promise<void>;
|
|
113
232
|
handleKeydown(event: KeyboardEvent): void;
|
|
114
|
-
/**
|
|
115
|
-
* Returns a store-shaped object for Alpine's `$store.command`.
|
|
116
|
-
* The store delegates to this controller.
|
|
117
|
-
*/
|
|
118
233
|
toStore(): CommandStore;
|
|
234
|
+
destroy(): void;
|
|
119
235
|
}
|
|
120
|
-
/**
|
|
121
|
-
* Creates a CommandController instance.
|
|
122
|
-
* Convenience for non-Alpine consumers.
|
|
123
|
-
*/
|
|
236
|
+
/** Creates a CommandController instance. */
|
|
124
237
|
declare function createCommandController(config?: CommandStoreConfig): CommandController;
|
|
238
|
+
/** Creates a CommandStore (store-shaped object) directly. */
|
|
239
|
+
declare function createCommandStore(config?: CommandStoreConfig): CommandStore;
|
|
240
|
+
|
|
125
241
|
/**
|
|
126
|
-
*
|
|
127
|
-
*
|
|
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.
|
|
128
246
|
*/
|
|
129
|
-
|
|
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
|
+
});
|
|
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;
|
|
130
265
|
|
|
131
266
|
/**
|
|
132
267
|
* Alpine.js integration for `@ailuracode/alpine-command`.
|
|
133
|
-
*
|
|
134
|
-
* Thin adapter that wires {@link CommandController} into
|
|
135
|
-
* `$store.command` and the `$command` magic.
|
|
136
268
|
*/
|
|
137
269
|
|
|
138
270
|
/**
|
|
@@ -143,5 +275,7 @@ declare function createCommandStore(config?: CommandStoreConfig): CommandStore;
|
|
|
143
275
|
declare function commandPlugin(options?: CommandPluginOptions): CommandPluginCallback;
|
|
144
276
|
/** Builds typed command plugin options. */
|
|
145
277
|
declare function commandOptions<const T extends CommandPluginOptions>(options: T): T;
|
|
278
|
+
/** @deprecated Use {@link createCommandAlpineStore} via the plugin adapter. */
|
|
279
|
+
declare function createCommandStoreFromController(controller: CommandController): CommandStore;
|
|
146
280
|
|
|
147
|
-
export { type CommandAction, type CommandAlpine, CommandController, type CommandEvents, type CommandItem, type CommandPluginCallback, type CommandPluginOptions, type CommandStore, type CommandStoreConfig, commandOptions, commandPlugin, createCommandController, 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,304 +1 @@
|
|
|
1
|
-
// src/controller.ts
|
|
2
|
-
import { BaseController, generateId } from "@ailuracode/alpine-core";
|
|
3
|
-
function defaultFilter(item, search) {
|
|
4
|
-
if (!search.trim()) {
|
|
5
|
-
return true;
|
|
6
|
-
}
|
|
7
|
-
const query = search.trim().toLowerCase();
|
|
8
|
-
const haystack = [item.label, item.group ?? "", item.shortcut ?? "", ...item.keywords ?? []].join(" ").toLowerCase();
|
|
9
|
-
return haystack.includes(query);
|
|
10
|
-
}
|
|
11
|
-
function isEditableTarget(target) {
|
|
12
|
-
if (!(target instanceof HTMLElement)) {
|
|
13
|
-
return false;
|
|
14
|
-
}
|
|
15
|
-
const tag = target.tagName;
|
|
16
|
-
return tag === "INPUT" || tag === "TEXTAREA" || target.isContentEditable;
|
|
17
|
-
}
|
|
18
|
-
function isTypingKey(event) {
|
|
19
|
-
return event.key.length === 1 && !event.metaKey && !event.ctrlKey && !event.altKey && !event.isComposing;
|
|
20
|
-
}
|
|
21
|
-
var CommandController = class extends BaseController {
|
|
22
|
-
#items = {};
|
|
23
|
-
#search = "";
|
|
24
|
-
#activeIndex = 0;
|
|
25
|
-
#visible = false;
|
|
26
|
-
#filter;
|
|
27
|
-
#config;
|
|
28
|
-
constructor(id, config = {}) {
|
|
29
|
-
super(id ?? generateId("command"));
|
|
30
|
-
this.#filter = config.filter ?? defaultFilter;
|
|
31
|
-
this.#config = config;
|
|
32
|
-
}
|
|
33
|
-
get items() {
|
|
34
|
-
return this.#items;
|
|
35
|
-
}
|
|
36
|
-
get search() {
|
|
37
|
-
return this.#search;
|
|
38
|
-
}
|
|
39
|
-
set search(value) {
|
|
40
|
-
this.#search = value;
|
|
41
|
-
}
|
|
42
|
-
get activeIndex() {
|
|
43
|
-
return this.#activeIndex;
|
|
44
|
-
}
|
|
45
|
-
set activeIndex(value) {
|
|
46
|
-
this.#activeIndex = value;
|
|
47
|
-
}
|
|
48
|
-
get visible() {
|
|
49
|
-
return this.#visible;
|
|
50
|
-
}
|
|
51
|
-
get isOpen() {
|
|
52
|
-
return this.#visible;
|
|
53
|
-
}
|
|
54
|
-
get filteredItems() {
|
|
55
|
-
const list = Object.values(this.#items).filter(
|
|
56
|
-
(item) => !item.disabled && this.#filter(item, this.#search)
|
|
57
|
-
);
|
|
58
|
-
if (this.#activeIndex >= list.length) {
|
|
59
|
-
this.#activeIndex = Math.max(list.length - 1, 0);
|
|
60
|
-
}
|
|
61
|
-
return list;
|
|
62
|
-
}
|
|
63
|
-
get groupedItems() {
|
|
64
|
-
const groups = {};
|
|
65
|
-
for (const item of this.filteredItems) {
|
|
66
|
-
const group = item.group ?? "General";
|
|
67
|
-
groups[group] ??= [];
|
|
68
|
-
groups[group].push(item);
|
|
69
|
-
}
|
|
70
|
-
return groups;
|
|
71
|
-
}
|
|
72
|
-
open() {
|
|
73
|
-
if (this.isDestroyed || this.#visible) {
|
|
74
|
-
return;
|
|
75
|
-
}
|
|
76
|
-
this.#visible = true;
|
|
77
|
-
this.#search = "";
|
|
78
|
-
this.#activeIndex = 0;
|
|
79
|
-
this.#config.onOpen?.();
|
|
80
|
-
this.emit("open", void 0);
|
|
81
|
-
}
|
|
82
|
-
close() {
|
|
83
|
-
if (this.isDestroyed || !this.#visible) {
|
|
84
|
-
return;
|
|
85
|
-
}
|
|
86
|
-
this.#visible = false;
|
|
87
|
-
this.#search = "";
|
|
88
|
-
this.#activeIndex = 0;
|
|
89
|
-
this.#config.onClose?.();
|
|
90
|
-
this.emit("close", void 0);
|
|
91
|
-
}
|
|
92
|
-
toggle() {
|
|
93
|
-
if (this.#visible) {
|
|
94
|
-
this.close();
|
|
95
|
-
} else {
|
|
96
|
-
this.open();
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
register(item) {
|
|
100
|
-
if (this.isDestroyed) {
|
|
101
|
-
return;
|
|
102
|
-
}
|
|
103
|
-
this.#items[item.id] = item;
|
|
104
|
-
}
|
|
105
|
-
unregister(id) {
|
|
106
|
-
if (this.isDestroyed) {
|
|
107
|
-
return;
|
|
108
|
-
}
|
|
109
|
-
delete this.#items[id];
|
|
110
|
-
}
|
|
111
|
-
async run(id) {
|
|
112
|
-
if (this.isDestroyed) {
|
|
113
|
-
return;
|
|
114
|
-
}
|
|
115
|
-
const item = this.#items[id];
|
|
116
|
-
if (!item || item.disabled) {
|
|
117
|
-
return;
|
|
118
|
-
}
|
|
119
|
-
await item.action();
|
|
120
|
-
this.#config.onRun?.(item);
|
|
121
|
-
this.emit("run", item);
|
|
122
|
-
this.close();
|
|
123
|
-
}
|
|
124
|
-
handleKeydown(event) {
|
|
125
|
-
if (this.isDestroyed || !this.#visible) {
|
|
126
|
-
return;
|
|
127
|
-
}
|
|
128
|
-
const list = this.filteredItems;
|
|
129
|
-
if (isEditableTarget(event.target)) {
|
|
130
|
-
} else if (event.key === "Backspace") {
|
|
131
|
-
event.preventDefault();
|
|
132
|
-
this.#search = this.#search.slice(0, -1);
|
|
133
|
-
this.#activeIndex = 0;
|
|
134
|
-
return;
|
|
135
|
-
} else if (isTypingKey(event)) {
|
|
136
|
-
event.preventDefault();
|
|
137
|
-
this.#search += event.key;
|
|
138
|
-
this.#activeIndex = 0;
|
|
139
|
-
return;
|
|
140
|
-
}
|
|
141
|
-
switch (event.key) {
|
|
142
|
-
case "ArrowDown":
|
|
143
|
-
event.preventDefault();
|
|
144
|
-
this.#activeIndex = list.length === 0 ? 0 : (this.#activeIndex + 1) % list.length;
|
|
145
|
-
break;
|
|
146
|
-
case "ArrowUp":
|
|
147
|
-
event.preventDefault();
|
|
148
|
-
this.#activeIndex = list.length === 0 ? 0 : (this.#activeIndex - 1 + list.length) % list.length;
|
|
149
|
-
break;
|
|
150
|
-
case "Home":
|
|
151
|
-
event.preventDefault();
|
|
152
|
-
this.#activeIndex = 0;
|
|
153
|
-
break;
|
|
154
|
-
case "End":
|
|
155
|
-
event.preventDefault();
|
|
156
|
-
this.#activeIndex = Math.max(list.length - 1, 0);
|
|
157
|
-
break;
|
|
158
|
-
case "Enter": {
|
|
159
|
-
event.preventDefault();
|
|
160
|
-
const active = list[this.#activeIndex];
|
|
161
|
-
if (active) {
|
|
162
|
-
void this.run(active.id);
|
|
163
|
-
}
|
|
164
|
-
break;
|
|
165
|
-
}
|
|
166
|
-
case "Escape":
|
|
167
|
-
event.preventDefault();
|
|
168
|
-
this.close();
|
|
169
|
-
break;
|
|
170
|
-
default:
|
|
171
|
-
break;
|
|
172
|
-
}
|
|
173
|
-
}
|
|
174
|
-
/**
|
|
175
|
-
* Returns a store-shaped object for Alpine's `$store.command`.
|
|
176
|
-
* The store delegates to this controller.
|
|
177
|
-
*/
|
|
178
|
-
toStore() {
|
|
179
|
-
const controller = this;
|
|
180
|
-
return {
|
|
181
|
-
get search() {
|
|
182
|
-
return controller.search;
|
|
183
|
-
},
|
|
184
|
-
set search(value) {
|
|
185
|
-
controller.search = value;
|
|
186
|
-
},
|
|
187
|
-
get activeIndex() {
|
|
188
|
-
return controller.activeIndex;
|
|
189
|
-
},
|
|
190
|
-
set activeIndex(value) {
|
|
191
|
-
controller.activeIndex = value;
|
|
192
|
-
},
|
|
193
|
-
get visible() {
|
|
194
|
-
return controller.visible;
|
|
195
|
-
},
|
|
196
|
-
get items() {
|
|
197
|
-
return controller.items;
|
|
198
|
-
},
|
|
199
|
-
get isOpen() {
|
|
200
|
-
return controller.isOpen;
|
|
201
|
-
},
|
|
202
|
-
open: () => controller.open(),
|
|
203
|
-
close: () => controller.close(),
|
|
204
|
-
toggle: () => controller.toggle(),
|
|
205
|
-
register: (item) => controller.register(item),
|
|
206
|
-
unregister: (id) => controller.unregister(id),
|
|
207
|
-
run: (id) => controller.run(id),
|
|
208
|
-
handleKeydown: (event) => controller.handleKeydown(event),
|
|
209
|
-
get filteredItems() {
|
|
210
|
-
return controller.filteredItems;
|
|
211
|
-
},
|
|
212
|
-
get groupedItems() {
|
|
213
|
-
return controller.groupedItems;
|
|
214
|
-
},
|
|
215
|
-
destroy: () => controller.destroy()
|
|
216
|
-
};
|
|
217
|
-
}
|
|
218
|
-
};
|
|
219
|
-
function createCommandController(config = {}) {
|
|
220
|
-
return new CommandController(void 0, config);
|
|
221
|
-
}
|
|
222
|
-
function createCommandStore(config = {}) {
|
|
223
|
-
return new CommandController(void 0, config).toStore();
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
// src/plugin.ts
|
|
227
|
-
var COMMAND_STORE_KEY = "command";
|
|
228
|
-
function commandPlugin(options = {}) {
|
|
229
|
-
return function registerCommand(alpine) {
|
|
230
|
-
const Alpine = alpine;
|
|
231
|
-
const controller = new CommandController(options.id, options);
|
|
232
|
-
const store = {
|
|
233
|
-
search: "",
|
|
234
|
-
activeIndex: 0,
|
|
235
|
-
visible: false,
|
|
236
|
-
items: {},
|
|
237
|
-
get isOpen() {
|
|
238
|
-
return this.visible;
|
|
239
|
-
},
|
|
240
|
-
get filteredItems() {
|
|
241
|
-
const defaultFilter2 = (i, s) => {
|
|
242
|
-
if (!s.trim()) {
|
|
243
|
-
return true;
|
|
244
|
-
}
|
|
245
|
-
const query = s.trim().toLowerCase();
|
|
246
|
-
const haystack = [i.label, i.group ?? "", i.shortcut ?? "", ...i.keywords ?? []].join(" ").toLowerCase();
|
|
247
|
-
return haystack.includes(query);
|
|
248
|
-
};
|
|
249
|
-
const filter = options.filter ?? defaultFilter2;
|
|
250
|
-
const list = Object.values(this.items).filter(
|
|
251
|
-
(item) => !item.disabled && filter(item, this.search)
|
|
252
|
-
);
|
|
253
|
-
if (this.activeIndex >= list.length) {
|
|
254
|
-
this.activeIndex = Math.max(list.length - 1, 0);
|
|
255
|
-
}
|
|
256
|
-
return list;
|
|
257
|
-
},
|
|
258
|
-
get groupedItems() {
|
|
259
|
-
const groups = {};
|
|
260
|
-
for (const item of this.filteredItems) {
|
|
261
|
-
const group = item.group ?? "General";
|
|
262
|
-
groups[group] ??= [];
|
|
263
|
-
groups[group].push(item);
|
|
264
|
-
}
|
|
265
|
-
return groups;
|
|
266
|
-
},
|
|
267
|
-
open: () => controller.open(),
|
|
268
|
-
close: () => controller.close(),
|
|
269
|
-
toggle: () => controller.toggle(),
|
|
270
|
-
register: (item) => controller.register(item),
|
|
271
|
-
unregister: (id) => controller.unregister(id),
|
|
272
|
-
run: (id) => controller.run(id),
|
|
273
|
-
handleKeydown: (event) => controller.handleKeydown(event),
|
|
274
|
-
destroy: () => controller.destroy()
|
|
275
|
-
};
|
|
276
|
-
Alpine.store(COMMAND_STORE_KEY, store);
|
|
277
|
-
const reactiveStore = Alpine.store(COMMAND_STORE_KEY);
|
|
278
|
-
controller.on("open", () => {
|
|
279
|
-
reactiveStore.visible = true;
|
|
280
|
-
reactiveStore.search = "";
|
|
281
|
-
reactiveStore.activeIndex = 0;
|
|
282
|
-
});
|
|
283
|
-
controller.on("close", () => {
|
|
284
|
-
reactiveStore.visible = false;
|
|
285
|
-
reactiveStore.search = "";
|
|
286
|
-
reactiveStore.activeIndex = 0;
|
|
287
|
-
});
|
|
288
|
-
Alpine.magic(COMMAND_STORE_KEY, () => reactiveStore);
|
|
289
|
-
if (typeof Alpine.cleanup === "function") {
|
|
290
|
-
Alpine.cleanup(() => controller.destroy());
|
|
291
|
-
}
|
|
292
|
-
};
|
|
293
|
-
}
|
|
294
|
-
function commandOptions(options) {
|
|
295
|
-
return options;
|
|
296
|
-
}
|
|
297
|
-
export {
|
|
298
|
-
CommandController,
|
|
299
|
-
commandOptions,
|
|
300
|
-
commandPlugin,
|
|
301
|
-
createCommandController,
|
|
302
|
-
createCommandStore,
|
|
303
|
-
commandPlugin as default
|
|
304
|
-
};
|
|
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": "
|
|
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
|
},
|
|
@@ -32,14 +33,21 @@
|
|
|
32
33
|
},
|
|
33
34
|
"types": "./dist/global.d.ts",
|
|
34
35
|
"devDependencies": {
|
|
35
|
-
"@
|
|
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"
|
|
36
40
|
},
|
|
37
41
|
"peerDependencies": {
|
|
38
|
-
"@ailuracode/alpine-core": "^0.
|
|
42
|
+
"@ailuracode/alpine-core": "^0.3.0",
|
|
43
|
+
"@ailuracode/alpine-scroll": "^3.0.0",
|
|
39
44
|
"alpinejs": "^3.0.0",
|
|
40
45
|
"@types/alpinejs": "^3.13.11"
|
|
41
46
|
},
|
|
42
47
|
"peerDependenciesMeta": {
|
|
48
|
+
"@ailuracode/alpine-scroll": {
|
|
49
|
+
"optional": true
|
|
50
|
+
},
|
|
43
51
|
"@types/alpinejs": {
|
|
44
52
|
"optional": true
|
|
45
53
|
}
|
|
@@ -53,8 +61,30 @@
|
|
|
53
61
|
"spotlight",
|
|
54
62
|
"headless"
|
|
55
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
|
+
],
|
|
56
84
|
"scripts": {
|
|
57
|
-
"build": "tsup src/index.ts --format esm --dts --clean --out-dir dist && cp src/global.d.ts dist/global.d.ts",
|
|
58
|
-
"
|
|
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"
|
|
59
89
|
}
|
|
60
90
|
}
|