modern-cmdk 1.0.2 → 1.1.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.
Files changed (30) hide show
  1. package/dist/codemod/cli.mjs +1 -3
  2. package/dist/codemod/transforms/data-attrs.d.mts +1 -2
  3. package/dist/codemod/transforms/data-attrs.mjs +0 -2
  4. package/dist/codemod/transforms/forward-ref.d.mts +1 -2
  5. package/dist/codemod/transforms/forward-ref.mjs +0 -2
  6. package/dist/codemod/transforms/import-rewrite.d.mts +1 -2
  7. package/dist/codemod/transforms/import-rewrite.mjs +0 -2
  8. package/dist/codemod/transforms/should-filter.d.mts +1 -2
  9. package/dist/codemod/transforms/should-filter.mjs +0 -2
  10. package/dist/core/index.d.mts +568 -2
  11. package/dist/core/index.mjs +1268 -1
  12. package/dist/react/index.d.mts +349 -3
  13. package/dist/react/index.mjs +1292 -209
  14. package/package.json +26 -12
  15. package/dist/codemod/cli.mjs.map +0 -1
  16. package/dist/codemod/transforms/data-attrs.d.mts.map +0 -1
  17. package/dist/codemod/transforms/data-attrs.mjs.map +0 -1
  18. package/dist/codemod/transforms/forward-ref.d.mts.map +0 -1
  19. package/dist/codemod/transforms/forward-ref.mjs.map +0 -1
  20. package/dist/codemod/transforms/import-rewrite.d.mts.map +0 -1
  21. package/dist/codemod/transforms/import-rewrite.mjs.map +0 -1
  22. package/dist/codemod/transforms/should-filter.d.mts.map +0 -1
  23. package/dist/codemod/transforms/should-filter.mjs.map +0 -1
  24. package/dist/core-CgQYpHK3.mjs +0 -1235
  25. package/dist/core-CgQYpHK3.mjs.map +0 -1
  26. package/dist/index-C6g94P7V.d.mts +0 -563
  27. package/dist/index-C6g94P7V.d.mts.map +0 -1
  28. package/dist/react/index.d.mts.map +0 -1
  29. package/dist/react/index.mjs.map +0 -1
  30. /package/{src → dist}/react/styles.css +0 -0
@@ -1,8 +1,9 @@
1
- import { C as itemId, S as groupId, d as formatShortcut, f as parseShortcut, i as createCommandMachine } from "../core-CgQYpHK3.mjs";
1
+ 'use client';
2
2
  import * as React from "react";
3
3
  import { Component, Suspense, createContext, use, useCallback, useEffect, useId, useLayoutEffect, useMemo, useOptimistic, useRef, useState, useSyncExternalStore, useTransition } from "react";
4
4
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
5
5
  import { Dialog } from "radix-ui";
6
+ import { createPortal } from "react-dom";
6
7
  //#region src/react/activity.tsx
7
8
  const ActivityComponent = "Activity" in React ? React.Activity : null;
8
9
  /**
@@ -24,20 +25,28 @@ const CommandStableContext = createContext(null);
24
25
  CommandStableContext.displayName = "CommandStableContext";
25
26
  const CommandStateContext = createContext(null);
26
27
  CommandStateContext.displayName = "CommandStateContext";
27
- /**
28
- * @deprecated Use CommandStableContext and CommandStateContext instead.
29
- * Kept for backward compatibility with useCommandState hook.
30
- */
31
- const CommandContext = createContext(null);
32
- CommandContext.displayName = "CommandContext";
28
+ /** Consume stable context with a descriptive error — replaces 3-line boilerplate in every component */
29
+ function useStableContext(component) {
30
+ const ctx = use(CommandStableContext);
31
+ if (!ctx) throw new Error(`${component} must be used within a <Command> component`);
32
+ return ctx;
33
+ }
34
+ /** Consume state context with a descriptive error — replaces 3-line boilerplate in every component */
35
+ function useStateContext(component) {
36
+ const ctx = use(CommandStateContext);
37
+ if (!ctx) throw new Error(`${component} must be used within a <Command> component`);
38
+ return ctx;
39
+ }
33
40
  const CommandGroupContext = createContext(null);
34
41
  CommandGroupContext.displayName = "CommandGroupContext";
42
+ /** Context for the status container rendered outside the listbox — used by Command.Empty */
43
+ const CommandListStatusContext = createContext(null);
44
+ CommandListStatusContext.displayName = "CommandListStatusContext";
35
45
  //#endregion
36
46
  //#region src/react/async-items.tsx
37
47
  /** Inner component that suspends via use() until items resolve */
