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.
@@ -80,14 +80,32 @@ function CommandBadge({ ref, children, ...props }) {
80
80
  });
81
81
  }
82
82
  //#endregion
83
+ //#region src/core/utils/string-wellformed.ts
84
+ /**
85
+ * Regex matching lone surrogates (unpaired high or low surrogates).
86
+ * A lone high surrogate (U+D800–U+DBFF) not followed by a low surrogate,
87
+ * or a lone low surrogate (U+DC00–U+DFFF) not preceded by a high surrogate.
88
+ */
89
+ const LONE_SURROGATE_RE = /[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?<![\uD800-\uDBFF])[\uDC00-\uDFFF]/g;
90
+ /** Unicode replacement character used to replace lone surrogates. */
91
+ const REPLACEMENT_CHAR = "�";
92
+ /**
93
+ * Ensures a string is well-formed Unicode by replacing lone surrogates
94
+ * with the Unicode replacement character (U+FFFD).
95
+ * Cross-browser replacement for `str.isWellFormed() ? str : str.toWellFormed()`.
96
+ */
97
+ function ensureWellFormed(str) {
98
+ return str.replace(LONE_SURROGATE_RE, REPLACEMENT_CHAR);
99
+ }
100
+ //#endregion
83
101
  //#region src/core/types.ts
84
- /** Create a branded {@link ItemId} from a plain string. Ensures well-formed Unicode (ES2026). */
102
+ /** Create a branded {@link ItemId} from a plain string. Ensures well-formed Unicode. */
85
103
  function itemId(id) {
86
- return id.isWellFormed() ? id : id.toWellFormed();
104
+ return ensureWellFormed(id);
87
105
  }
88
- /** Create a branded {@link GroupId} from a plain string. Ensures well-formed Unicode (ES2026). */
106
+ /** Create a branded {@link GroupId} from a plain string. Ensures well-formed Unicode. */
89
107
  function groupId(id) {
90
- return id.isWellFormed() ? id : id.toWellFormed();
108
+ return ensureWellFormed(id);
91
109
  }
92
110
  /** Default frecency decay configuration */
93
111
  const DEFAULT_FRECENCY_DECAY = {
@@ -109,7 +127,7 @@ function createInitialState(options) {
109
127
  page: "root",
110
128
  pageStack: [],
111
129
  open: options?.open ?? false,
112
- lastUpdated: Temporal.Now.instant()
130
+ lastUpdated: Date.now()
113
131
  };
114
132
  }
115
133
  //#endregion
@@ -148,11 +166,11 @@ const BUCKET_HOURS = {
148
166
  };
149
167
  /**
150
168
  * Computes a frecency bonus for an item based on usage frequency and recency.
151
- * Uses Temporal.Duration bucket boundaries (hour/day/week/month) with
169
+ * Uses hour/day/week/month bucket boundaries with
152
170
  * configurable exponential decay weights per time bucket.
153
171
  */
