modern-cmdk 1.1.1 → 1.1.3
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 +3 -3
- package/dist/codemod/cli.mjs +21 -4
- package/dist/core/index.d.mts +28 -27
- package/dist/core/index.mjs +144 -63
- package/dist/react/index.d.mts +22 -13
- package/dist/react/index.mjs +191 -78
- package/dist/react/styles.css +1 -1
- package/package.json +1 -1
package/dist/react/index.mjs
CHANGED
|
@@ -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
|
|
102
|
+
/** Create a branded {@link ItemId} from a plain string. Ensures well-formed Unicode. */
|
|
85
103
|
function itemId(id) {
|
|
86
|
-
return
|
|
104
|
+
return ensureWellFormed(id);
|
|
87
105
|
}
|
|
88
|
-
/** Create a branded {@link GroupId} from a plain string. Ensures well-formed Unicode
|
|
106
|
+
/** Create a branded {@link GroupId} from a plain string. Ensures well-formed Unicode. */
|
|
89
107
|
function groupId(id) {
|
|
90
|
-
return
|
|
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:
|
|
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
|
|
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 =
|
|
155
|
-
const hours = now
|
|
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
|
|
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 =
|
|
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 =
|
|
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.
|
|
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,
|
|
@@ -256,7 +290,7 @@ function matchesShortcut(event, shortcut) {
|
|
|
256
290
|
* Map.groupBy returns a Map directly — no Object.entries conversion needed.
|
|
257
291
|
*/
|
|
258
292
|
function detectConflicts(shortcuts) {
|
|
259
|
-
const grouped =
|
|
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
|
-
*
|
|
314
|
+
* Ensures valid Unicode input via ensureWellFormed helper.
|
|
281
315
|
*/
|
|
282
316
|
function parseShortcut(shortcut) {
|
|
283
|
-
const safeShortcut =
|
|
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
|
|
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
|
|
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
|
|
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
|
|
585
|
+
/** Get items grouped by groupId — direct Map, no Object.entries conversion */
|
|
508
586
|
getGroupedItems() {
|
|
509
|
-
const
|
|
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
|
|
601
|
+
/** Find intersection of registered items with a candidate set */
|
|
525
602
|
intersectWith(candidateIds) {
|
|
526
|
-
return this.#itemOrderSet
|
|
603
|
+
return setIntersection(this.#itemOrderSet, candidateIds);
|
|
527
604
|
}
|
|
528
|
-
/** Find items registered but not in a given set
|
|
605
|
+
/** Find items registered but not in a given set */
|
|
529
606
|
differenceFrom(otherIds) {
|
|
530
|
-
return this.#itemOrderSet
|
|
607
|
+
return setDifference(this.#itemOrderSet, otherIds);
|
|
531
608
|
}
|
|
532
|
-
/** Union of registered IDs with another set
|
|
609
|
+
/** Union of registered IDs with another set */
|
|
533
610
|
unionWith(otherIds) {
|
|
534
|
-
return this.#itemOrderSet
|
|
611
|
+
return setUnion(this.#itemOrderSet, otherIds);
|
|
535
612
|
}
|
|
536
|
-
/** Check if registered items are a subset of the given set
|
|
613
|
+
/** Check if registered items are a subset of the given set */
|
|
537
614
|
isSubsetOf(otherIds) {
|
|
538
|
-
return this.#itemOrderSet
|
|
615
|
+
return setIsSubsetOf(this.#itemOrderSet, otherIds);
|
|
539
616
|
}
|
|
540
|
-
/** Check if registered items are a superset of the given set
|
|
617
|
+
/** Check if registered items are a superset of the given set */
|
|
541
618
|
isSupersetOf(otherIds) {
|
|
542
|
-
return this.#itemOrderSet
|
|
619
|
+
return setIsSupersetOf(this.#itemOrderSet, otherIds);
|
|
543
620
|
}
|
|
544
|
-
/** Check if registered items share no IDs with the given set
|
|
621
|
+
/** Check if registered items share no IDs with the given set */
|
|
545
622
|
isDisjointFrom(otherIds) {
|
|
546
|
-
return this.#itemOrderSet
|
|
623
|
+
return setIsDisjointFrom(this.#itemOrderSet, otherIds);
|
|
547
624
|
}
|
|
548
|
-
/** Symmetric difference between registered IDs and another set
|
|
625
|
+
/** Symmetric difference between registered IDs and another set */
|
|
549
626
|
symmetricDifferenceWith(otherIds) {
|
|
550
|
-
return this.#itemOrderSet
|
|
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
|
|
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 =
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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
|
-
|
|
1638
|
-
for (let i = 0; i < count; i++)
|
|
1639
|
-
return
|
|
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
|
-
|
|
1657
|
-
for (let i = 0; i < startIdx; 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
|
-
|
|
1688
|
-
for (let i = 0; i < clampedIndex; 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: "
|
|
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
|
|
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 ?
|
|
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 { visibleIdSet, virtualPositionMap } = useMemo(() => {
|
|
1859
|
+
if (!shouldVirtualize) return {
|
|
1860
|
+
visibleIdSet: null,
|
|
1861
|
+
virtualPositionMap: null
|
|
1862
|
+
};
|
|
1863
|
+
const { filteredIds } = stateCtx.state;
|
|
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
|
+
stateCtx.state.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__ */
|
|
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:
|
|
1802
|
-
|
|
1803
|
-
|
|
1804
|
-
|
|
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": "",
|
package/dist/react/styles.css
CHANGED