modern-cmdk 1.0.0 → 1.0.2

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 ADDED
@@ -0,0 +1,261 @@
1
+ <p align="center">
2
+ <img src="https://raw.githubusercontent.com/ABCrimson/modern-cmdk/main/apps/docs/public/og-image.svg" alt="modern-cmdk" width="640" />
3
+ </p>
4
+
5
+ <p align="center">
6
+ <strong>modern-cmdk</strong><br/>
7
+ The definitive command palette engine for the modern web.
8
+ </p>
9
+
10
+ <p align="center">
11
+ <a href="https://www.npmjs.com/package/modern-cmdk"><img alt="npm version" src="https://img.shields.io/npm/v/modern-cmdk?style=flat-square&color=dc2626&labelColor=0a0e27"/></a>
12
+ <a href="https://bundlephobia.com/package/modern-cmdk"><img alt="bundle size" src="https://img.shields.io/bundlephobia/minzip/modern-cmdk?style=flat-square&color=dc2626&labelColor=0a0e27&label=core"/></a>
13
+ <a href="https://github.com/ABCrimson/modern-cmdk/actions/workflows/ci.yml"><img alt="CI" src="https://img.shields.io/github/actions/workflow/status/ABCrimson/modern-cmdk/ci.yml?style=flat-square&color=dc2626&labelColor=0a0e27&label=CI"/></a>
14
+ <a href="https://www.typescriptlang.org/"><img alt="TypeScript" src="https://img.shields.io/badge/TypeScript-6.0-dc2626?style=flat-square&labelColor=0a0e27"/></a>
15
+ <a href="https://react.dev/"><img alt="React" src="https://img.shields.io/badge/React-19-dc2626?style=flat-square&labelColor=0a0e27"/></a>
16
+ <a href="https://github.com/ABCrimson/modern-cmdk/blob/main/LICENSE"><img alt="MIT License" src="https://img.shields.io/npm/l/modern-cmdk?style=flat-square&color=dc2626&labelColor=0a0e27"/></a>
17
+ </p>
18
+
19
+ <p align="center">
20
+ <a href="https://command.crimson.dev">Documentation</a> &middot;
21
+ <a href="https://command.crimson.dev/guide/getting-started">Getting Started</a> &middot;
22
+ <a href="https://command.crimson.dev/api/command">API Reference</a> &middot;
23
+ <a href="https://command.crimson.dev/examples/basic">Examples</a>
24
+ </p>
25
+
26
+ ---
27
+
28
+ ## Install
29
+
30
+ ```bash
31
+ npm install modern-cmdk
32
+ ```
33
+
34
+ The package exports three entry points:
35
+
36
+ ```ts
37
+ import { createCommandMachine } from 'modern-cmdk'; // Core engine (no React)
38
+ import { Command } from 'modern-cmdk/react'; // React 19 components
39
+ import 'modern-cmdk/styles.css'; // Default styles
40
+ ```
41
+
42
+ React is an **optional** peer dependency — use the core engine standalone in any framework, or pair it with the React adapter for a batteries-included experience.
43
+
44
+ ---
45
+
46
+ ## Quick Start
47
+
48
+ ```tsx
49
+ 'use client';
50
+
51
+ import { Command } from 'modern-cmdk/react';
52
+ import 'modern-cmdk/styles.css';
53
+
54
+ function CommandPalette() {
55
+ return (
56
+ <Command.Dialog>
57
+ <Command.Input placeholder="Type a command..." />
58
+ <Command.List>
59
+ <Command.Empty>No results found.</Command.Empty>
60
+
61
+ <Command.Group heading="Actions">
62
+ <Command.Item value="copy" onSelect={() => copyToClipboard()}>
63
+ <Command.Highlight>Copy to Clipboard</Command.Highlight>
64
+ <Command.Shortcut>Mod+C</Command.Shortcut>
65
+ </Command.Item>
66
+ <Command.Item value="paste" onSelect={() => paste()}>
67
+ <Command.Highlight>Paste</Command.Highlight>
68
+ <Command.Shortcut>Mod+V</Command.Shortcut>
69
+ </Command.Item>
70
+ </Command.Group>
71
+
72
+ <Command.Group heading="Navigation">
73
+ <Command.Item value="settings" onSelect={() => navigate('/settings')}>
74
+ Settings
75
+ <Command.Badge>New</Command.Badge>
76
+ </Command.Item>
77
+ </Command.Group>
78
+ </Command.List>
79
+ <Command.Loading>Searching...</Command.Loading>
80
+ </Command.Dialog>
81
+ );
82
+ }
83
+ ```
84
+
85
+ Or scaffold a new project instantly:
86
+
87
+ ```bash
88
+ npm create modern-cmdk
89
+ ```
90
+
91
+ ---
92
+
93
+ ## Why modern-cmdk?
94
+
95
+ A ground-up rewrite of `cmdk` for **React 19**, **ES2026**, and **TypeScript 6**. Framework-agnostic core. Zero compromise on performance, accessibility, or developer experience.
96
+
97
+ | | cmdk | modern-cmdk |
98
+ |---|---|---|
99
+ | **Architecture** | React-only, tightly coupled | Framework-agnostic core + thin adapters |
100
+ | **React** | 18 | 19 (`use()`, `useOptimistic`, `useId`, `ref` as prop) |
101
+ | **Search** | Basic substring | Fuzzy scoring + optional WASM (sub-1ms on 100K items) |
102
+ | **Ranking** | Static order | Frecency with `Temporal`-based exponential decay |
103
+ | **Virtualization** | None | Automatic variable-height, `content-visibility: auto` |
104
+ | **Animations** | CSS transitions | GPU-composited: `@starting-style`, `scroll-timeline`, spring easing |
105
+ | **Keyboard** | External | Built-in registry, `Mod` key, conflict detection |
106
+ | **Accessibility** | Partial ARIA | Full WAI-ARIA combobox, `forced-colors`, `prefers-contrast` |
107
+ | **Bundle** | ~6 KB | Core ~5.2 KB, React ~6.3 KB |
108
+ | **TypeScript** | 4.x/5.x | 6.0, isolated declarations, branded types |
109
+ | **Cleanup** | Manual | `using`/`await using` (Explicit Resource Management) |
110
+ | **Error handling** | None | `CommandErrorBoundary` with fallback UI |
111
+ | **DevTools** | None | Built-in devtools hook for browser inspection |
112
+
113
+ ---
114
+
115
+ ## Features
116
+
117
+ - **Framework-agnostic core** -- Pure TypeScript state machine. Zero DOM dependencies. Portable to any runtime.
118
+ - **React 19 adapter** -- 14 compound components. `useSyncExternalStore`, `useTransition`, `useOptimistic`, `use()` for Suspense. React Compiler compatible.
119
+ - **Automatic virtualization** -- Variable-height virtual scrolling kicks in at 100+ items. Handles 100K+ items smoothly with `content-visibility: auto`.
120
+ - **Fuzzy search** -- Built-in TypeScript scorer with incremental filtering. Optional [WASM engine](https://www.npmjs.com/package/modern-cmdk-search-wasm) for sub-1ms on 100K items.
121
+ - **Frecency ranking** -- Frequency x recency with `Temporal`-based exponential decay. Pluggable persistence (memory or IndexedDB).
122
+ - **Keyboard shortcuts** -- Built-in registry with cross-platform `Mod` key, conflict detection, and `RegExp.escape` parsing.
123
+ - **Full accessibility** -- WAI-ARIA combobox pattern, `aria-live` announcements, `forced-colors`, `prefers-contrast`, `prefers-reduced-motion`.
124
+ - **GPU-composited animations** -- `@starting-style` entry, `scroll-timeline` progress, spring `linear()` easing. All customizable via CSS custom properties.
125
+ - **Error boundary** -- `CommandErrorBoundary` with static or render-function fallback.
126
+ - **DevTools** -- `useCommandDevtools()` exposes machine state via `CustomEvent` for browser inspection.
127
+ - **Telemetry middleware** -- Pluggable hooks for palette open/close, search, and item selection analytics.
128
+ - **ESM-only** -- Zero CommonJS. Tree-shakeable. `sideEffects: false`. Isolated declarations.
129
+
130
+ ---
131
+
132
+ ## React Components
133
+
134
+ ```tsx
135
+ <Command> {/* Root -- state machine provider */}
136
+ <Command.Input /> {/* Search input */}
137
+ <Command.List> {/* Scrollable list with auto-virtualization */}
138
+ <Command.Empty /> {/* Shown when filteredCount === 0 */}
139
+ <Command.Loading /> {/* Shown when loading */}
140
+ <Command.Group> {/* Labeled group of items */}
141
+ <Command.Item> {/* Selectable item */}
142
+ <Command.Highlight /> {/* Fuzzy match highlighting */}
143
+ <Command.Badge /> {/* Status badge */}
144
+ <Command.Shortcut /> {/* Keyboard shortcut display */}
145
+ </Command.Item>
146
+ </Command.Group>
147
+ <Command.Separator />
148
+ </Command.List>
149
+ </Command>
150
+
151
+ <Command.Dialog /> {/* Radix Dialog + overlay + portal */}
152
+ <Command.Page /> {/* Nested page navigation */}
153
+ <Command.AsyncItems /> {/* Suspense-powered async loading */}
154
+ ```
155
+
156
+ ---
157
+
158
+ ## Core API (No React)
159
+
160
+ Use the core engine standalone in any framework or runtime:
161
+
162
+ ```ts
163
+ import { createCommandMachine, itemId } from 'modern-cmdk';
164
+
165
+ using machine = createCommandMachine({
166
+ items: [
167
+ { id: itemId('copy'), value: 'Copy', shortcut: 'Mod+C', onSelect: () => copy() },
168
+ { id: itemId('paste'), value: 'Paste', shortcut: 'Mod+V', onSelect: () => paste() },
169
+ ],
170
+ frecency: { enabled: true },
171
+ loop: true,
172
+ });
173
+
174
+ machine.send({ type: 'SEARCH_CHANGE', query: 'cop' });
175
+ machine.send({ type: 'NAVIGATE', direction: 'next' });
176
+ machine.send({ type: 'ITEM_SELECT', id: itemId('copy') });
177
+
178
+ machine.subscribe(() => {
179
+ console.log(machine.getState());
180
+ });
181
+ // Automatic cleanup via `using` -- no manual dispose needed
182
+ ```
183
+
184
+ ---
185
+
186
+ ## Performance
187
+
188
+ | Benchmark | Target | Measured |
189
+ |---|---|---|
190
+ | Search 10K items (TS scorer) | < 16 ms | ~8.2 ms |
191
+ | Search 100K items (WASM) | < 1 ms | ~0.7 ms |
192
+ | Filter 10K (incremental) | < 2 ms | ~1.1 ms |
193
+ | State update cycle | < 4 ms | ~2.3 ms |
194
+ | Core bundle (gzipped) | <= 6 KB | ~5.2 KB |
195
+ | React bundle (gzipped) | <= 25 KB | ~6.3 KB |
196
+
197
+ CI enforces 5% warning / 15% failure regression thresholds with 3-run averaging.
198
+
199
+ ---
200
+
201
+ ## CSS Customization
202
+
203
+ All animations and dimensions are customizable via CSS custom properties:
204
+
205
+ ```css
206
+ [data-command-root] {
207
+ --command-duration-enter: 200ms;
208
+ --command-duration-exit: 150ms;
209
+ --command-scale-from: 0.96;
210
+ --command-overlay-blur: 4px;
211
+ --command-dialog-radius: 12px;
212
+ --command-dialog-max-width: 640px;
213
+ --command-item-height: 44px;
214
+ --command-highlight-color: oklch(0.85 0.15 90 / 0.3);
215
+ }
216
+ ```
217
+
218
+ All styles use `@layer command` for zero-specificity conflicts and CSS logical properties for full RTL support.
219
+
220
+ ---
221
+
222
+ ## Migration from cmdk
223
+
224
+ Automated codemods handle the most common migration patterns:
225
+
226
+ ```bash
227
+ npx modern-cmdk --transform import-rewrite ./src
228
+ npx modern-cmdk --transform data-attrs ./src
229
+ npx modern-cmdk --transform forward-ref ./src
230
+ npx modern-cmdk --transform should-filter ./src
231
+ ```
232
+
233
+ See the [full migration guide](https://command.crimson.dev/guide/migration-from-cmdk).
234
+
235
+ ---
236
+
237
+ ## Documentation
238
+
239
+ **[command.crimson.dev](https://command.crimson.dev)**
240
+
241
+ **Guides:** [Getting Started](https://command.crimson.dev/guide/getting-started) | [Installation](https://command.crimson.dev/guide/installation) | [Basic Usage](https://command.crimson.dev/guide/basic-usage) | [Async Items](https://command.crimson.dev/guide/async-items) | [WASM Search](https://command.crimson.dev/guide/wasm-search) | [Frecency](https://command.crimson.dev/guide/frecency) | [Shortcuts](https://command.crimson.dev/guide/shortcuts) | [Virtualization](https://command.crimson.dev/guide/virtualization) | [SSR / Next.js](https://command.crimson.dev/guide/ssr) | [Theming](https://command.crimson.dev/guide/theming) | [Accessibility](https://command.crimson.dev/guide/accessibility)
242
+
243
+ **Recipes:** [File Picker](https://command.crimson.dev/recipes/file-picker) | [Emoji Picker](https://command.crimson.dev/recipes/emoji-picker) | [AI Chat Commands](https://command.crimson.dev/recipes/ai-chat-commands) | [Nested Commands](https://command.crimson.dev/recipes/nested-commands) | [Spotlight Search](https://command.crimson.dev/recipes/spotlight-search)
244
+
245
+ **API:** [Core Engine](https://command.crimson.dev/api/command) | [React Adapter](https://command.crimson.dev/api/command-react) | [WASM Search](https://command.crimson.dev/api/command-search-wasm)
246
+
247
+ ---
248
+
249
+ ## Related Packages
250
+
251
+ | Package | Description |
252
+ |---|---|
253
+ | [`create-modern-cmdk`](https://www.npmjs.com/package/create-modern-cmdk) | `npm create modern-cmdk` -- scaffold a new project |
254
+ | [`modern-cmdk-search-wasm`](https://www.npmjs.com/package/modern-cmdk-search-wasm) | Rust/WASM fuzzy search -- sub-1ms on 100K items |
255
+ | [`vscode-modern-cmdk`](https://marketplace.visualstudio.com/items?itemName=crimson-dev.vscode-modern-cmdk) | VS Code snippets extension |
256
+
257
+ ---
258
+
259
+ ## License
260
+
261
+ [MIT](https://github.com/ABCrimson/modern-cmdk/blob/main/LICENSE) -- Copyright (c) 2026 Crimson Dev
@@ -1,2 +1,2 @@
1
- import { A as DEFAULT_FRECENCY_DECAY, B as groupId, C as computeFrecencyBonus, D as CommandItem, E as CommandGroup, F as FrecencyRecord, H as ScorerFn, I as FrecencyStorage, L as GroupId, M as FrecencyData, N as FrecencyDecayConfig, O as CommandMachineOptions, P as FrecencyOptions, R as ItemId, S as FrecencyEngine, T as CommandEvent, U as SearchEngine, V as itemId, W as SearchResult, _ as matchesShortcut, a as TelemetryMiddleware, b as parseShortcut, c as batchScoreItems, d as CommandMachine, f as createCommandMachine, g as findMatchingShortcut, h as detectConflicts, i as CommandTelemetryHooks, j as DEFAULT_MACHINE_OPTIONS, k as CommandState, l as scoreItemAsync, m as KeyboardShortcutRegistry, n as createScheduler, o as createTelemetryMiddleware, p as CommandRegistry, r as TypedEmitter, s as createSearchEngine, t as Scheduler, u as scoreItem, v as ParsedShortcut, w as IdbFrecencyStorage, x as MemoryFrecencyStorage, y as formatShortcut, z as createInitialState } from "../index-Ctz7l29k.mjs";
1
+ import { A as DEFAULT_FRECENCY_DECAY, B as groupId, C as computeFrecencyBonus, D as CommandItem, E as CommandGroup, F as FrecencyRecord, H as ScorerFn, I as FrecencyStorage, L as GroupId, M as FrecencyData, N as FrecencyDecayConfig, O as CommandMachineOptions, P as FrecencyOptions, R as ItemId, S as FrecencyEngine, T as CommandEvent, U as SearchEngine, V as itemId, W as SearchResult, _ as matchesShortcut, a as TelemetryMiddleware, b as parseShortcut, c as batchScoreItems, d as CommandMachine, f as createCommandMachine, g as findMatchingShortcut, h as detectConflicts, i as CommandTelemetryHooks, j as DEFAULT_MACHINE_OPTIONS, k as CommandState, l as scoreItemAsync, m as KeyboardShortcutRegistry, n as createScheduler, o as createTelemetryMiddleware, p as CommandRegistry, r as TypedEmitter, s as createSearchEngine, t as Scheduler, u as scoreItem, v as ParsedShortcut, w as IdbFrecencyStorage, x as MemoryFrecencyStorage, y as formatShortcut, z as createInitialState } from "../index-C6g94P7V.mjs";
2
2
  export { CommandEvent, CommandGroup, CommandItem, CommandMachine, CommandMachineOptions, CommandRegistry, CommandState, CommandTelemetryHooks, DEFAULT_FRECENCY_DECAY, DEFAULT_MACHINE_OPTIONS, FrecencyData, FrecencyDecayConfig, FrecencyEngine, FrecencyOptions, FrecencyRecord, FrecencyStorage, GroupId, IdbFrecencyStorage, ItemId, KeyboardShortcutRegistry, MemoryFrecencyStorage, ParsedShortcut, Scheduler, ScorerFn, SearchEngine, SearchResult, TelemetryMiddleware, TypedEmitter, batchScoreItems, computeFrecencyBonus, createCommandMachine, createInitialState, createScheduler, createSearchEngine, createTelemetryMiddleware, detectConflicts, findMatchingShortcut, formatShortcut, groupId, itemId, matchesShortcut, parseShortcut, scoreItem, scoreItemAsync };
@@ -1,2 +1,2 @@
1
- import { C as itemId, S as groupId, _ as computeFrecencyBonus, a as createScheduler, b as DEFAULT_MACHINE_OPTIONS, c as scoreItem, d as formatShortcut, f as parseShortcut, g as FrecencyEngine, h as matchesShortcut, i as createCommandMachine, l as CommandRegistry, m as findMatchingShortcut, n as batchScoreItems, o as TypedEmitter, p as detectConflicts, r as scoreItemAsync, s as createSearchEngine, t as createTelemetryMiddleware, u as KeyboardShortcutRegistry, v as MemoryFrecencyStorage, w as IdbFrecencyStorage, x as createInitialState, y as DEFAULT_FRECENCY_DECAY } from "../core-Bq0eDvlc.mjs";
1
+ import { C as itemId, S as groupId, _ as computeFrecencyBonus, a as createScheduler, b as DEFAULT_MACHINE_OPTIONS, c as scoreItem, d as formatShortcut, f as parseShortcut, g as FrecencyEngine, h as matchesShortcut, i as createCommandMachine, l as CommandRegistry, m as findMatchingShortcut, n as batchScoreItems, o as TypedEmitter, p as detectConflicts, r as scoreItemAsync, s as createSearchEngine, t as createTelemetryMiddleware, u as KeyboardShortcutRegistry, v as MemoryFrecencyStorage, w as IdbFrecencyStorage, x as createInitialState, y as DEFAULT_FRECENCY_DECAY } from "../core-CgQYpHK3.mjs";
2
2
  export { CommandRegistry, DEFAULT_FRECENCY_DECAY, DEFAULT_MACHINE_OPTIONS, FrecencyEngine, IdbFrecencyStorage, KeyboardShortcutRegistry, MemoryFrecencyStorage, TypedEmitter, batchScoreItems, computeFrecencyBonus, createCommandMachine, createInitialState, createScheduler, createSearchEngine, createTelemetryMiddleware, detectConflicts, findMatchingShortcut, formatShortcut, groupId, itemId, matchesShortcut, parseShortcut, scoreItem, scoreItemAsync };
@@ -45,10 +45,10 @@ var IdbFrecencyStorage = class {
45
45
  /**
46
46
  * Create an IDB-backed frecency storage.
47
47
  *
48
- * @param dbName — IndexedDB database name (default: 'crimson-command')
48
+ * @param dbName — IndexedDB database name (default: 'modern-cmdk')
49
49
  * @param storeName — IndexedDB object store name (default: 'frecency')
50
50
  */
51
- constructor(dbName = "crimson-command", storeName = "frecency") {
51
+ constructor(dbName = "modern-cmdk", storeName = "frecency") {
52
52
  this.#store = createStore(dbName, storeName);
53
53
  }
54
54
  /** Load frecency data from IndexedDB for the given namespace */
@@ -141,6 +141,8 @@ var MemoryFrecencyStorage = class {
141
141
  };
142
142
  //#endregion
143
143
  //#region src/core/frecency/index.ts
144
+ /** Shared empty bonus map — avoids allocation when no frecency records exist */
145
+ const EMPTY_BONUS_MAP = /* @__PURE__ */ new Map();
144
146
  /** Pre-computed bucket boundaries in hours — avoids Duration.total() per call */
145
147
  const BUCKET_HOURS = {
146
148
  hour: 1,
@@ -187,9 +189,7 @@ var FrecencyEngine = class {
187
189
  frequency: (this.#data.records.get(itemId)?.frequency ?? 0) + 1,
188
190
  lastUsed: now
189
191
  };
190
- const newRecords = new Map(this.#data.records);
191
- newRecords.set(itemId, updated);
192
- this.#data = { records: newRecords };
192
+ this.#data.records.set(itemId, updated);
193
193
  this.#dirty = true;
194
194
  }
195
195
  /** Get frecency bonus for an item */
@@ -200,6 +200,7 @@ var FrecencyEngine = class {
200
200
  }
201
201
  /** Get all frecency bonuses as a map — for...of for hot ranking path (no closure overhead) */
202
202
  getAllBonuses(now = Temporal.Now.instant()) {
203
+ if (this.#data.records.size === 0) return EMPTY_BONUS_MAP;
203
204
  const bonuses = /* @__PURE__ */ new Map();
204
205
  for (const [id, record] of this.#data.records) bonuses.set(id, computeFrecencyBonus(record, now, this.#config));
205
206
  return bonuses;
@@ -277,7 +278,6 @@ const MODIFIER_MAP = {
277
278
  };
278
279
  /**
279
280
  * Parse a shortcut string like "Mod+Shift+K" into a structured ParsedShortcut.
280
- * Uses RegExp.escape (ES2026) for safe pattern construction from user-provided strings.
281
281
  * Uses String.isWellFormed() (ES2026) to ensure valid Unicode input.
282
282
  */
283
283
  function parseShortcut(shortcut) {
@@ -368,11 +368,9 @@ var KeyboardShortcutRegistry = class {
368
368
  const shortcut = parseShortcut(shortcutStr);
369
369
  this.#bindings.delete(shortcut.normalized);
370
370
  }
371
- /** Unregister all shortcuts for a given item — for...of with deferred delete (avoids mutation during iteration) */
371
+ /** Unregister all shortcuts for a given item — Map spec allows deletion during iteration */
372
372
  unregisterByItem(itemId) {
373
- const keysToDelete = [];
374
- for (const [key, binding] of this.#bindings) if (binding.itemId === itemId) keysToDelete.push(key);
375
- for (const key of keysToDelete) this.#bindings.delete(key);
373
+ for (const [key, binding] of this.#bindings) if (binding.itemId === itemId) this.#bindings.delete(key);
376
374
  }
377
375
  /** Check for conflicting shortcuts */
378
376
  getConflicts() {
@@ -423,9 +421,13 @@ var CommandRegistry = class {
423
421
  #itemOrder = [];
424
422
  #itemOrderSet = /* @__PURE__ */ new Set();
425
423
  #cachedItems = null;
424
+ #cachedGroups = null;
426
425
  #invalidateCache() {
427
426
  this.#cachedItems = null;
428
427
  }
428
+ #invalidateGroupCache() {
429
+ this.#cachedGroups = null;
430
+ }
429
431
  /** Register an item — returns Disposable for `using` auto-deregister */
430
432
  registerItem(item) {
431
433
  this.#items.set(item.id, item);
@@ -471,13 +473,16 @@ var CommandRegistry = class {
471
473
  /** Register a group */
472
474
  registerGroup(group) {
473
475
  this.#groups.set(group.id, group);
476
+ this.#invalidateGroupCache();
474
477
  return { [Symbol.dispose]: () => {
475
478
  this.#groups.delete(group.id);
479
+ this.#invalidateGroupCache();
476
480
  } };
477
481
  }
478
482
  /** Unregister a group */
479
483
  unregisterGroup(id) {
480
484
  this.#groups.delete(id);
485
+ this.#invalidateGroupCache();
481
486
  }
482
487
  /** Get an item by ID — O(1) */
483
488
  getItem(id) {
@@ -493,9 +498,11 @@ var CommandRegistry = class {
493
498
  this.#cachedItems = this.#itemOrder.values().map((id) => this.#items.get(id)).filter((item) => item != null).toArray();
494
499
  return this.#cachedItems;
495
500
  }
496
- /** Get all registered groups sorted by priority */
501
+ /** Get all registered groups sorted by priority — cached between group mutations */
497
502
  getGroups() {
498
- return this.#groups.values().toArray().sort((a, b) => (a.priority ?? 0) - (b.priority ?? 0));
503
+ if (this.#cachedGroups !== null) return this.#cachedGroups;
504
+ this.#cachedGroups = this.#groups.values().toArray().sort((a, b) => (a.priority ?? 0) - (b.priority ?? 0));
505
+ return this.#cachedGroups;
499
506
  }
500
507
  /** Get items grouped by groupId using Map.groupBy (ES2026) — direct Map, no Object.entries conversion */
501
508
  getGroupedItems() {
@@ -557,6 +564,7 @@ var CommandRegistry = class {
557
564
  this.#itemOrder = [];
558
565
  this.#itemOrderSet.clear();
559
566
  this.#invalidateCache();
567
+ this.#invalidateGroupCache();
560
568
  }
561
569
  [Symbol.dispose]() {
562
570
  this.clear();
@@ -763,7 +771,7 @@ function createSearchEngine(options) {
763
771
  }));
764
772
  }
765
773
  const lowerQuery = query.toLowerCase();
766
- const candidateItems = query.startsWith(previousQuery) && previousQuery.length > 0 ? items.filter((item) => previousResults.has(item.id)) : items;
774
+ const candidateItems = query.startsWith(previousQuery) && previousQuery.length > 0 ? items.values().filter((item) => previousResults.has(item.id)) : items;
767
775
  const results = [];
768
776
  if (useDefaultScorer) for (const item of candidateItems) {
769
777
  const entry = indexedEntries.get(item.id);
@@ -908,6 +916,7 @@ function createScheduler() {
908
916
  function createCommandMachine(options = {}) {
909
917
  let state = createInitialState(options);
910
918
  let filteredIdSet = /* @__PURE__ */ new Set();
919
+ let filteredIdIndex = /* @__PURE__ */ new Map();
911
920
  const registry = new CommandRegistry();
912
921
  const searchEngine = options.search ?? createSearchEngine(typeof options.filter === "function" ? { scorer: (query, item) => {
913
922
  const score = options.filter(item, query);
@@ -953,7 +962,13 @@ function createCommandMachine(options = {}) {
953
962
  }
954
963
  filteredIds = results.map((r) => r.id);
955
964
  }
956
- filteredIdSet = new Set(filteredIds);
965
+ filteredIdSet = /* @__PURE__ */ new Set();
966
+ filteredIdIndex = /* @__PURE__ */ new Map();
967
+ for (let i = 0; i < filteredIds.length; i++) {
968
+ const id = filteredIds[i];
969
+ filteredIdSet.add(id);
970
+ filteredIdIndex.set(id, i);
971
+ }
957
972
  const groupedIds = Map.groupBy(filteredIds, (id) => {
958
973
  return registry.getItem(id)?.groupId ?? "__ungrouped";
959
974
  });
@@ -996,7 +1011,7 @@ function createCommandMachine(options = {}) {
996
1011
  return;
997
1012
  }
998
1013
  const loop = options.loop ?? true;
999
- const currentIdx = activeId ? filteredIds.indexOf(activeId) : -1;
1014
+ const currentIdx = activeId ? filteredIdIndex.get(activeId) ?? -1 : -1;
1000
1015
  let newIdx;
1001
1016
  if (direction === "next") if (currentIdx === -1 || currentIdx >= filteredIds.length - 1) newIdx = loop ? 0 : filteredIds.length - 1;
1002
1017
  else newIdx = currentIdx + 1;
@@ -1139,6 +1154,9 @@ function createCommandMachine(options = {}) {
1139
1154
  getKeyboardRegistry() {
1140
1155
  return keyboardRegistry;
1141
1156
  },
1157
+ getFilteredIdSet() {
1158
+ return filteredIdSet;
1159
+ },
1142
1160
  [Symbol.dispose]() {
1143
1161
  scheduler[Symbol.dispose]();
1144
1162
  registry[Symbol.dispose]();
@@ -1163,7 +1181,7 @@ async function scoreItemAsync(query, item) {
1163
1181
  * Enables streaming consumption and lazy evaluation of scoring operations.
1164
1182
  */
1165
1183
  async function* scoreItemsGenerator(query, items) {
1166
- for (const item of items) yield Promise.try(() => scoreItem(query, item));
1184
+ for (const item of items) yield Promise.try(() => scoreItem(query, item)).catch(() => null);
1167
1185
  }
1168
1186
  /**
1169
1187
  * Batch score items using Array.fromAsync (ES2026) with an async generator pipeline.
@@ -1214,4 +1232,4 @@ function createTelemetryMiddleware(hooks) {
1214
1232
  //#endregion
1215
1233
  export { itemId as C, groupId as S, computeFrecencyBonus as _, createScheduler as a, DEFAULT_MACHINE_OPTIONS as b, scoreItem as c, formatShortcut as d, parseShortcut as f, FrecencyEngine as g, matchesShortcut as h, createCommandMachine as i, CommandRegistry as l, findMatchingShortcut as m, batchScoreItems as n, TypedEmitter as o, detectConflicts as p, scoreItemAsync as r, createSearchEngine as s, createTelemetryMiddleware as t, KeyboardShortcutRegistry as u, MemoryFrecencyStorage as v, IdbFrecencyStorage as w, createInitialState as x, DEFAULT_FRECENCY_DECAY as y };
1216
1234
 
1217
- //# sourceMappingURL=core-Bq0eDvlc.mjs.map
1235
+ //# sourceMappingURL=core-CgQYpHK3.mjs.map