154
- function computeFrecencyBonus(history, now = Temporal.Now.instant(), config = DEFAULT_FRECENCY_DECAY) {
155
- const hours = now.since(history.lastUsed).total("hours");
172
+ function computeFrecencyBonus(history, now = Date.now(), config = DEFAULT_FRECENCY_DECAY) {
173
+ const hours = (now - history.lastUsed) / 36e5;
156
174
  const { hourWeight = 4, dayWeight = 2, weekWeight = 1.5, monthWeight = 1, olderWeight = .5 } = config;
157
175
  const recencyWeight = hours < BUCKET_HOURS.hour ? hourWeight : hours < BUCKET_HOURS.day ? dayWeight : hours < BUCKET_HOURS.week ? weekWeight : hours < BUCKET_HOURS.month ? monthWeight : olderWeight;
158
176
  return history.frequency * recencyWeight;
@@ -160,7 +178,7 @@ function computeFrecencyBonus(history, now = Temporal.Now.instant(), config = DE
160
178
  /**
161
179
  * Stateful frecency engine that tracks item selection history and provides
162
180
  * ranking boosts for search results. Supports pluggable storage backends
163
- * and automatic flush on dispose. Uses Temporal.Now.instant() for timestamps.
181
+ * and automatic flush on dispose. Uses Date.now() for timestamps.
164
182
  */
165
183
  var FrecencyEngine = class {
166
184
  #storage;
@@ -180,7 +198,7 @@ var FrecencyEngine = class {
180
198
  }
181
199
  /** Record a selection event for an item */
182
200
  recordSelection(itemId) {
183
- const now = Temporal.Now.instant();
201
+ const now = Date.now();
184
202
  const updated = {
185
203
  frequency: (this.#data.records.get(itemId)?.frequency ?? 0) + 1,
186
204
  lastUsed: now
@@ -195,7 +213,7 @@ var FrecencyEngine = class {
195
213
  return computeFrecencyBonus(record, now, this.#config);
196
214
  }
197
215
  /** Get all frecency bonuses as a map — for...of for hot ranking path (no closure overhead) */
198
- getAllBonuses(now = Temporal.Now.instant()) {
216
+ getAllBonuses(now = Date.now()) {
199
217
  if (this.#data.records.size === 0) return EMPTY_BONUS_MAP;
200
218
  const bonuses = /* @__PURE__ */ new Map();
201
219
  for (const [id, record] of this.#data.records) bonuses.set(id, computeFrecencyBonus(record, now, this.#config));
@@ -231,13 +249,29 @@ var FrecencyEngine = class {
231
249
  this.#storage[Symbol.dispose]();
232
250
  }
233
251
  [Symbol.dispose]() {
234
- if (this.#dirty) Promise.try(() => this.#storage.save(this.#namespace, this.#data)).catch((err) => {
252
+ if (this.#dirty) Promise.resolve().then(() => this.#storage.save(this.#namespace, this.#data)).catch((err) => {
235
253
  console.warn("[modern-cmdk] FrecencyEngine dispose flush failed:", err);
236
254
  });
237
255
  this.#storage[Symbol.dispose]();
238
256
  }
239
257
  };
240
258
  //#endregion
259
+ //#region src/core/utils/group-by.ts
260
+ /**
261
+ * Groups items from an iterable into a Map keyed by the result of `keyFn`.
262
+ * Cross-browser replacement for `Map.groupBy()`.
263
+ */
264
+ function mapGroupBy(items, keyFn) {
265
+ const map = /* @__PURE__ */ new Map();
266
+ for (const item of items) {
267
+ const key = keyFn(item);
268
+ const group = map.get(key);
269
+ if (group) group.push(item);
270
+ else map.set(key, [item]);
271
+ }
272
+ return map;
273
+ }
274
+ //#endregion
241
275
  //#region src/core/keyboard/matcher.ts
242
276
  /**
243
277
  * Tests whether a KeyboardEvent exactly matches a ParsedShortcut,
@@ -252,11 +286,11 @@ function matchesShortcut(event, shortcut) {
252
286
  }
253
287
  /**
254
288
  * 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.
289
+ * via mapGroupBy helper. Returns only groups with two or more colliding shortcuts.
290
+ * mapGroupBy returns a Map directly — no Object.entries conversion needed.
257
291
  */
258
292
  function detectConflicts(shortcuts) {
259
- const grouped = Map.groupBy(shortcuts, (s) => s.normalized);
293
+ const grouped = mapGroupBy(shortcuts, (s) => s.normalized);
260
294
  return new Map(grouped.entries().filter(([, group]) => group.length > 1));
261
295
  }
262
296
  //#endregion
@@ -277,10 +311,10 @@ const MODIFIER_MAP = {
277
311
  };
278
312
  /**
279
313
  * Parse a shortcut string like "Mod+Shift+K" into a structured ParsedShortcut.
280
- * Uses String.isWellFormed() (ES2026) to ensure valid Unicode input.
314
+ * Ensures valid Unicode input via ensureWellFormed helper.
281
315
  */
282
316
  function parseShortcut(shortcut) {
283
- const safeShortcut = shortcut.isWellFormed() ? shortcut : shortcut.toWellFormed();
317
+ const safeShortcut = ensureWellFormed(shortcut);
284
318
  const parts = safeShortcut.split("+").map((p) => p.trim().toLowerCase());
285
319
  let meta = false;
286
320
  let ctrl = false;
@@ -408,10 +442,54 @@ var KeyboardShortcutRegistry = class {
408
442
  }
409
443
  };
410
444
  //#endregion
445
+ //#region src/core/utils/set-ops.ts
446
+ /** Returns a new Set containing elements present in both `a` and `b`. */
447
+ function setIntersection(a, b) {
448
+ const result = /* @__PURE__ */ new Set();
449
+ const [smaller, larger] = a.size <= b.size ? [a, b] : [b, a];
450
+ for (const item of smaller) if (larger.has(item)) result.add(item);
451
+ return result;
452
+ }
453
+ /** Returns a new Set containing elements in `a` but not in `b`. */
454
+ function setDifference(a, b) {
455
+ const result = /* @__PURE__ */ new Set();
456
+ for (const item of a) if (!b.has(item)) result.add(item);
457
+ return result;
458
+ }
459
+ /** Returns a new Set containing all elements from both `a` and `b`. */
460
+ function setUnion(a, b) {
461
+ const result = new Set(a);
462
+ for (const item of b) result.add(item);
463
+ return result;
464
+ }
465
+ /** Returns `true` if every element in `a` is also in `b`. */
466
+ function setIsSubsetOf(a, b) {
467
+ if (a.size > b.size) return false;
468
+ for (const item of a) if (!b.has(item)) return false;
469
+ return true;
470
+ }
471
+ /** Returns `true` if every element in `b` is also in `a`. */
472
+ function setIsSupersetOf(a, b) {
473
+ return setIsSubsetOf(b, a);
474
+ }
475
+ /** Returns `true` if `a` and `b` share no elements. */
476
+ function setIsDisjointFrom(a, b) {
477
+ const [smaller, larger] = a.size <= b.size ? [a, b] : [b, a];
478
+ for (const item of smaller) if (larger.has(item)) return false;
479
+ return true;
480
+ }
481
+ /** Returns a new Set containing elements in either `a` or `b` but not both. */
482
+ function setSymmetricDifference(a, b) {
483
+ const result = new Set(a);
484
+ for (const item of b) if (result.has(item)) result.delete(item);
485
+ else result.add(item);
486
+ return result;
487
+ }
488
+ //#endregion
411
489
  //#region src/core/registry.ts
412
490
  /**
413
491
  * Manages registration and lookup of command items and groups.
414
- * Uses ES2026 Set methods (union/intersection/difference) for efficient ID operations.
492
+ * Uses set operation helpers (union/intersection/difference) for efficient ID operations.
415
493
  * All registration methods return Disposable for automatic cleanup via `using`.
416
494
  */
417
495
  var CommandRegistry = class {
@@ -463,10 +541,10 @@ var CommandRegistry = class {
463
541
  if (idx !== -1) this.#itemOrder = this.#itemOrder.toSpliced(idx, 1);
464
542
  this.#invalidateCache();
465
543
  }
466
- /** Bulk unregister using Set.difference (ES2026) — for...of for hot path */
544
+ /** Bulk unregister — for...of for hot path */
467
545
  unregisterItems(ids) {
468
546
  for (const id of ids) this.#items.delete(id);
469
- this.#itemOrderSet = this.#itemOrderSet.difference(ids);
547
+ this.#itemOrderSet = setDifference(this.#itemOrderSet, ids);
470
548
  this.#itemOrder = this.#itemOrder.filter((id) => !ids.has(id));
471
549
  this.#invalidateCache();
472
550
  }
@@ -504,10 +582,9 @@ var CommandRegistry = class {
504
582
  this.#cachedGroups = this.#groups.values().toArray().toSorted((a, b) => (a.priority ?? 0) - (b.priority ?? 0));
505
583
  return this.#cachedGroups;
506
584
  }
507
- /** Get items grouped by groupId using Map.groupBy (ES2026) — direct Map, no Object.entries conversion */
585
+ /** Get items grouped by groupId — direct Map, no Object.entries conversion */
508
586
  getGroupedItems() {
509
- const items = this.getItems();
510
- const grouped = Map.groupBy(items, (item) => item.groupId ?? "__ungrouped");
587
+ const grouped = mapGroupBy(this.getItems(), (item) => item.groupId ?? "__ungrouped");
511
588
  const result = /* @__PURE__ */ new Map();
512
589
  for (const group of this.getGroups()) {
513
590
  const groupItems = grouped.get(group.id);
@@ -521,33 +598,33 @@ var CommandRegistry = class {
521
598
  getItemIds() {
522
599
  return this.#itemOrderSet;
523
600
  }
524
- /** Find intersection of registered items with a candidate set (ES2026 Set.intersection) */
601
+ /** Find intersection of registered items with a candidate set */
525
602
  intersectWith(candidateIds) {
526
- return this.#itemOrderSet.intersection(candidateIds);
603
+ return setIntersection(this.#itemOrderSet, candidateIds);
527
604
  }
528
- /** Find items registered but not in a given set (ES2026 Set.difference) */
605
+ /** Find items registered but not in a given set */
529
606
  differenceFrom(otherIds) {
530
- return this.#itemOrderSet.difference(otherIds);
607
+ return setDifference(this.#itemOrderSet, otherIds);
531
608
  }
532
- /** Union of registered IDs with another set (ES2026 Set.union) */
609
+ /** Union of registered IDs with another set */
533
610
  unionWith(otherIds) {
534
- return this.#itemOrderSet.union(otherIds);
611
+ return setUnion(this.#itemOrderSet, otherIds);
535
612
  }
536
- /** Check if registered items are a subset of the given set (ES2026 Set.isSubsetOf) */
613
+ /** Check if registered items are a subset of the given set */
537
614
  isSubsetOf(otherIds) {
538
- return this.#itemOrderSet.isSubsetOf(otherIds);
615
+ return setIsSubsetOf(this.#itemOrderSet, otherIds);
539
616
  }
540
- /** Check if registered items are a superset of the given set (ES2026 Set.isSupersetOf) */
617
+ /** Check if registered items are a superset of the given set */
541
618
  isSupersetOf(otherIds) {
542
- return this.#itemOrderSet.isSupersetOf(otherIds);
619
+ return setIsSupersetOf(this.#itemOrderSet, otherIds);
543
620
  }
544
- /** Check if registered items share no IDs with the given set (ES2026 Set.isDisjointFrom) */
621
+ /** Check if registered items share no IDs with the given set */
545
622
  isDisjointFrom(otherIds) {
546
- return this.#itemOrderSet.isDisjointFrom(otherIds);
623
+ return setIsDisjointFrom(this.#itemOrderSet, otherIds);
547
624
  }
548
- /** Symmetric difference between registered IDs and another set (ES2026 Set.symmetricDifference) */
625
+ /** Symmetric difference between registered IDs and another set */
549
626
  symmetricDifferenceWith(otherIds) {
550
- return this.#itemOrderSet.symmetricDifference(otherIds);
627
+ return setSymmetricDifference(this.#itemOrderSet, otherIds);
551
628
  }
552
629
  /** Total number of registered items */
553
630
  get size() {
@@ -753,7 +830,7 @@ function createSearchEngine(options) {
753
830
  },
754
831
  remove(ids) {
755
832
  for (const id of ids) indexedEntries.delete(id);
756
- previousResults = previousResults.difference(ids);
833
+ previousResults = setDifference(previousResults, ids);
757
834
  },
758
835
  clear() {
759
836
  indexedEntries.clear();
@@ -937,7 +1014,7 @@ function createCommandMachine(options = {}) {
937
1014
  }
938
1015
  filteredIdSet = new Set(filteredIds);
939
1016
  filteredIdIndex = new Map(filteredIds.entries().map(([i, id]) => [id, i]));
940
- const groupedIds = Map.groupBy(filteredIds, (id) => {
1017
+ const groupedIds = mapGroupBy(filteredIds, (id) => {
941
1018
  return registry.getItem(id)?.groupId ?? "__ungrouped";
942
1019
  });
943
1020
  const activeId = state.activeId && filteredIdSet.has(state.activeId) ? state.activeId : filteredIds[0] ?? null;
@@ -948,7 +1025,7 @@ function createCommandMachine(options = {}) {
948
1025
  groupedIds,
949
1026
  filteredCount: filteredIds.length,
950
1027
  activeId,
951
- lastUpdated: Temporal.Now.instant()
1028
+ lastUpdated: Date.now()
952
1029
  });
953
1030
  }
954
1031
  function navigate(direction) {
@@ -960,7 +1037,7 @@ function createCommandMachine(options = {}) {
960
1037
  setState({
961
1038
  ...state,
962
1039
  activeId: newActiveId,
963
- lastUpdated: Temporal.Now.instant()
1040
+ lastUpdated: Date.now()
964
1041
  });
965
1042
  options.onActiveChange?.(newActiveId);
966
1043
  }
@@ -972,7 +1049,7 @@ function createCommandMachine(options = {}) {
972
1049
  setState({
973
1050
  ...state,
974
1051
  activeId: newActiveId,
975
- lastUpdated: Temporal.Now.instant()
1052
+ lastUpdated: Date.now()
976
1053
  });
977
1054
  options.onActiveChange?.(newActiveId);
978
1055
  }
@@ -990,7 +1067,7 @@ function createCommandMachine(options = {}) {
990
1067
  setState({
991
1068
  ...state,
992
1069
  activeId: newActiveId,
993
- lastUpdated: Temporal.Now.instant()
1070
+ lastUpdated: Date.now()
994
1071
  });
995
1072
  options.onActiveChange?.(newActiveId);
996
1073
  }
@@ -1016,7 +1093,7 @@ function createCommandMachine(options = {}) {
1016
1093
  setState({
1017
1094
  ...state,
1018
1095
  activeId: event.id,
1019
- lastUpdated: Temporal.Now.instant()
1096
+ lastUpdated: Date.now()
1020
1097
  });
1021
1098
  options.onActiveChange?.(event.id);
1022
1099
  }
@@ -1029,7 +1106,7 @@ function createCommandMachine(options = {}) {
1029
1106
  ...state,
1030
1107
  page: event.page,
1031
1108
  pageStack: [...state.pageStack, state.page],
1032
- lastUpdated: Temporal.Now.instant()
1109
+ lastUpdated: Date.now()
1033
1110
  });
1034
1111
  break;
1035
1112
  case "PAGE_POP": {
@@ -1038,7 +1115,7 @@ function createCommandMachine(options = {}) {
1038
1115
  ...state,
1039
1116
  page: prevPage,
1040
1117
  pageStack: state.pageStack.toSpliced(-1, 1),
1041
- lastUpdated: Temporal.Now.instant()
1118
+ lastUpdated: Date.now()
1042
1119
  });
1043
1120
  break;
1044
1121
  }
@@ -1046,7 +1123,7 @@ function createCommandMachine(options = {}) {
1046
1123
  setState({
1047
1124
  ...state,
1048
1125
  open: true,
1049
- lastUpdated: Temporal.Now.instant()
1126
+ lastUpdated: Date.now()
1050
1127
  });
1051
1128
  emitter.emit("openChange", true);
1052
1129
  options.onOpenChange?.(true);
@@ -1055,7 +1132,7 @@ function createCommandMachine(options = {}) {
1055
1132
  setState({
1056
1133
  ...state,
1057
1134
  open: false,
1058
- lastUpdated: Temporal.Now.instant()
1135
+ lastUpdated: Date.now()
1059
1136
  });
1060
1137
  emitter.emit("openChange", false);
1061
1138
  options.onOpenChange?.(false);
@@ -1065,7 +1142,7 @@ function createCommandMachine(options = {}) {
1065
1142
  setState({
1066
1143
  ...state,
1067
1144
  open: newOpen,
1068
- lastUpdated: Temporal.Now.instant()
1145
+ lastUpdated: Date.now()
1069
1146
  });
1070
1147
  emitter.emit("openChange", newOpen);
1071
1148
  options.onOpenChange?.(newOpen);
@@ -1269,7 +1346,9 @@ function useCommandSetup(machineFactory, label) {
1269
1346
  stateContextValue: useMemo(() => ({
1270
1347
  state,
1271
1348
  isPending,
1272
- filteredIdSet
1349
+ filteredIdSet,
1350
+ visibleIdSet: null,
1351
+ virtualPositionMap: null
1273
1352
  }), [
1274
1353
  state,
1275
1354
  isPending,
@@ -1562,6 +1641,7 @@ function CommandItem({ ref, children, value, keywords, onSelect, disabled = fals
1562
1641
  });
1563
1642
  const isActive = stateCtx.state.activeId === id;
1564
1643
  const isFiltered = stateCtx.filteredIdSet.has(id);
1644
+ const isVisible = stateCtx.visibleIdSet === null || stateCtx.visibleIdSet.has(id);
1565
1645
  const handleSelect = useCallback(() => {
1566
1646
  if (!disabled) stable.machine.send({
1567
1647
  type: "ITEM_SELECT",
@@ -1588,7 +1668,15 @@ function CommandItem({ ref, children, value, keywords, onSelect, disabled = fals
1588
1668
  id,
1589
1669
  disabled
1590
1670
  ]);
1591
- if (!isFiltered) return null;
1671
+ if (!isFiltered || !isVisible) return null;
1672
+ const virtualStart = stateCtx.virtualPositionMap?.get(id);
1673
+ const virtualStyle = virtualStart !== void 0 ? {
1674
+ position: "absolute",
1675
+ top: 0,
1676
+ left: 0,
1677
+ width: "100%",
1678
+ translate: `0 ${virtualStart}px`
1679
+ } : void 0;
1592
1680
  return /* @__PURE__ */ jsx("div", {
1593
1681
  ref,
1594
1682
  "data-command-item": "",
@@ -1602,6 +1690,7 @@ function CommandItem({ ref, children, value, keywords, onSelect, disabled = fals
1602
1690
  "aria-keyshortcuts": shortcut ?? void 0,
1603
1691
  onClick: handleSelect,
1604
1692
  onPointerMove: handlePointerMove,
1693
+ style: virtualStyle,
1605
1694
  ...props,
1606
1695
  children
1607
1696
  });
@@ -1634,9 +1723,9 @@ function useVirtualizer(options) {
1634
1723
  const getItemSize = useCallback((index) => measuredSizes.current.get(index) ?? estimateSize, [estimateSize]);
1635
1724
  const totalSize = useMemo(() => {
1636
1725
  if (!enabled) return 0;
1637
- const sizes = [];
1638
- for (let i = 0; i < count; i++) sizes.push(getItemSize(i));
1639
- return Math.sumPrecise(sizes);
1726
+ let total = 0;
1727
+ for (let i = 0; i < count; i++) total += getItemSize(i);
1728
+ return total;
1640
1729
  }, [
1641
1730
  enabled,
1642
1731
  count,
@@ -1653,9 +1742,8 @@ function useVirtualizer(options) {
1653
1742
  startIdx++;
1654
1743
  }
1655
1744
  startIdx = Math.max(0, startIdx - overscan);
1656
- const offsetSizes = [];
1657
- for (let i = 0; i < startIdx; i++) offsetSizes.push(getItemSize(i));
1658
- offset = Math.sumPrecise(offsetSizes);
1745
+ offset = 0;
1746
+ for (let i = 0; i < startIdx; i++) offset += getItemSize(i);
1659
1747
  const items = [];
1660
1748
  let currentOffset = offset;
1661
1749
  const endOffset = scrollOffset + containerHeight;
@@ -1684,12 +1772,11 @@ function useVirtualizer(options) {
1684
1772
  scrollToIndex: useCallback((index) => {
1685
1773
  if (!scrollElement || !enabled) return;
1686
1774
  const clampedIndex = Math.min(index, count - 1);
1687
- const sizes = [];
1688
- for (let i = 0; i < clampedIndex; i++) sizes.push(getItemSize(i));
1689
- const targetOffset = Math.sumPrecise(sizes);
1775
+ let targetOffset = 0;
1776
+ for (let i = 0; i < clampedIndex; i++) targetOffset += getItemSize(i);
1690
1777
  scrollElement.scrollTo({
1691
1778
  top: targetOffset,
1692
- behavior: "smooth"
1779
+ behavior: "instant"
1693
1780
  });
1694
1781
  }, [
1695
1782
  scrollElement,
@@ -1715,21 +1802,18 @@ function useVirtualizer(options) {
1715
1802
  }
1716
1803
  //#endregion
1717
1804
  //#region src/react/list.tsx
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
1805
  /** Scrollable list container with auto-virtualization and aria-live announcements. */
1726
1806
  function CommandList({ ref, children, virtualize, estimateSize = 44, overscan = 8, announceResults, style, ...props }) {
1727
1807
  const stable = useStableContext("Command.List");
1728
1808
  const stateCtx = useStateContext("Command.List");
1729
1809
  const innerRef = useRef(null);
1730
1810
  const scrollRef = useRef(null);
1811
+ const [scrollElement, setScrollElement] = useState(null);
1731
1812
  const [height, setHeight] = useState(0);
1732
- const shouldVirtualize = virtualize ?? stateCtx.state.filteredCount > 100;
1813
+ const shouldVirtualizeRef = useRef(false);
1814
+ if (virtualize ?? stateCtx.state.filteredCount > 100) shouldVirtualizeRef.current = true;
1815
+ else if (stateCtx.state.filteredCount <= 50) shouldVirtualizeRef.current = false;
1816
+ const shouldVirtualize = virtualize ?? shouldVirtualizeRef.current;
1733
1817
  useEffect(() => {
1734
1818
  const element = innerRef.current;
1735
1819
  if (!element) return;
@@ -1746,7 +1830,7 @@ function CommandList({ ref, children, virtualize, estimateSize = 44, overscan =
1746
1830
  count: stateCtx.state.filteredCount,
1747
1831
  estimateSize,
1748
1832
  overscan,
1749
- scrollElement: shouldVirtualize ? scrollRef.current : null,
1833
+ scrollElement: shouldVirtualize ? scrollElement : null,
1750
1834
  enabled: shouldVirtualize
1751
1835
  });
1752
1836
  const prevActiveId = useRef(stateCtx.state.activeId);
@@ -1771,10 +1855,44 @@ function CommandList({ ref, children, virtualize, estimateSize = 44, overscan =
1771
1855
  ...style,
1772
1856
  "--command-list-height": `${height.toFixed(1)}px`
1773
1857
  }), [style, height]);
1858
+ const filteredIds = stateCtx.state.filteredIds;
1859
+ const { visibleIdSet, virtualPositionMap } = useMemo(() => {
1860
+ if (!shouldVirtualize) return {
1861
+ visibleIdSet: null,
1862
+ virtualPositionMap: null
1863
+ };
1864
+ const idSet = /* @__PURE__ */ new Set();
1865
+ const posMap = /* @__PURE__ */ new Map();
1866
+ for (const vItem of virtualizer.virtualItems) {
1867
+ const id = filteredIds[vItem.index];
1868
+ if (id !== void 0) {
1869
+ idSet.add(id);
1870
+ posMap.set(id, vItem.start);
1871
+ }
1872
+ }
1873
+ return {
1874
+ visibleIdSet: idSet,
1875
+ virtualPositionMap: posMap
1876
+ };
1877
+ }, [
1878
+ shouldVirtualize,
1879
+ virtualizer.virtualItems,
1880
+ filteredIds
1881
+ ]);
1882
+ const virtualizedStateCtx = useMemo(() => ({
1883
+ ...stateCtx,
1884
+ visibleIdSet,
1885
+ virtualPositionMap
1886
+ }), [
1887
+ stateCtx,
1888
+ visibleIdSet,
1889
+ virtualPositionMap
1890
+ ]);
1774
1891
  const externalRefRef = useRef(ref);
1775
1892
  externalRefRef.current = ref;
1776
1893
  const setScrollRef = useCallback((el) => {
1777
1894
  scrollRef.current = el;
1895
+ setScrollElement(el);
1778
1896
  const externalRef = externalRefRef.current;
1779
1897
  if (typeof externalRef === "function") externalRef(el);
1780
1898
  else if (externalRef) externalRef.current = el;
@@ -1792,21 +1910,16 @@ function CommandList({ ref, children, virtualize, estimateSize = 44, overscan =
1792
1910
  id: stable.listId,
1793
1911
  style: mergedStyle,
1794
1912
  ...props,
1795
- children: shouldVirtualize ? /* @__PURE__ */ jsxs("div", {
1913
+ children: shouldVirtualize ? /* @__PURE__ */ jsx("div", {
1796
1914
  "data-command-list-virtual": "",
1797
1915
  style: {
1798
1916
  height: `${virtualizer.totalSize}px`,
1799
1917
  position: "relative"
1800
1918
  },
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]
1919
+ children: /* @__PURE__ */ jsx(CommandStateContext, {
1920
+ value: virtualizedStateCtx,
1921
+ children
1922
+ })
1810
1923
  }) : /* @__PURE__ */ jsx("div", {
1811
1924
  ref: innerRef,
1812
1925
  "data-command-list-inner": "",
@@ -430,7 +430,7 @@
430
430
 
431
431
  [data-command-scroll-indicator] {
432
432
  block-size: 2px;
433
- transform-origin: left center;
433
+ transform-origin: inline-start center;
434
434
  scale: 0 1;
435
435
  animation: scroll-progress linear both;
436
436
  animation-timeline: --list-scroll;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "modern-cmdk",
3
- "version": "1.1.1",
3
+ "version": "1.1.4",
4
4
  "type": "module",
5
5
  "description": "The definitive command palette engine for the modern web",
6
6
  "license": "MIT",