38
48
  function AsyncItemsInner({ items: itemsPromise, children }) {
39
- const stable = use(CommandStableContext);
40
- if (!stable) throw new Error("Command.AsyncItems must be used within a <Command> component");
49
+ const stable = useStableContext("Command.AsyncItems");
41
50
  const items = use(itemsPromise);
42
51
  useLayoutEffect(() => {
43
52
  stable.machine.send({
@@ -47,6 +56,7 @@ function AsyncItemsInner({ items: itemsPromise, children }) {
47
56
  }, [stable.machine, items]);
48
57
  return /* @__PURE__ */ jsx(Fragment, { children: children(items) });
49
58
  }
59
+ /** Suspense-based async item loader that resolves a promise of items using React 19 `use()`. */
50
60
  function CommandAsyncItems({ items, fallback, children }) {
51
61
  const stableRef = useRef(items);
52
62
  if (stableRef.current !== items) stableRef.current = items;
@@ -60,6 +70,7 @@ function CommandAsyncItems({ items, fallback, children }) {
60
70
  }
61
71
  //#endregion
62
72
  //#region src/react/badge.tsx
73
+ /** Inline badge for categorization or status indicators on command items. */
63
74
  function CommandBadge({ ref, children, ...props }) {
64
75
  return /* @__PURE__ */ jsx("span", {
65
76
  ref,
@@ -69,13 +80,1078 @@ function CommandBadge({ ref, children, ...props }) {
69
80
  });
70
81
  }
71
82
  //#endregion
83
+ //#region src/core/types.ts
84
+ /** Create a branded {@link ItemId} from a plain string. Ensures well-formed Unicode (ES2026). */
85
+ function itemId(id) {
86
+ return id.isWellFormed() ? id : id.toWellFormed();
87
+ }
88
+ /** Create a branded {@link GroupId} from a plain string. Ensures well-formed Unicode (ES2026). */
89
+ function groupId(id) {
90
+ return id.isWellFormed() ? id : id.toWellFormed();
91
+ }
92
+ /** Default frecency decay configuration */
93
+ const DEFAULT_FRECENCY_DECAY = {
94
+ hourWeight: 4,
95
+ dayWeight: 2,
96
+ weekWeight: 1.5,
97
+ monthWeight: 1,
98
+ olderWeight: .5
99
+ };
100
+ /** Initial state factory */
101
+ function createInitialState(options) {
102
+ return {
103
+ search: "",
104
+ activeId: null,
105
+ filteredIds: [],
106
+ groupedIds: /* @__PURE__ */ new Map(),
107
+ filteredCount: 0,
108
+ loading: false,
109
+ page: "root",
110
+ pageStack: [],
111
+ open: options?.open ?? false,
112
+ lastUpdated: Temporal.Now.instant()
113
+ };
114
+ }
115
+ //#endregion
116
+ //#region src/core/frecency/memory-storage.ts
117
+ /**
118
+ * In-memory implementation of FrecencyStorage using a Map.
119
+ * Data does not persist across page reloads. Suitable for ephemeral
120
+ * sessions or as the default when no persistent storage is configured.
121
+ */
122
+ var MemoryFrecencyStorage = class {
123
+ #store = /* @__PURE__ */ new Map();
124
+ load(namespace) {
125
+ return this.#store.get(namespace) ?? { records: /* @__PURE__ */ new Map() };
126
+ }
127
+ save(namespace, data) {
128
+ this.#store.set(namespace, data);
129
+ }
130
+ /** Serialize all stored data — Iterator Helpers pipeline (ES2026) */
131
+ toJSON() {
132
+ return Object.fromEntries(this.#store.entries().map(([namespace, data]) => [namespace, { records: data.records.entries().toArray() }]));
133
+ }
134
+ [Symbol.dispose]() {
135
+ this.#store.clear();
136
+ }
137
+ };
138
+ //#endregion
139
+ //#region src/core/frecency/index.ts
140
+ /** Shared empty bonus map — avoids allocation when no frecency records exist */
141
+ const EMPTY_BONUS_MAP = /* @__PURE__ */ new Map();
142
+ /** Pre-computed bucket boundaries in hours — avoids Duration.total() per call */
143
+ const BUCKET_HOURS = {
144
+ hour: 1,
145
+ day: 24,
146
+ week: 168,
147
+ month: 720
148
+ };
149
+ /**
150
+ * Computes a frecency bonus for an item based on usage frequency and recency.
151
+ * Uses Temporal.Duration bucket boundaries (hour/day/week/month) with
152
+ * configurable exponential decay weights per time bucket.
153
+ */
154
+ function computeFrecencyBonus(history, now = Temporal.Now.instant(), config = DEFAULT_FRECENCY_DECAY) {
155
+ const hours = now.since(history.lastUsed).total("hours");
156
+ const { hourWeight = 4, dayWeight = 2, weekWeight = 1.5, monthWeight = 1, olderWeight = .5 } = config;
157
+ const recencyWeight = hours < BUCKET_HOURS.hour ? hourWeight : hours < BUCKET_HOURS.day ? dayWeight : hours < BUCKET_HOURS.week ? weekWeight : hours < BUCKET_HOURS.month ? monthWeight : olderWeight;
158
+ return history.frequency * recencyWeight;
159
+ }
160
+ /**
161
+ * Stateful frecency engine that tracks item selection history and provides
162
+ * ranking boosts for search results. Supports pluggable storage backends
163
+ * and automatic flush on dispose. Uses Temporal.Now.instant() for timestamps.
164
+ */
165
+ var FrecencyEngine = class {
166
+ #storage;
167
+ #namespace;
168
+ #data;
169
+ #config;
170
+ #dirty = false;
171
+ constructor(options) {
172
+ this.#storage = options.storage ?? new MemoryFrecencyStorage();
173
+ this.#namespace = options.namespace ?? "default";
174
+ this.#config = options.decayConfig ?? DEFAULT_FRECENCY_DECAY;
175
+ this.#data = { records: /* @__PURE__ */ new Map() };
176
+ }
177
+ /** Load frecency data from storage */
178
+ async load() {
179
+ this.#data = await this.#storage.load(this.#namespace);
180
+ }
181
+ /** Record a selection event for an item */
182
+ recordSelection(itemId) {
183
+ const now = Temporal.Now.instant();
184
+ const updated = {
185
+ frequency: (this.#data.records.get(itemId)?.frequency ?? 0) + 1,
186
+ lastUsed: now
187
+ };
188
+ this.#data.records.set(itemId, updated);
189
+ this.#dirty = true;
190
+ }
191
+ /** Get frecency bonus for an item */
192
+ getBonus(itemId, now) {
193
+ const record = this.#data.records.get(itemId);
194
+ if (!record) return 0;
195
+ return computeFrecencyBonus(record, now, this.#config);
196
+ }
197
+ /** Get all frecency bonuses as a map — for...of for hot ranking path (no closure overhead) */
198
+ getAllBonuses(now = Temporal.Now.instant()) {
199
+ if (this.#data.records.size === 0) return EMPTY_BONUS_MAP;
200
+ const bonuses = /* @__PURE__ */ new Map();
201
+ for (const [id, record] of this.#data.records) bonuses.set(id, computeFrecencyBonus(record, now, this.#config));
202
+ return bonuses;
203
+ }
204
+ /** Get the decay configuration */
205
+ getConfig() {
206
+ return this.#config;
207
+ }
208
+ /** Update decay configuration at runtime */
209
+ setConfig(config) {
210
+ this.#config = {
211
+ ...this.#config,
212
+ ...config
213
+ };
214
+ }
215
+ /** Flush dirty data to storage */
216
+ async flush() {
217
+ if (!this.#dirty) return;
218
+ await this.#storage.save(this.#namespace, this.#data);
219
+ this.#dirty = false;
220
+ }
221
+ /** Get the raw frecency data */
222
+ getData() {
223
+ return this.#data;
224
+ }
225
+ /** Async dispose — flushes dirty data before cleanup. Use with `await using`. */
226
+ async [Symbol.asyncDispose]() {
227
+ if (this.#dirty) {
228
+ await this.#storage.save(this.#namespace, this.#data);
229
+ this.#dirty = false;
230
+ }
231
+ this.#storage[Symbol.dispose]();
232
+ }
233
+ [Symbol.dispose]() {
234
+ if (this.#dirty) Promise.try(() => this.#storage.save(this.#namespace, this.#data)).catch((err) => {
235
+ console.warn("[modern-cmdk] FrecencyEngine dispose flush failed:", err);
236
+ });
237
+ this.#storage[Symbol.dispose]();
238
+ }
239
+ };
240
+ //#endregion
241
+ //#region src/core/keyboard/matcher.ts
242
+ /**
243
+ * Tests whether a KeyboardEvent exactly matches a ParsedShortcut,
244
+ * comparing all four modifier keys (meta, ctrl, shift, alt) and the primary key.
245
+ */
246
+ function matchesShortcut(event, shortcut) {
247
+ if (shortcut.meta !== event.metaKey) return false;
248
+ if (shortcut.ctrl !== event.ctrlKey) return false;
249
+ if (shortcut.shift !== event.shiftKey) return false;
250
+ if (shortcut.alt !== event.altKey) return false;
251
+ return event.key.toLowerCase() === shortcut.key;
252
+ }
253
+ /**
254
+ * Detects conflicting shortcut bindings by grouping on their normalized form
255
+ * via Map.groupBy (ES2026). Returns only groups with two or more colliding shortcuts.
256
+ * Map.groupBy returns a Map directly — no Object.entries conversion needed.
257
+ */
258
+ function detectConflicts(shortcuts) {
259
+ const grouped = Map.groupBy(shortcuts, (s) => s.normalized);
260
+ return new Map(grouped.entries().filter(([, group]) => group.length > 1));
261
+ }
262
+ //#endregion
263
+ //#region src/core/keyboard/parser.ts
264
+ const isMac = typeof navigator !== "undefined" && (navigator.userAgentData?.platform === "macOS" || /Mac|iPod|iPhone|iPad/.test(navigator.platform));
265
+ /** Modifier aliases — cross-platform Mod resolution */
266
+ const MODIFIER_MAP = {
267
+ mod: isMac ? "meta" : "ctrl",
268
+ meta: "meta",
269
+ cmd: "meta",
270
+ command: "meta",
271
+ ctrl: "ctrl",
272
+ control: "ctrl",
273
+ shift: "shift",
274
+ alt: "alt",
275
+ option: "alt",
276
+ opt: "alt"
277
+ };
278
+ /**
279
+ * Parse a shortcut string like "Mod+Shift+K" into a structured ParsedShortcut.
280
+ * Uses String.isWellFormed() (ES2026) to ensure valid Unicode input.
281
+ */
282
+ function parseShortcut(shortcut) {
283
+ const safeShortcut = shortcut.isWellFormed() ? shortcut : shortcut.toWellFormed();
284
+ const parts = safeShortcut.split("+").map((p) => p.trim().toLowerCase());
285
+ let meta = false;
286
+ let ctrl = false;
287
+ let shift = false;
288
+ let alt = false;
289
+ let key = "";
290
+ for (const part of parts) {
291
+ const modifier = MODIFIER_MAP[part];
292
+ if (modifier) switch (modifier) {
293
+ case "meta":
294
+ meta = true;
295
+ break;
296
+ case "ctrl":
297
+ ctrl = true;
298
+ break;
299
+ case "shift":
300
+ shift = true;
301
+ break;
302
+ case "alt":
303
+ alt = true;
304
+ break;
305
+ }
306
+ else key = part;
307
+ }
308
+ const modifiers = [];
309
+ if (meta) modifiers.push("meta");
310
+ if (ctrl) modifiers.push("ctrl");
311
+ if (shift) modifiers.push("shift");
312
+ if (alt) modifiers.push("alt");
313
+ modifiers.push(key);
314
+ return {
315
+ key,
316
+ meta,
317
+ ctrl,
318
+ shift,
319
+ alt,
320
+ raw: safeShortcut,
321
+ normalized: modifiers.join("+")
322
+ };
323
+ }
324
+ /**
325
+ * Formats a ParsedShortcut for display using platform-appropriate symbols.
326
+ * On macOS: renders glyphs (⌘, ⇧, ⌥, ⌃). On other platforms: text labels joined by "+".
327
+ */
328
+ function formatShortcut(parsed) {
329
+ const parts = [];
330
+ if (parsed.meta) parts.push(isMac ? "⌘" : "Ctrl");
331
+ if (parsed.ctrl && !(!isMac && parsed.meta)) parts.push(isMac ? "⌃" : "Ctrl");
332
+ if (parsed.shift) parts.push(isMac ? "⇧" : "Shift");
333
+ if (parsed.alt) parts.push(isMac ? "⌥" : "Alt");
334
+ const displayKey = parsed.key.length === 1 ? parsed.key.toUpperCase() : parsed.key.charAt(0).toUpperCase() + parsed.key.slice(1);
335
+ parts.push(displayKey);
336
+ return isMac ? parts.join("") : parts.join("+");
337
+ }
338
+ //#endregion
339
+ //#region src/core/keyboard/index.ts
340
+ /**
341
+ * Manages keyboard shortcut bindings with global keydown listening.
342
+ * Supports `using` for automatic cleanup of both individual bindings and the
343
+ * global document listener. Detects conflicts via Map.groupBy (ES2026).
344
+ */
345
+ var KeyboardShortcutRegistry = class {
346
+ #bindings = /* @__PURE__ */ new Map();
347
+ #globalListener = null;
348
+ #enabled = true;
349
+ constructor(options) {
350
+ this.#enabled = options?.enabled ?? true;
351
+ this.#attachGlobalListener();
352
+ }
353
+ /** Register a shortcut binding — returns Disposable for `using` auto-deregister */
354
+ register(shortcutStr, itemId, handler) {
355
+ const shortcut = parseShortcut(shortcutStr);
356
+ this.#bindings.set(shortcut.normalized, {
357
+ shortcut,
358
+ itemId,
359
+ handler
360
+ });
361
+ return { [Symbol.dispose]: () => {
362
+ this.#bindings.delete(shortcut.normalized);
363
+ } };
364
+ }
365
+ /** Unregister a shortcut by its string representation */
366
+ unregister(shortcutStr) {
367
+ const shortcut = parseShortcut(shortcutStr);
368
+ this.#bindings.delete(shortcut.normalized);
369
+ }
370
+ /** Unregister all shortcuts for a given item — Map spec allows deletion during iteration */
371
+ unregisterByItem(itemId) {
372
+ for (const [key, binding] of this.#bindings) if (binding.itemId === itemId) this.#bindings.delete(key);
373
+ }
374
+ /** Check for conflicting shortcuts */
375
+ getConflicts() {
376
+ return detectConflicts(this.#bindings.values().map((b) => b.shortcut).toArray());
377
+ }
378
+ /** Enable or disable global shortcut listening */
379
+ setEnabled(enabled) {
380
+ this.#enabled = enabled;
381
+ }
382
+ /** Get all registered bindings */
383
+ getBindings() {
384
+ return this.#bindings;
385
+ }
386
+ #attachGlobalListener() {
387
+ if (typeof document === "undefined") return;
388
+ this.#globalListener = (event) => {
389
+ if (!this.#enabled) return;
390
+ for (const binding of this.#bindings.values()) if (matchesShortcut(event, binding.shortcut)) {
391
+ event.preventDefault();
392
+ event.stopPropagation();
393
+ binding.handler();
394
+ return;
395
+ }
396
+ };
397
+ document.addEventListener("keydown", this.#globalListener);
398
+ }
399
+ #detachGlobalListener() {
400
+ if (this.#globalListener && typeof document !== "undefined") {
401
+ document.removeEventListener("keydown", this.#globalListener);
402
+ this.#globalListener = null;
403
+ }
404
+ }
405
+ [Symbol.dispose]() {
406
+ this.#detachGlobalListener();
407
+ this.#bindings.clear();
408
+ }
409
+ };
410
+ //#endregion
411
+ //#region src/core/registry.ts
412
+ /**
413
+ * Manages registration and lookup of command items and groups.
414
+ * Uses ES2026 Set methods (union/intersection/difference) for efficient ID operations.
415
+ * All registration methods return Disposable for automatic cleanup via `using`.
416
+ */
417
+ var CommandRegistry = class {
418
+ #items = /* @__PURE__ */ new Map();
419
+ #groups = /* @__PURE__ */ new Map();
420
+ #itemOrder = [];
421
+ #itemOrderSet = /* @__PURE__ */ new Set();
422
+ #cachedItems = null;
423
+ #cachedGroups = null;
424
+ #invalidateCache() {
425
+ this.#cachedItems = null;
426
+ }
427
+ #invalidateGroupCache() {
428
+ this.#cachedGroups = null;
429
+ }
430
+ /** Register an item — returns Disposable for `using` auto-deregister */
431
+ registerItem(item) {
432
+ this.#items.set(item.id, item);
433
+ if (!this.#itemOrderSet.has(item.id)) {
434
+ this.#itemOrder.push(item.id);
435
+ this.#itemOrderSet.add(item.id);
436
+ }
437
+ this.#invalidateCache();
438
+ return { [Symbol.dispose]: () => {
439
+ this.unregisterItem(item.id);
440
+ } };
441
+ }
442
+ /** Register multiple items at once — single Disposable for batch cleanup. for...of for hot path. */
443
+ registerItems(items) {
444
+ for (const item of items) {
445
+ this.#items.set(item.id, item);
446
+ if (!this.#itemOrderSet.has(item.id)) {
447
+ this.#itemOrder.push(item.id);
448
+ this.#itemOrderSet.add(item.id);
449
+ }
450
+ }
451
+ this.#invalidateCache();
452
+ const ids = new Set(items.values().map((i) => i.id));
453
+ return { [Symbol.dispose]: () => {
454
+ this.unregisterItems(ids);
455
+ } };
456
+ }
457
+ /** Unregister a single item — O(1) Map/Set delete, indexOf+toSpliced for order */
458
+ unregisterItem(id) {
459
+ if (!this.#items.has(id)) return;
460
+ this.#items.delete(id);
461
+ this.#itemOrderSet.delete(id);
462
+ const idx = this.#itemOrder.indexOf(id);
463
+ if (idx !== -1) this.#itemOrder = this.#itemOrder.toSpliced(idx, 1);
464
+ this.#invalidateCache();
465
+ }
466
+ /** Bulk unregister using Set.difference (ES2026) — for...of for hot path */
467
+ unregisterItems(ids) {
468
+ for (const id of ids) this.#items.delete(id);
469
+ this.#itemOrderSet = this.#itemOrderSet.difference(ids);
470
+ this.#itemOrder = this.#itemOrder.filter((id) => !ids.has(id));
471
+ this.#invalidateCache();
472
+ }
473
+ /** Register a group */
474
+ registerGroup(group) {
475
+ this.#groups.set(group.id, group);
476
+ this.#invalidateGroupCache();
477
+ return { [Symbol.dispose]: () => {
478
+ this.#groups.delete(group.id);
479
+ this.#invalidateGroupCache();
480
+ } };
481
+ }
482
+ /** Unregister a group */
483
+ unregisterGroup(id) {
484
+ this.#groups.delete(id);
485
+ this.#invalidateGroupCache();
486
+ }
487
+ /** Get an item by ID — O(1) */
488
+ getItem(id) {
489
+ return this.#items.get(id);
490
+ }
491
+ /** Get a group by ID — O(1) */
492
+ getGroup(id) {
493
+ return this.#groups.get(id);
494
+ }
495
+ /** Get all registered items in insertion order — cached between mutations */
496
+ getItems() {
497
+ if (this.#cachedItems !== null) return this.#cachedItems;
498
+ this.#cachedItems = this.#itemOrder.values().map((id) => this.#items.get(id)).filter((item) => item != null).toArray();
499
+ return this.#cachedItems;
500
+ }
501
+ /** Get all registered groups sorted by priority — cached between group mutations */
502
+ getGroups() {
503
+ if (this.#cachedGroups !== null) return this.#cachedGroups;
504
+ this.#cachedGroups = this.#groups.values().toArray().toSorted((a, b) => (a.priority ?? 0) - (b.priority ?? 0));
505
+ return this.#cachedGroups;
506
+ }
507
+ /** Get items grouped by groupId using Map.groupBy (ES2026) — direct Map, no Object.entries conversion */
508
+ getGroupedItems() {
509
+ const items = this.getItems();
510
+ const grouped = Map.groupBy(items, (item) => item.groupId ?? "__ungrouped");
511
+ const result = /* @__PURE__ */ new Map();
512
+ for (const group of this.getGroups()) {
513
+ const groupItems = grouped.get(group.id);
514
+ if (groupItems != null) result.set(group.id, groupItems);
515
+ }
516
+ const ungrouped = grouped.get("__ungrouped");
517
+ if (ungrouped) result.set("__ungrouped", ungrouped);
518
+ return result;
519
+ }
520
+ /** Get item IDs as a Set — returns internal set (O(1), no allocation) */
521
+ getItemIds() {
522
+ return this.#itemOrderSet;
523
+ }
524
+ /** Find intersection of registered items with a candidate set (ES2026 Set.intersection) */
525
+ intersectWith(candidateIds) {
526
+ return this.#itemOrderSet.intersection(candidateIds);
527
+ }
528
+ /** Find items registered but not in a given set (ES2026 Set.difference) */
529
+ differenceFrom(otherIds) {
530
+ return this.#itemOrderSet.difference(otherIds);
531
+ }
532
+ /** Union of registered IDs with another set (ES2026 Set.union) */
533
+ unionWith(otherIds) {
534
+ return this.#itemOrderSet.union(otherIds);
535
+ }
536
+ /** Check if registered items are a subset of the given set (ES2026 Set.isSubsetOf) */
537
+ isSubsetOf(otherIds) {
538
+ return this.#itemOrderSet.isSubsetOf(otherIds);
539
+ }
540
+ /** Check if registered items are a superset of the given set (ES2026 Set.isSupersetOf) */
541
+ isSupersetOf(otherIds) {
542
+ return this.#itemOrderSet.isSupersetOf(otherIds);
543
+ }
544
+ /** Check if registered items share no IDs with the given set (ES2026 Set.isDisjointFrom) */
545
+ isDisjointFrom(otherIds) {
546
+ return this.#itemOrderSet.isDisjointFrom(otherIds);
547
+ }
548
+ /** Symmetric difference between registered IDs and another set (ES2026 Set.symmetricDifference) */
549
+ symmetricDifferenceWith(otherIds) {
550
+ return this.#itemOrderSet.symmetricDifference(otherIds);
551
+ }
552
+ /** Total number of registered items */
553
+ get size() {
554
+ return this.#items.size;
555
+ }
556
+ /** Total number of registered groups */
557
+ get groupCount() {
558
+ return this.#groups.size;
559
+ }
560
+ /** Clear all items and groups */
561
+ clear() {
562
+ this.#items.clear();
563
+ this.#groups.clear();
564
+ this.#itemOrder = [];
565
+ this.#itemOrderSet.clear();
566
+ this.#invalidateCache();
567
+ this.#invalidateGroupCache();
568
+ }
569
+ [Symbol.dispose]() {
570
+ this.clear();
571
+ }
572
+ };
573
+ //#endregion
574
+ //#region src/core/search/default-scorer.ts
575
+ function isWordSeparator(code) {
576
+ return code === 32 || code === 9 || code === 45 || code === 95 || code === 46 || code === 47 || code === 92;
577
+ }
578
+ /**
579
+ * Internal fast path: called by search engine with pre-lowered query and targets.
580
+ * Eliminates all toLowerCase() calls on the hot path.
581
+ */
582
+ function scoreItemPreLowered(lowerQuery, id, lowerValue, lowerKeywords) {
583
+ let bestScore = 0;
584
+ let bestMatches = null;
585
+ const valueResult = scoreTarget(lowerQuery, lowerValue);
586
+ if (valueResult !== null) {
587
+ bestScore = valueResult.score;
588
+ bestMatches = valueResult.matches;
589
+ if (bestScore >= 1) return {
590
+ id,
591
+ score: 1,
592
+ matches: bestMatches
593
+ };
594
+ }
595
+ if (lowerKeywords !== void 0) for (let i = 0; i < lowerKeywords.length; i++) {
596
+ const result = scoreTarget(lowerQuery, lowerKeywords[i]);
597
+ if (result !== null && result.score > bestScore) {
598
+ bestScore = result.score;
599
+ bestMatches = result.matches;
600
+ if (bestScore >= 1) break;
601
+ }
602
+ }
603
+ return bestMatches !== null ? {
604
+ id,
605
+ score: bestScore,
606
+ matches: bestMatches
607
+ } : null;
608
+ }
609
+ function scoreTarget(query, lowerTarget) {
610
+ const tLen = lowerTarget.length;
611
+ if (tLen === 0) return null;
612
+ const qLen = query.length;
613
+ if (qLen === 0) return {
614
+ score: 1,
615
+ matches: []
616
+ };
617
+ if (qLen > tLen) return null;
618
+ if (lowerTarget === query) return {
619
+ score: 1,
620
+ matches: [[0, qLen]]
621
+ };
622
+ if (lowerTarget.startsWith(query)) return {
623
+ score: .9 + .1 * (qLen / tLen),
624
+ matches: [[0, qLen]]
625
+ };
626
+ const substringIdx = lowerTarget.indexOf(query);
627
+ if (substringIdx !== -1) {
628
+ const positionBonus = 1 - substringIdx / tLen;
629
+ const lengthRatio = qLen / tLen;
630
+ return {
631
+ score: .5 + .3 * positionBonus + .2 * lengthRatio,
632
+ matches: [[substringIdx, substringIdx + qLen]]
633
+ };
634
+ }
635
+ const wordBoundaryResult = scoreWordBoundary(query, qLen, lowerTarget, tLen);
636
+ if (wordBoundaryResult) return wordBoundaryResult;
637
+ return scoreFuzzy(query, qLen, lowerTarget, tLen);
638
+ }
639
+ function scoreWordBoundary(query, qLen, lowerTarget, tLen) {
640
+ let queryIdx = 0;
641
+ const matches = [];
642
+ let totalWeightedLen = 0;
643
+ let i = 0;
644
+ while (i < tLen && queryIdx < qLen) if ((i === 0 || isWordSeparator(lowerTarget.charCodeAt(i - 1))) && lowerTarget.charCodeAt(i) === query.charCodeAt(queryIdx)) {
645
+ const matchStart = i;
646
+ let matchLen = 0;
647
+ while (queryIdx < qLen && i < tLen && !isWordSeparator(lowerTarget.charCodeAt(i)) && lowerTarget.charCodeAt(i) === query.charCodeAt(queryIdx)) {
648
+ queryIdx++;
649
+ i++;
650
+ matchLen++;
651
+ }
652
+ matches.push([matchStart, matchStart + matchLen]);
653
+ totalWeightedLen += matchLen * 2;
654
+ } else i++;
655
+ if (queryIdx !== qLen) return null;
656
+ const maxPossible = qLen * 2;
657
+ return {
658
+ score: .3 + .4 * (totalWeightedLen / maxPossible),
659
+ matches
660
+ };
661
+ }
662
+ function scoreFuzzy(query, qLen, lowerTarget, tLen) {
663
+ let queryIdx = 0;
664
+ let targetIdx = 0;
665
+ const matches = [];
666
+ let currentMatchStart = -1;
667
+ let contiguityScore = 0;
668
+ while (queryIdx < qLen && targetIdx < tLen) if (query.charCodeAt(queryIdx) === lowerTarget.charCodeAt(targetIdx)) {
669
+ if (currentMatchStart === -1) currentMatchStart = targetIdx;
670
+ queryIdx++;
671
+ targetIdx++;
672
+ } else {
673
+ if (currentMatchStart !== -1) {
674
+ const segmentLen = targetIdx - currentMatchStart;
675
+ matches.push([currentMatchStart, targetIdx]);
676
+ contiguityScore += segmentLen * segmentLen;
677
+ currentMatchStart = -1;
678
+ }
679
+ targetIdx++;
680
+ }
681
+ if (currentMatchStart !== -1) {
682
+ const segmentLen = targetIdx - currentMatchStart;
683
+ matches.push([currentMatchStart, targetIdx]);
684
+ contiguityScore += segmentLen * segmentLen;
685
+ }
686
+ if (queryIdx !== qLen) return null;
687
+ const maxContiguity = qLen * qLen;
688
+ const contiguityRatio = contiguityScore / maxContiguity;
689
+ const positionBonus = 1 - (matches[0]?.[0] ?? 0) / tLen;
690
+ const lengthRatio = qLen / tLen;
691
+ return {
692
+ score: .1 + .3 * contiguityRatio + .15 * positionBonus + .1 * lengthRatio,
693
+ matches
694
+ };
695
+ }
696
+ //#endregion
697
+ //#region src/core/search/index.ts
698
+ const __DEV__$1 = globalThis.process ? globalThis.process.env.NODE_ENV !== "production" : false;
699
+ let leakRegistry;
700
+ if (__DEV__$1) leakRegistry = new FinalizationRegistry((label) => {
701
+ console.warn(`[modern-cmdk] ${label} was garbage collected without being disposed. Use \`using\` or call [Symbol.dispose]() to prevent leaks.`);
702
+ });
703
+ /**
704
+ * Creates a search engine with pluggable scorer and incremental filtering.
705
+ * When the query extends the previous one, only prior matches are re-scored.
706
+ * Pre-caches lowercase at index time — zero toLowerCase() calls during search.
707
+ * Uses Iterator Helpers (ES2026) for result pipelines and Set.difference for pruning.
708
+ */
709
+ function createSearchEngine(options) {
710
+ const customScorer = options?.scorer;
711
+ const useDefaultScorer = customScorer === void 0;
712
+ const indexedEntries = /* @__PURE__ */ new Map();
713
+ let previousQuery = "";
714
+ let previousResults = /* @__PURE__ */ new Set();
715
+ const sentinel = {};
716
+ if (__DEV__$1 && leakRegistry) leakRegistry.register(sentinel, "SearchEngine", sentinel);
717
+ return {
718
+ index(items) {
719
+ for (const item of items) indexedEntries.set(item.id, {
720
+ item,
721
+ lowerValue: item.value.toLowerCase(),
722
+ lowerKeywords: item.keywords?.map((k) => k.toLowerCase())
723
+ });
724
+ previousQuery = "";
725
+ previousResults = /* @__PURE__ */ new Set();
726
+ },
727
+ search(query, items) {
728
+ if (query === "") {
729
+ previousQuery = "";
730
+ previousResults = new Set(items.values().map((i) => i.id));
731
+ return items.values().map((item) => ({
732
+ id: item.id,
733
+ score: 1,
734
+ matches: []
735
+ }));
736
+ }
737
+ const lowerQuery = query.toLowerCase();
738
+ const candidateItems = query.startsWith(previousQuery) && previousQuery.length > 0 ? items.values().filter((item) => previousResults.has(item.id)) : items;
739
+ const results = [];
740
+ if (useDefaultScorer) for (const item of candidateItems) {
741
+ const entry = indexedEntries.get(item.id);
742
+ const result = entry ? scoreItemPreLowered(lowerQuery, item.id, entry.lowerValue, entry.lowerKeywords) : scoreItemPreLowered(lowerQuery, item.id, item.value.toLowerCase(), item.keywords?.map((k) => k.toLowerCase()));
743
+ if (result !== null) results.push(result);
744
+ }
745
+ else for (const item of candidateItems) {
746
+ const result = customScorer(query, item);
747
+ if (result != null) results.push(result);
748
+ }
749
+ results.sort((a, b) => b.score - a.score);
750
+ previousQuery = query;
751
+ previousResults = new Set(results.values().map((r) => r.id));
752
+ return results.values();
753
+ },
754
+ remove(ids) {
755
+ for (const id of ids) indexedEntries.delete(id);
756
+ previousResults = previousResults.difference(ids);
757
+ },
758
+ clear() {
759
+ indexedEntries.clear();
760
+ previousQuery = "";
761
+ previousResults.clear();
762
+ },
763
+ [Symbol.dispose]() {
764
+ indexedEntries.clear();
765
+ previousResults.clear();
766
+ if (__DEV__$1 && leakRegistry) leakRegistry.unregister(sentinel);
767
+ }
768
+ };
769
+ }
770
+ //#endregion
771
+ //#region src/core/utils/event-emitter.ts
772
+ /**
773
+ * Type-safe event emitter with strong references and Disposable subscriptions.
774
+ * Returns Disposable from `on()` for automatic unsubscription via `using`.
775
+ */
776
+ var TypedEmitter = class {
777
+ #listeners = /* @__PURE__ */ new Map();
778
+ /** Subscribe to an event. Returns a Disposable that removes the listener when disposed. */
779
+ on(event, listener) {
780
+ let set = this.#listeners.get(event);
781
+ if (!set) {
782
+ set = /* @__PURE__ */ new Set();
783
+ this.#listeners.set(event, set);
784
+ }
785
+ const fn = listener;
786
+ set.add(fn);
787
+ return { [Symbol.dispose]: () => {
788
+ set.delete(fn);
789
+ } };
790
+ }
791
+ /** Emit an event to all listeners. Snapshots the set to prevent live-iteration issues. */
792
+ emit(event, data) {
793
+ const set = this.#listeners.get(event);
794
+ if (!set || set.size === 0) return;
795
+ for (const fn of [...set]) fn(data);
796
+ }
797
+ /** Check if any listeners exist for an event */
798
+ has(event) {
799
+ const set = this.#listeners.get(event);
800
+ return set !== void 0 && set.size > 0;
801
+ }
802
+ /** Get count of listeners for an event */
803
+ listenerCount(event) {
804
+ return this.#listeners.get(event)?.size ?? 0;
805
+ }
806
+ /** Remove all listeners */
807
+ removeAll() {
808
+ this.#listeners.clear();
809
+ }
810
+ [Symbol.dispose]() {
811
+ this.#listeners.clear();
812
+ }
813
+ };
814
+ //#endregion
815
+ //#region src/core/utils/scheduler.ts
816
+ /** Whether we're in a browser environment with rAF available */
817
+ const hasRAF = typeof requestAnimationFrame === "function";
818
+ /** Feature detection for cutting-edge scheduling APIs */
819
+ const canYield = typeof globalThis.scheduler?.yield === "function";
820
+ const hasInputPending = typeof navigator?.scheduling?.isInputPending === "function";
821
+ /**
822
+ * Creates a batched update scheduler. Coalesces rapid state updates into a single
823
+ * execution pass using rAF (browser) or queueMicrotask (Node.js).
824
+ */
825
+ function createScheduler() {
826
+ let pending = [];
827
+ let rafId = null;
828
+ let microtaskScheduled = false;
829
+ function executeBatch() {
830
+ const batch = pending;
831
+ pending = [];
832
+ rafId = null;
833
+ microtaskScheduled = false;
834
+ if (batch.length > 10 && hasInputPending && navigator.scheduling?.isInputPending?.()) {
835
+ pending = [...batch, ...pending];
836
+ if (canYield) globalThis.scheduler?.yield?.().then(() => void executeBatch());
837
+ else rafId ??= requestAnimationFrame(() => void executeBatch());
838
+ return;
839
+ }
840
+ for (const update of batch) try {
841
+ update();
842
+ } catch (error) {
843
+ queueMicrotask(() => {
844
+ throw error;
845
+ });
846
+ }
847
+ }
848
+ function flush() {
849
+ if (rafId !== null) {
850
+ cancelAnimationFrame(rafId);
851
+ rafId = null;
852
+ }
853
+ microtaskScheduled = false;
854
+ executeBatch();
855
+ }
856
+ return {
857
+ schedule(update) {
858
+ pending.push(update);
859
+ if (hasRAF) rafId ??= requestAnimationFrame(() => void executeBatch());
860
+ else if (!microtaskScheduled) {
861
+ microtaskScheduled = true;
862
+ queueMicrotask(executeBatch);
863
+ }
864
+ },
865
+ flush,
866
+ [Symbol.dispose]() {
867
+ if (rafId !== null) {
868
+ cancelAnimationFrame(rafId);
869
+ rafId = null;
870
+ }
871
+ microtaskScheduled = false;
872
+ pending = [];
873
+ }
874
+ };
875
+ }
876
+ //#endregion
877
+ //#region src/core/machine.ts
878
+ /**
879
+ * Creates a headless command palette state machine with search, frecency,
880
+ * and keyboard shortcut support. Implements Disposable for `using` cleanup.
881
+ *
882
+ * @example
883
+ * ```ts
884
+ * using machine = createCommandMachine({ items, loop: true });
885
+ * machine.subscribe(() => console.log(machine.getState()));
886
+ * ```
887
+ */
888
+ function createCommandMachine(options = {}) {
889
+ let state = createInitialState(options);
890
+ let filteredIdSet = /* @__PURE__ */ new Set();
891
+ let filteredIdIndex = /* @__PURE__ */ new Map();
892
+ const registry = new CommandRegistry();
893
+ const searchEngine = options.search ?? createSearchEngine(typeof options.filter === "function" ? { scorer: (query, item) => {
894
+ const score = options.filter(item, query);
895
+ if (score === false) return null;
896
+ return {
897
+ id: item.id,
898
+ score,
899
+ matches: []
900
+ };
901
+ } } : void 0);
902
+ const scheduler = createScheduler();
903
+ const emitter = new TypedEmitter();
904
+ const keyboardRegistry = new KeyboardShortcutRegistry();
905
+ let frecencyEngine = null;
906
+ if (options.frecency?.enabled) {
907
+ frecencyEngine = new FrecencyEngine(options.frecency);
908
+ frecencyEngine.load();
909
+ }
910
+ if (options.items) {
911
+ registry.registerItems(options.items);
912
+ searchEngine.index(options.items);
913
+ for (const item of options.items) if (item.shortcut != null && item.onSelect != null) keyboardRegistry.register(item.shortcut, item.id, item.onSelect);
914
+ }
915
+ if (options.groups) for (const group of options.groups) registry.registerGroup(group);
916
+ refilter("");
917
+ function setState(newState) {
918
+ state = newState;
919
+ emitter.emit("stateChange", state);
920
+ }
921
+ function refilter(query) {
922
+ const items = registry.getItems();
923
+ const disableFilter = options.filter === false;
924
+ let filteredIds;
925
+ if (disableFilter || query === "") filteredIds = items.values().filter((i) => !i.disabled).map((i) => i.id).toArray();
926
+ else {
927
+ let results = searchEngine.search(query, items).toArray();
928
+ if (frecencyEngine) {
929
+ const bonuses = frecencyEngine.getAllBonuses();
930
+ results = results.toSorted((a, b) => {
931
+ const aBonus = bonuses.get(a.id) ?? 0;
932
+ const bBonus = bonuses.get(b.id) ?? 0;
933
+ return b.score + bBonus - (a.score + aBonus);
934
+ });
935
+ }
936
+ filteredIds = results.map((r) => r.id);
937
+ }
938
+ filteredIdSet = new Set(filteredIds);
939
+ filteredIdIndex = new Map(filteredIds.entries().map(([i, id]) => [id, i]));
940
+ const groupedIds = Map.groupBy(filteredIds, (id) => {
941
+ return registry.getItem(id)?.groupId ?? "__ungrouped";
942
+ });
943
+ const activeId = state.activeId && filteredIdSet.has(state.activeId) ? state.activeId : filteredIds[0] ?? null;
944
+ setState({
945
+ ...state,
946
+ search: query,
947
+ filteredIds,
948
+ groupedIds,
949
+ filteredCount: filteredIds.length,
950
+ activeId,
951
+ lastUpdated: Temporal.Now.instant()
952
+ });
953
+ }
954
+ function navigate(direction) {
955
+ const { filteredIds, activeId } = state;
956
+ if (filteredIds.length === 0) return;
957
+ if (direction === "first") {
958
+ const newActiveId = filteredIds[0] ?? null;
959
+ if (newActiveId !== activeId) {
960
+ setState({
961
+ ...state,
962
+ activeId: newActiveId,
963
+ lastUpdated: Temporal.Now.instant()
964
+ });
965
+ options.onActiveChange?.(newActiveId);
966
+ }
967
+ return;
968
+ }
969
+ if (direction === "last") {
970
+ const newActiveId = filteredIds[filteredIds.length - 1] ?? null;
971
+ if (newActiveId !== activeId) {
972
+ setState({
973
+ ...state,
974
+ activeId: newActiveId,
975
+ lastUpdated: Temporal.Now.instant()
976
+ });
977
+ options.onActiveChange?.(newActiveId);
978
+ }
979
+ return;
980
+ }
981
+ const loop = options.loop ?? true;
982
+ const currentIdx = activeId ? filteredIdIndex.get(activeId) ?? -1 : -1;
983
+ let newIdx;
984
+ if (direction === "next") if (currentIdx === -1 || currentIdx >= filteredIds.length - 1) newIdx = loop ? 0 : filteredIds.length - 1;
985
+ else newIdx = currentIdx + 1;
986
+ else if (currentIdx <= 0) newIdx = loop ? filteredIds.length - 1 : 0;
987
+ else newIdx = currentIdx - 1;
988
+ const newActiveId = filteredIds[newIdx] ?? null;
989
+ if (newActiveId !== activeId) {
990
+ setState({
991
+ ...state,
992
+ activeId: newActiveId,
993
+ lastUpdated: Temporal.Now.instant()
994
+ });
995
+ options.onActiveChange?.(newActiveId);
996
+ }
997
+ }
998
+ function handleEvent(event) {
999
+ switch (event.type) {
1000
+ case "SEARCH_CHANGE":
1001
+ refilter(event.query);
1002
+ options.onSearchChange?.(event.query);
1003
+ break;
1004
+ case "ITEM_SELECT": {
1005
+ const item = registry.getItem(event.id);
1006
+ if (item && !item.disabled) {
1007
+ item.onSelect?.();
1008
+ frecencyEngine?.recordSelection(event.id);
1009
+ emitter.emit("select", event.id);
1010
+ options.onSelect?.(event.id);
1011
+ }
1012
+ break;
1013
+ }
1014
+ case "ITEM_ACTIVATE":
1015
+ if (filteredIdSet.has(event.id)) {
1016
+ setState({
1017
+ ...state,
1018
+ activeId: event.id,
1019
+ lastUpdated: Temporal.Now.instant()
1020
+ });
1021
+ options.onActiveChange?.(event.id);
1022
+ }
1023
+ break;
1024
+ case "NAVIGATE":
1025
+ navigate(event.direction);
1026
+ break;
1027
+ case "PAGE_PUSH":
1028
+ setState({
1029
+ ...state,
1030
+ page: event.page,
1031
+ pageStack: [...state.pageStack, state.page],
1032
+ lastUpdated: Temporal.Now.instant()
1033
+ });
1034
+ break;
1035
+ case "PAGE_POP": {
1036
+ const prevPage = state.pageStack.at(-1);
1037
+ if (prevPage != null) setState({
1038
+ ...state,
1039
+ page: prevPage,
1040
+ pageStack: state.pageStack.toSpliced(-1, 1),
1041
+ lastUpdated: Temporal.Now.instant()
1042
+ });
1043
+ break;
1044
+ }
1045
+ case "OPEN":
1046
+ setState({
1047
+ ...state,
1048
+ open: true,
1049
+ lastUpdated: Temporal.Now.instant()
1050
+ });
1051
+ emitter.emit("openChange", true);
1052
+ options.onOpenChange?.(true);
1053
+ break;
1054
+ case "CLOSE":
1055
+ setState({
1056
+ ...state,
1057
+ open: false,
1058
+ lastUpdated: Temporal.Now.instant()
1059
+ });
1060
+ emitter.emit("openChange", false);
1061
+ options.onOpenChange?.(false);
1062
+ break;
1063
+ case "TOGGLE": {
1064
+ const newOpen = !state.open;
1065
+ setState({
1066
+ ...state,
1067
+ open: newOpen,
1068
+ lastUpdated: Temporal.Now.instant()
1069
+ });
1070
+ emitter.emit("openChange", newOpen);
1071
+ options.onOpenChange?.(newOpen);
1072
+ break;
1073
+ }
1074
+ case "ITEMS_LOADED":
1075
+ registry.registerItems(event.items);
1076
+ searchEngine.index(event.items);
1077
+ state = {
1078
+ ...state,
1079
+ loading: false
1080
+ };
1081
+ refilter(state.search);
1082
+ break;
1083
+ case "REGISTER_ITEM":
1084
+ registry.registerItem(event.item);
1085
+ searchEngine.index([event.item]);
1086
+ if (event.item.shortcut && event.item.onSelect) keyboardRegistry.register(event.item.shortcut, event.item.id, event.item.onSelect);
1087
+ refilter(state.search);
1088
+ break;
1089
+ case "UNREGISTER_ITEM":
1090
+ registry.unregisterItem(event.id);
1091
+ searchEngine.remove(new Set([event.id]));
1092
+ keyboardRegistry.unregisterByItem(event.id);
1093
+ refilter(state.search);
1094
+ break;
1095
+ case "REGISTER_GROUP":
1096
+ registry.registerGroup(event.group);
1097
+ refilter(state.search);
1098
+ break;
1099
+ case "UNREGISTER_GROUP":
1100
+ registry.unregisterGroup(event.id);
1101
+ refilter(state.search);
1102
+ break;
1103
+ }
1104
+ }
1105
+ return {
1106
+ getState() {
1107
+ return state;
1108
+ },
1109
+ send(event) {
1110
+ scheduler.schedule(() => handleEvent(event));
1111
+ },
1112
+ subscribe(listener) {
1113
+ const disposable = emitter.on("stateChange", listener);
1114
+ return () => disposable[Symbol.dispose]();
1115
+ },
1116
+ subscribeState(listener) {
1117
+ return emitter.on("stateChange", listener);
1118
+ },
1119
+ getRegistry() {
1120
+ return registry;
1121
+ },
1122
+ getKeyboardRegistry() {
1123
+ return keyboardRegistry;
1124
+ },
1125
+ getFilteredIdSet() {
1126
+ return filteredIdSet;
1127
+ },
1128
+ getFilteredIdIndex() {
1129
+ return filteredIdIndex;
1130
+ },
1131
+ [Symbol.dispose]() {
1132
+ scheduler[Symbol.dispose]();
1133
+ registry[Symbol.dispose]();
1134
+ searchEngine[Symbol.dispose]();
1135
+ emitter[Symbol.dispose]();
1136
+ keyboardRegistry[Symbol.dispose]();
1137
+ frecencyEngine?.[Symbol.dispose]();
1138
+ }
1139
+ };
1140
+ }
1141
+ //#endregion
72
1142
  //#region src/react/hooks/use-command.ts
1143
+ /** Core hook that binds a CommandMachine to React via useSyncExternalStore, useTransition, and useOptimistic. */
73
1144
  function useCommand(machine) {
74
1145
  const id = useId();
75
1146
  const [isPending, startTransition] = useTransition();
76
1147
  const state = useSyncExternalStore(machine.subscribe, machine.getState, machine.getState);
77
- const filteredIdSet = useMemo(() => new Set(state.filteredIds), [state.filteredIds]);
78
- const [optimisticActiveId, setOptimisticActiveId] = useOptimistic(state.activeId);
1148
+ const filteredIdSet = useMemo(() => machine.getFilteredIdSet(), [state.filteredIds, machine]);
1149
+ const [optimisticActiveId, rawSetOptimisticActiveId] = useOptimistic(state.activeId);
1150
+ const setOptimisticActiveId = useCallback((id) => {
1151
+ startTransition(() => {
1152
+ rawSetOptimisticActiveId(id);
1153
+ });
1154
+ }, [rawSetOptimisticActiveId]);
79
1155
  const updateSearch = useCallback((query) => {
80
1156
  startTransition(() => {
81
1157
  machine.send({
@@ -143,30 +1219,77 @@ function createKeydownHandler(machine, getState) {
143
1219
  };
144
1220
  }
145
1221
  //#endregion
146
- //#region src/react/dialog.tsx
147
- function CommandDialog({ children, className, overlayClassName, contentClassName, label = "Command palette", open: controlledOpen, onOpenChange, container, ...machineOptions }) {
148
- const rootId = useId();
149
- const listId = `${rootId}-list`;
150
- const inputId = `${rootId}-input`;
151
- const onOpenChangeRef = useRef(onOpenChange);
152
- onOpenChangeRef.current = onOpenChange;
1222
+ //#region src/react/hooks/use-command-setup.ts
1223
+ /**
1224
+ * Shared setup for Command and Command.Dialog — creates machine once,
1225
+ * subscribes via useSyncExternalStore, memoizes context values, and
1226
+ * registers disposal on unmount.
1227
+ *
1228
+ * @param machineFactory - Called exactly once (first render) to create the machine.
1229
+ * Subsequent renders skip it via ref guard.
1230
+ * @param label - Accessible label for the command palette (included in stable context).
1231
+ */
1232
+ function useCommandSetup(machineFactory, label) {
153
1233
  const machineRef = useRef(null);
154
- if (machineRef.current === null) machineRef.current = createCommandMachine({
155
- ...machineOptions,
156
- open: controlledOpen ?? false,
157
- onOpenChange: (open) => {
158
- onOpenChangeRef.current?.(open);
159
- }
160
- });
1234
+ if (machineRef.current === null) machineRef.current = machineFactory();
161
1235
  const machine = machineRef.current;
162
1236
  const { state, isPending, updateSearch, setOptimisticActiveId, filteredIdSet, id } = useCommand(machine);
1237
+ const listId = `${id}-list`;
1238
+ const inputId = `${id}-input`;
163
1239
  useEffect(() => {
164
1240
  return () => {
165
1241
  machineRef.current?.[Symbol.dispose]();
166
1242
  machineRef.current = null;
167
1243
  };
168
1244
  }, []);
169
- const handleKeyDown = useCallback(createKeydownHandler(machine, () => machine.getState()), []);
1245
+ return {
1246
+ machine,
1247
+ state,
1248
+ isPending,
1249
+ listId,
1250
+ inputId,
1251
+ handleKeyDown: useMemo(() => createKeydownHandler(machine, () => machine.getState()), [machine]),
1252
+ stableContextValue: useMemo(() => ({
1253
+ machine,
1254
+ rootId: id,
1255
+ listId,
1256
+ inputId,
1257
+ label,
1258
+ updateSearch,
1259
+ setOptimisticActiveId
1260
+ }), [
1261
+ machine,
1262
+ id,
1263
+ listId,
1264
+ inputId,
1265
+ label,
1266
+ updateSearch,
1267
+ setOptimisticActiveId
1268
+ ]),
1269
+ stateContextValue: useMemo(() => ({
1270
+ state,
1271
+ isPending,
1272
+ filteredIdSet
1273
+ }), [
1274
+ state,
1275
+ isPending,
1276
+ filteredIdSet
1277
+ ])
1278
+ };
1279
+ }
1280
+ //#endregion
1281
+ //#region src/react/dialog.tsx
1282
+ /** Dialog variant that wraps the command palette in a Radix UI Dialog with focus trap and overlay. */
1283
+ function CommandDialog({ children, className, overlayClassName, contentClassName, label = "Command palette", description = "Type a command or search...", open: controlledOpen, onOpenChange, container, ...machineOptions }) {
1284
+ const onOpenChangeRef = useRef(onOpenChange);
1285
+ onOpenChangeRef.current = onOpenChange;
1286
+ const { machine, state, handleKeyDown, stableContextValue, stateContextValue, inputId } = useCommandSetup(() => createCommandMachine({
1287
+ ...machineOptions,
1288
+ open: controlledOpen ?? false,
1289
+ onOpenChange: (open) => {
1290
+ onOpenChangeRef.current?.(open);
1291
+ }
1292
+ }), label);
170
1293
  useEffect(() => {
171
1294
  if (!state.open) return;
172
1295
  document.addEventListener("keydown", handleKeyDown);
@@ -174,42 +1297,16 @@ function CommandDialog({ children, className, overlayClassName, contentClassName
174
1297
  document.removeEventListener("keydown", handleKeyDown);
175
1298
  };
176
1299
  }, [handleKeyDown, state.open]);
177
- const prevControlledOpen = useRef(controlledOpen);
178
1300
  useEffect(() => {
179
- if (controlledOpen !== void 0 && controlledOpen !== prevControlledOpen.current) {
180
- prevControlledOpen.current = controlledOpen;
181
- machine.send({ type: controlledOpen ? "OPEN" : "CLOSE" });
182
- }
1301
+ if (controlledOpen !== void 0) machine.send({ type: controlledOpen ? "OPEN" : "CLOSE" });
183
1302
  }, [controlledOpen, machine]);
1303
+ const handleOpenAutoFocus = useCallback((e) => {
1304
+ e.preventDefault();
1305
+ document.getElementById(inputId)?.focus();
1306
+ }, [inputId]);
184
1307
  const handleRadixOpenChange = useCallback((open) => {
185
1308
  machine.send({ type: open ? "OPEN" : "CLOSE" });
186
1309
  }, [machine]);
187
- const stableContextValue = useMemo(() => ({
188
- machine,
189
- rootId: id,
190
- listId,
191
- inputId,
192
- label,
193
- updateSearch,
194
- setOptimisticActiveId
195
- }), [
196
- machine,
197
- id,
198
- listId,
199
- inputId,
200
- label,
201
- updateSearch,
202
- setOptimisticActiveId
203
- ]);
204
- const stateContextValue = useMemo(() => ({
205
- state,
206
- isPending,
207
- filteredIdSet
208
- }), [
209
- state,
210
- isPending,
211
- filteredIdSet
212
- ]);
213
1310
  return /* @__PURE__ */ jsx(Dialog.Root, {
214
1311
  open: state.open,
215
1312
  onOpenChange: handleRadixOpenChange,
@@ -224,10 +1321,7 @@ function CommandDialog({ children, className, overlayClassName, contentClassName
224
1321
  "data-state": state.open ? "open" : "closed",
225
1322
  className,
226
1323
  "aria-label": label,
227
- onOpenAutoFocus: (e) => {
228
- e.preventDefault();
229
- document.getElementById(inputId)?.focus();
230
- },
1324
+ onOpenAutoFocus: handleOpenAutoFocus,
231
1325
  children: [
232
1326
  /* @__PURE__ */ jsx(Dialog.Title, {
233
1327
  className: "sr-only",
@@ -235,7 +1329,7 @@ function CommandDialog({ children, className, overlayClassName, contentClassName
235
1329
  }),
236
1330
  /* @__PURE__ */ jsx(Dialog.Description, {
237
1331
  className: "sr-only",
238
- children: "Type a command or search..."
1332
+ children: description
239
1333
  }),
240
1334
  /* @__PURE__ */ jsx(CommandStableContext, {
241
1335
  value: stableContextValue,
@@ -260,11 +1354,12 @@ function CommandDialog({ children, className, overlayClassName, contentClassName
260
1354
  }
261
1355
  //#endregion
262
1356
  //#region src/react/empty.tsx
1357
+ /** Rendered when no items match the current search query. */
263
1358
  function CommandEmpty({ ref, children, ...props }) {
264
- const stateCtx = use(CommandStateContext);
265
- if (!stateCtx) throw new Error("Command.Empty must be used within a <Command> component");
1359
+ const stateCtx = useStateContext("Command.Empty");
1360
+ const statusContainerRef = use(CommandListStatusContext);
266
1361
  if (stateCtx.state.filteredCount > 0 || stateCtx.state.loading) return null;
267
- return /* @__PURE__ */ jsx("div", {
1362
+ const content = /* @__PURE__ */ jsx("div", {
268
1363
  ref,
269
1364
  "data-command-empty": "",
270
1365
  role: "status",
@@ -272,15 +1367,21 @@ function CommandEmpty({ ref, children, ...props }) {
272
1367
  ...props,
273
1368
  children
274
1369
  });
1370
+ if (statusContainerRef?.current) return createPortal(content, statusContainerRef.current);
1371
+ return content;
275
1372
  }
276
1373
  //#endregion
277
1374
  //#region src/react/hooks/use-register.ts
278
1375
  /** Register a command item with the machine — auto-deregisters on unmount */
279
1376
  function useRegisterItem(value, options) {
280
- const stable = use(CommandStableContext);
281
- if (!stable) throw new Error("useRegisterItem must be used within a <Command> component");
1377
+ const stable = useStableContext("useRegisterItem");
282
1378
  const generatedId = useId();
283
1379
  const id = itemId(options?.forceId ?? generatedId);
1380
+ const onSelectRef = useRef(options?.onSelect);
1381
+ onSelectRef.current = options?.onSelect;
1382
+ const dataRef = useRef(options?.data);
1383
+ dataRef.current = options?.data;
1384
+ const keywordsDep = options?.keywords?.join("\0");
284
1385
  useLayoutEffect(() => {
285
1386
  const item = {
286
1387
  id,
@@ -289,8 +1390,8 @@ function useRegisterItem(value, options) {
289
1390
  groupId: options?.groupId ? groupId(options.groupId) : void 0,
290
1391
  shortcut: options?.shortcut,
291
1392
  disabled: options?.disabled,
292
- onSelect: options?.onSelect,
293
- data: options?.data
1393
+ onSelect: () => onSelectRef.current?.(),
1394
+ data: dataRef.current
294
1395
  };
295
1396
  stable.machine.send({
296
1397
  type: "REGISTER_ITEM",
@@ -307,18 +1408,15 @@ function useRegisterItem(value, options) {
307
1408
  id,
308
1409
  value,
309
1410
  options?.groupId,
310
- options?.data,
311
1411
  options?.disabled,
312
- options?.keywords,
313
- options?.onSelect,
1412
+ keywordsDep,
314
1413
  options?.shortcut
315
1414
  ]);
316
1415
  return id;
317
1416
  }
318
1417
  /** Register a command group with the machine — auto-deregisters on unmount */
319
1418
  function useRegisterGroup(heading, options) {
320
- const stable = use(CommandStableContext);
321
- if (!stable) throw new Error("useRegisterGroup must be used within a <Command> component");
1419
+ const stable = useStableContext("useRegisterGroup");
322
1420
  const generatedId = useId();
323
1421
  const id = groupId(options?.forceId ?? generatedId);
324
1422
  useLayoutEffect(() => {
@@ -347,9 +1445,11 @@ function useRegisterGroup(heading, options) {
347
1445
  }
348
1446
  //#endregion
349
1447
  //#region src/react/group.tsx
1448
+ /** Module-level constant — avoids allocating a new object on every render of hidden groups */
1449
+ const HIDDEN_STYLE = { display: "none" };
1450
+ /** Visual grouping container with an optional heading that hides when no items match. */
350
1451
  function CommandGroup({ ref, children, heading, priority, forceId, ...props }) {
351
- const stateCtx = use(CommandStateContext);
352
- if (!stateCtx) throw new Error("Command.Group must be used within a <Command> component");
1452
+ const stateCtx = useStateContext("Command.Group");
353
1453
  const headingId = useId();
354
1454
  const groupId = useRegisterGroup(heading, {
355
1455
  ...priority !== void 0 && { priority },
@@ -361,7 +1461,13 @@ function CommandGroup({ ref, children, heading, priority, forceId, ...props }) {
361
1461
  groupId,
362
1462
  headingId
363
1463
  }), [groupId, headingId]);
364
- if (!hasVisibleItems) return null;
1464
+ if (!hasVisibleItems) return /* @__PURE__ */ jsx(CommandGroupContext, {
1465
+ value: groupContext,
1466
+ children: /* @__PURE__ */ jsx("div", {
1467
+ style: HIDDEN_STYLE,
1468
+ children
1469
+ })
1470
+ });
365
1471
  return /* @__PURE__ */ jsx(CommandGroupContext, {
366
1472
  value: groupContext,
367
1473
  children: /* @__PURE__ */ jsxs("div", {
@@ -384,13 +1490,14 @@ function CommandGroup({ ref, children, heading, priority, forceId, ...props }) {
384
1490
  }
385
1491
  //#endregion
386
1492
  //#region src/react/highlight.tsx
1493
+ /** Renders search match highlighting by splitting text at scored match ranges. */
387
1494
  function CommandHighlight({ text, ranges, highlightClassName }) {
388
1495
  return useMemo(() => {
389
1496
  if (ranges.length === 0) return text;
390
- const sorted = Iterator.from(ranges).toArray().sort((a, b) => a[0] - b[0]);
1497
+ const sorted = ranges.toSorted((a, b) => a[0] - b[0]);
391
1498
  const segments = [];
392
1499
  let lastEnd = 0;
393
- Iterator.from(sorted).forEach(([start, end], i) => {
1500
+ sorted.forEach(([start, end], i) => {
394
1501
  if (start > lastEnd) segments.push(text.slice(lastEnd, start));
395
1502
  segments.push(/* @__PURE__ */ jsx("mark", {
396
1503
  "data-command-highlight": "",
@@ -409,11 +1516,10 @@ function CommandHighlight({ text, ranges, highlightClassName }) {
409
1516
  }
410
1517
  //#endregion
411
1518
  //#region src/react/input.tsx
1519
+ /** Combobox input that drives the command palette search state. */
412
1520
  function CommandInput({ ref, placeholder = "Search...", onValueChange, ...props }) {
413
- const stable = use(CommandStableContext);
414
- if (!stable) throw new Error("Command.Input must be used within a <Command> component");
415
- const stateCtx = use(CommandStateContext);
416
- if (!stateCtx) throw new Error("Command.Input must be used within a <Command> component");
1521
+ const stable = useStableContext("Command.Input");
1522
+ const stateCtx = useStateContext("Command.Input");
417
1523
  const handleChange = useCallback((event) => {
418
1524
  const { value } = event.target;
419
1525
  stable.updateSearch(value);
@@ -424,7 +1530,7 @@ function CommandInput({ ref, placeholder = "Search...", onValueChange, ...props
424
1530
  "data-command-input": "",
425
1531
  type: "text",
426
1532
  role: "combobox",
427
- "aria-expanded": stateCtx.state.filteredCount > 0,
1533
+ "aria-expanded": true,
428
1534
  "aria-controls": stable.listId,
429
1535
  "aria-activedescendant": stateCtx.state.activeId ?? void 0,
430
1536
  "aria-autocomplete": "list",
@@ -441,17 +1547,18 @@ function CommandInput({ ref, placeholder = "Search...", onValueChange, ...props
441
1547
  }
442
1548
  //#endregion
443
1549
  //#region src/react/item.tsx
1550
+ /** Individual selectable item in the command list with keyboard and pointer activation. */
444
1551
  function CommandItem({ ref, children, value, keywords, onSelect, disabled = false, shortcut, forceId, ...props }) {
445
- const stable = use(CommandStableContext);
446
- if (!stable) throw new Error("Command.Item must be used within a <Command> component");
447
- const stateCtx = use(CommandStateContext);
448
- if (!stateCtx) throw new Error("Command.Item must be used within a <Command> component");
1552
+ const stable = useStableContext("Command.Item");
1553
+ const stateCtx = useStateContext("Command.Item");
1554
+ const groupCtx = use(CommandGroupContext);
449
1555
  const id = useRegisterItem(value, {
450
1556
  ...keywords !== void 0 && { keywords },
451
1557
  ...shortcut !== void 0 && { shortcut },
452
1558
  disabled,
453
1559
  ...onSelect !== void 0 && { onSelect },
454
- ...forceId !== void 0 && { forceId }
1560
+ ...forceId !== void 0 && { forceId },
1561
+ ...groupCtx !== null && { groupId: groupCtx.groupId }
455
1562
  });
456
1563
  const isActive = stateCtx.state.activeId === id;
457
1564
  const isFiltered = stateCtx.filteredIdSet.has(id);
@@ -501,6 +1608,7 @@ function CommandItem({ ref, children, value, keywords, onSelect, disabled = fals
501
1608
  }
502
1609
  //#endregion
503
1610
  //#region src/react/hooks/use-virtualizer.ts
1611
+ /** Lightweight virtualizer for large item lists using ResizeObserver and scroll tracking. */
504
1612
  function useVirtualizer(options) {
505
1613
  const { count, estimateSize, overscan = 8, scrollElement, enabled = true } = options;
506
1614
  const [scrollOffset, setScrollOffset] = useState(0);
@@ -514,17 +1622,12 @@ function useVirtualizer(options) {
514
1622
  if (lastEntry) setContainerHeight(lastEntry.contentRect.height);
515
1623
  });
516
1624
  observer.observe(scrollElement);
517
- return () => {
518
- observer.disconnect();
519
- };
520
- }, [scrollElement, enabled]);
521
- useEffect(() => {
522
- if (!scrollElement || !enabled) return;
523
1625
  const handleScroll = () => {
524
1626
  setScrollOffset(scrollElement.scrollTop);
525
1627
  };
526
1628
  scrollElement.addEventListener("scroll", handleScroll, { passive: true });
527
1629
  return () => {
1630
+ observer.disconnect();
528
1631
  scrollElement.removeEventListener("scroll", handleScroll);
529
1632
  };
530
1633
  }, [scrollElement, enabled]);
@@ -612,11 +1715,17 @@ function useVirtualizer(options) {
612
1715
  }
613
1716
  //#endregion
614
1717
  //#region src/react/list.tsx
615
- function CommandList({ ref, children, virtualize, estimateSize = 44, overscan = 8, style, ...props }) {
616
- const stable = use(CommandStableContext);
617
- if (!stable) throw new Error("Command.List must be used within a <Command> component");
618
- const stateCtx = use(CommandStateContext);
619
- if (!stateCtx) throw new Error("Command.List must be used within a <Command> component");
1718
+ /** Base style for virtual items — hoisted to avoid allocation per item per render */
1719
+ const VIRTUAL_ITEM_BASE_STYLE = {
1720
+ position: "absolute",
1721
+ top: 0,
1722
+ left: 0,
1723
+ width: "100%"
1724
+ };
1725
+ /** Scrollable list container with auto-virtualization and aria-live announcements. */
1726
+ function CommandList({ ref, children, virtualize, estimateSize = 44, overscan = 8, announceResults, style, ...props }) {
1727
+ const stable = useStableContext("Command.List");
1728
+ const stateCtx = useStateContext("Command.List");
620
1729
  const innerRef = useRef(null);
621
1730
  const scrollRef = useRef(null);
622
1731
  const [height, setHeight] = useState(0);
@@ -645,7 +1754,7 @@ function CommandList({ ref, children, virtualize, estimateSize = 44, overscan =
645
1754
  if (stateCtx.state.activeId && stateCtx.state.activeId !== prevActiveId.current) {
646
1755
  prevActiveId.current = stateCtx.state.activeId;
647
1756
  if (shouldVirtualize) {
648
- const idx = stateCtx.state.filteredIds.indexOf(stateCtx.state.activeId);
1757
+ const idx = stable.machine.getFilteredIdIndex().get(stateCtx.state.activeId) ?? -1;
649
1758
  if (idx >= 0) virtualizer.scrollToIndex(idx);
650
1759
  } else (scrollRef.current?.querySelector(`[id="${CSS.escape(stateCtx.state.activeId)}"]`))?.scrollIntoView({
651
1760
  block: "nearest",
@@ -654,71 +1763,77 @@ function CommandList({ ref, children, virtualize, estimateSize = 44, overscan =
654
1763
  }
655
1764
  }, [
656
1765
  stateCtx.state.activeId,
657
- stateCtx.state.filteredIds,
658
1766
  shouldVirtualize,
659
- virtualizer
1767
+ virtualizer,
1768
+ stable.machine.getFilteredIdIndex
660
1769
  ]);
661
- const mergedStyle = {
1770
+ const mergedStyle = useMemo(() => ({
662
1771
  ...style,
663
1772
  "--command-list-height": `${height.toFixed(1)}px`
664
- };
665
- return /* @__PURE__ */ jsxs("div", {
666
- ref: useCallback((el) => {
667
- scrollRef.current = el;
668
- if (typeof ref === "function") ref(el);
669
- else if (ref) ref.current = el;
670
- }, [ref]),
671
- "data-command-list": "",
672
- role: "listbox",
673
- "aria-label": stable.label,
674
- "aria-busy": stateCtx.state.loading,
675
- id: stable.listId,
676
- style: mergedStyle,
677
- ...props,
678
- children: [shouldVirtualize ? /* @__PURE__ */ jsxs("div", {
679
- "data-command-list-virtual": "",
680
- style: {
681
- height: `${virtualizer.totalSize}px`,
682
- position: "relative"
683
- },
684
- children: [virtualizer.virtualItems.map((vItem) => /* @__PURE__ */ jsx("div", {
685
- "data-command-virtual-item": "",
686
- "data-index": vItem.index,
687
- ref: virtualizer.measureElement,
688
- style: {
689
- position: "absolute",
690
- top: 0,
691
- left: 0,
692
- width: "100%",
693
- translate: `0 ${vItem.start}px`
694
- }
695
- }, vItem.key)), children]
696
- }) : /* @__PURE__ */ jsx("div", {
697
- ref: innerRef,
698
- "data-command-list-inner": "",
699
- children
700
- }), /* @__PURE__ */ jsxs("div", {
701
- "aria-live": "polite",
702
- "aria-atomic": true,
703
- role: "status",
704
- "data-command-aria-live": "",
705
- className: "sr-only",
706
- children: [
707
- stateCtx.state.filteredCount,
708
- " result",
709
- stateCtx.state.filteredCount !== 1 ? "s" : "",
710
- " ",
711
- "available."
712
- ]
713
- })]
1773
+ }), [style, height]);
1774
+ const externalRefRef = useRef(ref);
1775
+ externalRefRef.current = ref;
1776
+ const setScrollRef = useCallback((el) => {
1777
+ scrollRef.current = el;
1778
+ const externalRef = externalRefRef.current;
1779
+ if (typeof externalRef === "function") externalRef(el);
1780
+ else if (externalRef) externalRef.current = el;
1781
+ }, []);
1782
+ const statusContainerRef = useRef(null);
1783
+ return /* @__PURE__ */ jsxs(CommandListStatusContext, {
1784
+ value: statusContainerRef,
1785
+ children: [
1786
+ /* @__PURE__ */ jsx("div", {
1787
+ ref: setScrollRef,
1788
+ "data-command-list": "",
1789
+ role: "listbox",
1790
+ "aria-label": stable.label,
1791
+ "aria-busy": stateCtx.state.loading,
1792
+ id: stable.listId,
1793
+ style: mergedStyle,
1794
+ ...props,
1795
+ children: shouldVirtualize ? /* @__PURE__ */ jsxs("div", {
1796
+ "data-command-list-virtual": "",
1797
+ style: {
1798
+ height: `${virtualizer.totalSize}px`,
1799
+ position: "relative"
1800
+ },
1801
+ children: [virtualizer.virtualItems.map((vItem) => /* @__PURE__ */ jsx("div", {
1802
+ "data-command-virtual-item": "",
1803
+ "data-index": vItem.index,
1804
+ ref: virtualizer.measureElement,
1805
+ style: {
1806
+ ...VIRTUAL_ITEM_BASE_STYLE,
1807
+ translate: `0 ${vItem.start}px`
1808
+ }
1809
+ }, vItem.key)), children]
1810
+ }) : /* @__PURE__ */ jsx("div", {
1811
+ ref: innerRef,
1812
+ "data-command-list-inner": "",
1813
+ children
1814
+ })
1815
+ }),
1816
+ /* @__PURE__ */ jsx("div", {
1817
+ ref: statusContainerRef,
1818
+ "data-command-list-status": ""
1819
+ }),
1820
+ /* @__PURE__ */ jsx("div", {
1821
+ "aria-live": "polite",
1822
+ "aria-atomic": true,
1823
+ role: "status",
1824
+ "data-command-aria-live": "",
1825
+ className: "sr-only",
1826
+ children: announceResults ? announceResults(stateCtx.state.filteredCount) : `${stateCtx.state.filteredCount} result${stateCtx.state.filteredCount !== 1 ? "s" : ""} available.`
1827
+ })
1828
+ ]
714
1829
  });
715
1830
  }
716
1831
  //#endregion
717
1832
  //#region src/react/loading.tsx
1833
+ /** Shown during async operations, with configurable loading state override. */
718
1834
  function CommandLoading({ ref, children, loading: loadingOverride, ...props }) {
719
- const stateCtx = use(CommandStateContext);
720
- if (!stateCtx) throw new Error("Command.Loading must be used within a <Command> component");
721
- if (!(loadingOverride ?? stateCtx.state.loading ?? stateCtx.isPending)) return null;
1835
+ const stateCtx = useStateContext("Command.Loading");
1836
+ if (!(loadingOverride ?? (stateCtx.state.loading || stateCtx.isPending))) return null;
722
1837
  return /* @__PURE__ */ jsx("div", {
723
1838
  ref,
724
1839
  "data-command-loading": "",
@@ -731,10 +1846,9 @@ function CommandLoading({ ref, children, loading: loadingOverride, ...props }) {
731
1846
  }
732
1847
  //#endregion
733
1848
  //#region src/react/page.tsx
1849
+ /** Page navigation container that only renders when its ID matches the active page. */
734
1850
  function CommandPage({ id, children }) {
735
- const stateCtx = use(CommandStateContext);
736
- if (!stateCtx) throw new Error("Command.Page must be used within a <Command> component");
737
- if (stateCtx.state.page !== id) return null;
1851
+ if (useStateContext("Command.Page").state.page !== id) return null;
738
1852
  return /* @__PURE__ */ jsx("div", {
739
1853
  "data-command-page": "",
740
1854
  "data-command-page-id": id,
@@ -743,77 +1857,48 @@ function CommandPage({ id, children }) {
743
1857
  }
744
1858
  //#endregion
745
1859
  //#region src/react/separator.tsx
1860
+ /** Visual divider between items or groups, rendered as a presentational element. */
746
1861
  function CommandSeparator({ ref, ...props }) {
747
1862
  return /* @__PURE__ */ jsx("div", {
748
1863
  ref,
749
1864
  "data-command-separator": "",
750
- role: "separator",
1865
+ role: "none",
751
1866
  ...props
752
1867
  });
753
1868
  }
754
1869
  //#endregion
755
1870
  //#region src/react/shortcut.tsx
1871
+ /** Keyboard shortcut display with platform-aware formatting (e.g., Cmd vs Ctrl). */
756
1872
  function CommandShortcut({ ref, shortcut, children, ...props }) {
757
- const displayText = useMemo(() => shortcut ? formatShortcut(parseShortcut(shortcut)) : children, [shortcut, children]);
1873
+ const formattedShortcut = useMemo(() => shortcut ? formatShortcut(parseShortcut(shortcut)) : null, [shortcut]);
758
1874
  return /* @__PURE__ */ jsx("kbd", {
759
1875
  ref,
760
1876
  "data-command-shortcut": "",
761
1877
  ...props,
762
- children: displayText
1878
+ children: formattedShortcut ?? children
763
1879
  });
764
1880
  }
765
1881
  //#endregion
766
1882
  //#region src/react/command.tsx
1883
+ /** Root compound component that provides the command state machine context to all children. */
767
1884
  function CommandRoot({ children, className, label = "Command palette", ...machineOptions }) {
768
- const rootId = useId();
769
- const listId = `${rootId}-list`;
770
- const inputId = `${rootId}-input`;
771
1885
  const rootRef = useRef(null);
772
- const machineRef = useRef(null);
773
- if (machineRef.current === null) machineRef.current = createCommandMachine(machineOptions);
774
- const machine = machineRef.current;
775
- const { state, isPending, updateSearch, setOptimisticActiveId, filteredIdSet, id } = useCommand(machine);
1886
+ const { state, handleKeyDown, stableContextValue, stateContextValue } = useCommandSetup(() => createCommandMachine(machineOptions), label);
776
1887
  useEffect(() => {
777
- return () => {
778
- machineRef.current?.[Symbol.dispose]();
779
- machineRef.current = null;
1888
+ const root = rootRef.current;
1889
+ if (!root) return;
1890
+ const scopedHandler = (e) => {
1891
+ if (root.contains(e.target)) handleKeyDown(e);
780
1892
  };
781
- }, []);
782
- const handleKeyDown = useCallback(createKeydownHandler(machine, () => machine.getState()), []);
783
- useEffect(() => {
784
- document.addEventListener("keydown", handleKeyDown);
1893
+ document.addEventListener("keydown", scopedHandler);
785
1894
  return () => {
786
- document.removeEventListener("keydown", handleKeyDown);
1895
+ document.removeEventListener("keydown", scopedHandler);
787
1896
  };
788
1897
  }, [handleKeyDown]);
789
1898
  return /* @__PURE__ */ jsx(CommandStableContext, {
790
- value: useMemo(() => ({
791
- machine,
792
- rootId: id,
793
- listId,
794
- inputId,
795
- label,
796
- updateSearch,
797
- setOptimisticActiveId
798
- }), [
799
- machine,
800
- id,
801
- listId,
802
- inputId,
803
- label,
804
- updateSearch,
805
- setOptimisticActiveId
806
- ]),
1899
+ value: stableContextValue,
807
1900
  children: /* @__PURE__ */ jsx(CommandStateContext, {
808
- value: useMemo(() => ({
809
- state,
810
- isPending,
811
- filteredIdSet
812
- }), [
813
- state,
814
- isPending,
815
- filteredIdSet
816
- ]),
1901
+ value: stateContextValue,
817
1902
  children: /* @__PURE__ */ jsx("div", {
818
1903
  ref: rootRef,
819
1904
  "data-command-root": "",
@@ -847,6 +1932,7 @@ const Command = Object.assign(CommandRoot, {
847
1932
  //#region src/react/error-boundary.tsx
848
1933
  /** Initial state — no error captured */
849
1934
  const INITIAL_STATE = { error: null };
1935
+ /** Error boundary for graceful failure recovery in command subtrees. */
850
1936
  var CommandErrorBoundary = class extends Component {
851
1937
  state = INITIAL_STATE;
852
1938
  static getDerivedStateFromError(error) {
@@ -868,8 +1954,7 @@ var CommandErrorBoundary = class extends Component {
868
1954
  //#endregion
869
1955
  //#region src/react/hooks/use-command-state.ts
870
1956
  function useCommandState(selector) {
871
- const stable = use(CommandStableContext);
872
- if (!stable) throw new Error("useCommandState must be used within a <Command> component");
1957
+ const stable = useStableContext("useCommandState");
873
1958
  const selectorRef = useRef(selector);
874
1959
  selectorRef.current = selector;
875
1960
  const getSnapshot = useCallback(() => selectorRef.current ? selectorRef.current(stable.machine.getState()) : stable.machine.getState(), [stable.machine]);
@@ -892,11 +1977,11 @@ const DEVTOOLS_KEY = "__CRIMSON_COMMAND_DEVTOOLS__";
892
1977
  * ```
893
1978
  */
894
1979
  function useCommandDevtools(label = "default") {
895
- if (!__DEV__) return;
896
1980
  const stable = use(CommandStableContext);
897
1981
  const stateCtx = use(CommandStateContext);
898
1982
  const stateRef = useRef(null);
899
1983
  useEffect(() => {
1984
+ if (!__DEV__) return;
900
1985
  if (!stable || !stateCtx) return;
901
1986
  const update = () => {
902
1987
  const s = stateCtx.state;
@@ -933,5 +2018,3 @@ function useCommandDevtools(label = "default") {
933
2018
  }
934
2019
  //#endregion
935
2020
  export { Command, CommandActivity, CommandAsyncItems, CommandBadge, CommandDialog, CommandEmpty, CommandErrorBoundary, CommandGroup, CommandHighlight, CommandInput, CommandItem, CommandList, CommandLoading, CommandPage, CommandSeparator, CommandShortcut, CommandStableContext, CommandStateContext, createKeydownHandler, useCommand, useCommandDevtools, useCommandState, useRegisterGroup, useRegisterItem, useVirtualizer };
936
-
937
- //# sourceMappingURL=index.mjs.map