modern-cmdk 1.1.1 → 1.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -99,7 +99,7 @@ A ground-up rewrite of `cmdk` for **React 19**, **ES2026**, and **TypeScript 6**
99
99
  | **Architecture** | React-only, tightly coupled | Framework-agnostic core + thin adapters |
100
100
  | **React** | 18 | 19 (`use()`, `useOptimistic`, `useId`, `ref` as prop) |
101
101
  | **Search** | Basic substring | Fuzzy scoring + optional WASM (sub-1ms on 100K items) |
102
- | **Ranking** | Static order | Frecency with `Temporal`-based exponential decay |
102
+ | **Ranking** | Static order | Frecency with time-based exponential decay |
103
103
  | **Virtualization** | None | Automatic variable-height, `content-visibility: auto` |
104
104
  | **Animations** | CSS transitions | GPU-composited: `@starting-style`, `scroll-timeline`, spring easing |
105
105
  | **Keyboard** | External | Built-in registry, `Mod` key, conflict detection |
@@ -118,8 +118,8 @@ A ground-up rewrite of `cmdk` for **React 19**, **ES2026**, and **TypeScript 6**
118
118
  - **React 19 adapter** -- 14 compound components. `useSyncExternalStore`, `useTransition`, `useOptimistic`, `use()` for Suspense. React Compiler compatible.
119
119
  - **Automatic virtualization** -- Variable-height virtual scrolling kicks in at 100+ items. Handles 100K+ items smoothly with `content-visibility: auto`.
120
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.
121
+ - **Frecency ranking** -- Frequency x recency with time-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. Cross-browser helper functions for grouping and set operations.
123
123
  - **Full accessibility** -- WAI-ARIA combobox pattern, `aria-live` announcements, `forced-colors`, `prefers-contrast`, `prefers-reduced-motion`.
124
124
  - **GPU-composited animations** -- `@starting-style` entry, `scroll-timeline` progress, spring `linear()` easing. All customizable via CSS custom properties.
125
125
  - **Error boundary** -- `CommandErrorBoundary` with static or render-function fallback.
@@ -89714,7 +89714,7 @@ var require_core = /* @__PURE__ */ __commonJSMin(((exports, module) => {
89714
89714
  module.exports = enrichCore(core, getParser());
89715
89715
  }));
89716
89716
  //#endregion
89717
- //#region src/codemod/cli.ts
89717
+ //#region src/core/utils/group-by.ts
89718
89718
  var import_jscodeshift = /* @__PURE__ */ __toESM((/* @__PURE__ */ __commonJSMin(((exports, module) => {
89719
89719
  /**
89720
89720
  * Copyright (c) Facebook, Inc. and its affiliates.
@@ -89724,6 +89724,22 @@ var import_jscodeshift = /* @__PURE__ */ __toESM((/* @__PURE__ */ __commonJSMin(
89724
89724
  */
89725
89725
  module.exports = require_core();
89726
89726
  })))(), 1);
89727
+ /**
89728
+ * Groups items from an iterable into a plain object keyed by the string result of `keyFn`.
89729
+ * Cross-browser replacement for `Object.groupBy()`.
89730
+ */
89731
+ function objectGroupBy(items, keyFn) {
89732
+ const result = Object.create(null);
89733
+ for (const item of items) {
89734
+ const key = keyFn(item);
89735
+ const existing = result[key];
89736
+ if (existing) existing.push(item);
89737
+ else result[key] = [item];
89738
+ }
89739
+ return result;
89740
+ }
89741
+ //#endregion
89742
+ //#region src/codemod/cli.ts
89727
89743
  const AVAILABLE_TRANSFORMS = new Map([
89728
89744
  ["import-rewrite", "Rewrites cmdk imports to modern-cmdk/react"],
89729
89745
  ["data-attrs", "Converts [cmdk-*] selectors to [data-command-*]"],
@@ -89802,7 +89818,10 @@ async function main() {
89802
89818
  stats: () => {},
89803
89819
  report: () => {}
89804
89820
  });
89805
- const { promise: done, resolve: resolveDone } = Promise.withResolvers();
89821
+ let resolveDone;
89822
+ const done = new Promise((r) => {
89823
+ resolveDone = r;
89824
+ });
89806
89825
  const allResults = [];
89807
89826
  for await (const batch of chunked(files, concurrency)) (await Promise.allSettled(batch.map((filePath) => processFile(filePath, transformFn, apiForParser, dryRun)))).values().forEach((result) => {
89808
89827
  if (result.status === "fulfilled") allResults.push(result.value);
@@ -89813,8 +89832,7 @@ async function main() {
89813
89832
  });
89814
89833
  });
89815
89834
  resolveDone(allResults);
89816
- const results = await done;
89817
- const grouped = Object.groupBy(results, (r) => {
89835
+ const grouped = objectGroupBy(await done, (r) => {
89818
89836
  if (r.error) return "errors";
89819
89837
  if (r.changed) return "changed";
89820
89838
  return "unchanged";
@@ -45,9 +45,9 @@ declare const __groupIdBrand: unique symbol;
45
45
  type GroupId = string & {
46
46
  readonly [__groupIdBrand]: never;
47
47
  };
48
- /** Create a branded {@link ItemId} from a plain string. Ensures well-formed Unicode (ES2026). */
48
+ /** Create a branded {@link ItemId} from a plain string. Ensures well-formed Unicode. */
49
49
  declare function itemId(id: string): ItemId;
50
- /** Create a branded {@link GroupId} from a plain string. Ensures well-formed Unicode (ES2026). */
50
+ /** Create a branded {@link GroupId} from a plain string. Ensures well-formed Unicode. */
51
51
  declare function groupId(id: string): GroupId;
52
52
  /**
53
53
  * A registered command item in the palette.
@@ -107,8 +107,8 @@ interface CommandState {
107
107
  readonly pageStack: readonly string[];
108
108
  /** Whether the command palette dialog is open */
109
109
  readonly open: boolean;
110
- /** Temporal.Instant of the last state mutation (ES2026) */
111
- readonly lastUpdated: Temporal.Instant;
110
+ /** Epoch-millisecond timestamp of the last state mutation */
111
+ readonly lastUpdated: number;
112
112
  }
113
113
  /** All possible events the state machine accepts */
114
114
  type CommandEvent = {
@@ -153,7 +153,8 @@ type CommandEvent = {
153
153
  /** Frecency record for a single item */
154
154
  interface FrecencyRecord {
155
155
  readonly frequency: number;
156
- readonly lastUsed: Temporal.Instant;
156
+ /** Epoch-millisecond timestamp of last usage */
157
+ readonly lastUsed: number;
157
158
  }
158
159
  /** Complete frecency data for persistence */
159
160
  interface FrecencyData {
@@ -255,14 +256,14 @@ declare class IdbFrecencyStorage implements FrecencyStorage, AsyncDisposable {
255
256
  //#region src/core/frecency/index.d.ts
256
257
  /**
257
258
  * Computes a frecency bonus for an item based on usage frequency and recency.
258
- * Uses Temporal.Duration bucket boundaries (hour/day/week/month) with
259
+ * Uses hour/day/week/month bucket boundaries with
259
260
  * configurable exponential decay weights per time bucket.
260
261
  */
261
- declare function computeFrecencyBonus(history: FrecencyRecord, now?: Temporal.Instant, config?: FrecencyDecayConfig): number;
262
+ declare function computeFrecencyBonus(history: FrecencyRecord, now?: number, config?: FrecencyDecayConfig): number;
262
263
  /**
263
264
  * Stateful frecency engine that tracks item selection history and provides
264
265
  * ranking boosts for search results. Supports pluggable storage backends
265
- * and automatic flush on dispose. Uses Temporal.Now.instant() for timestamps.
266
+ * and automatic flush on dispose. Uses Date.now() for timestamps.
266
267
  */
267
268
  declare class FrecencyEngine implements Disposable, AsyncDisposable {
268
269
  #private;
@@ -272,9 +273,9 @@ declare class FrecencyEngine implements Disposable, AsyncDisposable {
272
273
  /** Record a selection event for an item */
273
274
  recordSelection(itemId: ItemId): void;
274
275
  /** Get frecency bonus for an item */
275
- getBonus(itemId: ItemId, now?: Temporal.Instant): number;
276
+ getBonus(itemId: ItemId, now?: number): number;
276
277
  /** Get all frecency bonuses as a map — for...of for hot ranking path (no closure overhead) */
277
- getAllBonuses(now?: Temporal.Instant): ReadonlyMap<ItemId, number>;
278
+ getAllBonuses(now?: number): ReadonlyMap<ItemId, number>;
278
279
  /** Get the decay configuration */
279
280
  getConfig(): FrecencyDecayConfig;
280
281
  /** Update decay configuration at runtime */
@@ -321,7 +322,7 @@ interface ParsedShortcut {
321
322
  }
322
323
  /**
323
324
  * Parse a shortcut string like "Mod+Shift+K" into a structured ParsedShortcut.
324
- * Uses String.isWellFormed() (ES2026) to ensure valid Unicode input.
325
+ * Ensures valid Unicode input via ensureWellFormed helper.
325
326
  */
326
327
  declare function parseShortcut(shortcut: string): ParsedShortcut;
327
328
  /**
@@ -340,8 +341,8 @@ declare function matchesShortcut(event: KeyboardEvent, shortcut: ParsedShortcut)
340
341
  declare function findMatchingShortcut(event: KeyboardEvent, shortcuts: readonly ParsedShortcut[]): ParsedShortcut | null;
341
342
  /**
342
343
  * Detects conflicting shortcut bindings by grouping on their normalized form
343
- * via Map.groupBy (ES2026). Returns only groups with two or more colliding shortcuts.
344
- * Map.groupBy returns a Map directly — no Object.entries conversion needed.
344
+ * via mapGroupBy helper. Returns only groups with two or more colliding shortcuts.
345
+ * mapGroupBy returns a Map directly — no Object.entries conversion needed.
345
346
  */
346
347
  declare function detectConflicts(shortcuts: readonly ParsedShortcut[]): ReadonlyMap<string, readonly ParsedShortcut[]>;
347
348
  //#endregion
@@ -379,7 +380,7 @@ declare class KeyboardShortcutRegistry implements Disposable {
379
380
  //#region src/core/registry.d.ts
380
381
  /**
381
382
  * Manages registration and lookup of command items and groups.
382
- * Uses ES2026 Set methods (union/intersection/difference) for efficient ID operations.
383
+ * Uses set operation helpers (union/intersection/difference) for efficient ID operations.
383
384
  * All registration methods return Disposable for automatic cleanup via `using`.
384
385
  */
385
386
  declare class CommandRegistry implements Disposable {
@@ -390,7 +391,7 @@ declare class CommandRegistry implements Disposable {
390
391
  registerItems(items: readonly CommandItem[]): Disposable;
391
392
  /** Unregister a single item — O(1) Map/Set delete, indexOf+toSpliced for order */
392
393
  unregisterItem(id: ItemId): void;
393
- /** Bulk unregister using Set.difference (ES2026) — for...of for hot path */
394
+ /** Bulk unregister — for...of for hot path */
394
395
  unregisterItems(ids: ReadonlySet<ItemId>): void;
395
396
  /** Register a group */
396
397
  registerGroup(group: CommandGroup): Disposable;
@@ -404,23 +405,23 @@ declare class CommandRegistry implements Disposable {
404
405
  getItems(): readonly CommandItem[];
405
406
  /** Get all registered groups sorted by priority — cached between group mutations */
406
407
  getGroups(): readonly CommandGroup[];
407
- /** Get items grouped by groupId using Map.groupBy (ES2026) — direct Map, no Object.entries conversion */
408
+ /** Get items grouped by groupId — direct Map, no Object.entries conversion */
408
409
  getGroupedItems(): ReadonlyMap<GroupId, readonly CommandItem[]>;
409
410
  /** Get item IDs as a Set — returns internal set (O(1), no allocation) */
410
411
  getItemIds(): ReadonlySet<ItemId>;
411
- /** Find intersection of registered items with a candidate set (ES2026 Set.intersection) */
412
+ /** Find intersection of registered items with a candidate set */
412
413
  intersectWith(candidateIds: ReadonlySet<ItemId>): ReadonlySet<ItemId>;
413
- /** Find items registered but not in a given set (ES2026 Set.difference) */
414
+ /** Find items registered but not in a given set */
414
415
  differenceFrom(otherIds: ReadonlySet<ItemId>): ReadonlySet<ItemId>;
415
- /** Union of registered IDs with another set (ES2026 Set.union) */
416
+ /** Union of registered IDs with another set */
416
417
  unionWith(otherIds: ReadonlySet<ItemId>): ReadonlySet<ItemId>;
417
- /** Check if registered items are a subset of the given set (ES2026 Set.isSubsetOf) */
418
+ /** Check if registered items are a subset of the given set */
418
419
  isSubsetOf(otherIds: ReadonlySet<ItemId>): boolean;
419
- /** Check if registered items are a superset of the given set (ES2026 Set.isSupersetOf) */
420
+ /** Check if registered items are a superset of the given set */
420
421
  isSupersetOf(otherIds: ReadonlySet<ItemId>): boolean;
421
- /** Check if registered items share no IDs with the given set (ES2026 Set.isDisjointFrom) */
422
+ /** Check if registered items share no IDs with the given set */
422
423
  isDisjointFrom(otherIds: ReadonlySet<ItemId>): boolean;
423
- /** Symmetric difference between registered IDs and another set (ES2026 Set.symmetricDifference) */
424
+ /** Symmetric difference between registered IDs and another set */
424
425
  symmetricDifferenceWith(otherIds: ReadonlySet<ItemId>): ReadonlySet<ItemId>;
425
426
  /** Total number of registered items */
426
427
  get size(): number;
@@ -471,12 +472,12 @@ declare function scoreItem(query: string, item: CommandItem): SearchResult | nul
471
472
  //#endregion
472
473
  //#region src/core/search/fuzzy-scorer.d.ts
473
474
  /**
474
- * Async-safe fuzzy scorer — wraps scoring in Promise.try (ES2026) for error resilience.
475
- * Falls back gracefully on error via Promise.try's synchronous-throw capture.
475
+ * Async-safe fuzzy scorer — wraps scoring in Promise for error resilience.
476
+ * Falls back gracefully on error via synchronous-throw capture.
476
477
  */
477
478
  declare function scoreItemAsync(query: string, item: CommandItem): Promise<SearchResult | null>;
478
479
  /**
479
- * Batch score items synchronously inside a single Promise.try (ES2026).
480
+ * Batch score items synchronously inside a single Promise.
480
481
  * Eliminates per-item microtask overhead from async generators — runs the
481
482
  * entire scoring pipeline in one synchronous pass, wrapped for error safety.
482
483
  */
@@ -508,13 +509,13 @@ interface CommandTelemetryHooks {
508
509
  interface TelemetryMiddleware {
509
510
  readonly onOpen: () => void;
510
511
  readonly onClose: () => void;
511
- readonly onSearch: (query: string, resultCount: number, startTime: Temporal.Instant) => void;
512
+ readonly onSearch: (query: string, resultCount: number, startTime: number) => void;
512
513
  readonly onSelect: (itemId: ItemId, query: string, position: number) => void;
513
514
  }
514
515
  /**
515
516
  * Creates a telemetry middleware that measures command palette interactions.
516
- * Uses Temporal.Now.instant() (ES2026) for nanosecond-precision timestamps
517
- * and Temporal.Instant.since() for accurate duration computation.
517
+ * Uses Date.now() for millisecond-precision timestamps
518
+ * and subtraction for duration computation.
518
519
  *
519
520
  * @example
520
521
  * ```ts