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 +3 -3
- package/dist/codemod/cli.mjs +22 -4
- package/dist/core/index.d.mts +30 -29
- package/dist/core/index.mjs +146 -65
- package/dist/react/index.d.mts +22 -13
- package/dist/react/index.mjs +193 -80
- package/dist/react/styles.css +1 -1
- package/package.json +1 -1
package/dist/core/index.mjs
CHANGED
|
@@ -1,16 +1,34 @@
|
|
|
1
|
+
//#region src/core/utils/string-wellformed.ts
|
|
2
|
+
/**
|
|
3
|
+
* Regex matching lone surrogates (unpaired high or low surrogates).
|
|
4
|
+
* A lone high surrogate (U+D800–U+DBFF) not followed by a low surrogate,
|
|
5
|
+
* or a lone low surrogate (U+DC00–U+DFFF) not preceded by a high surrogate.
|
|
6
|
+
*/
|
|
7
|
+
const LONE_SURROGATE_RE = /[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?<![\uD800-\uDBFF])[\uDC00-\uDFFF]/g;
|
|
8
|
+
/** Unicode replacement character used to replace lone surrogates. */
|
|
9
|
+
const REPLACEMENT_CHAR = "�";
|
|
10
|
+
/**
|
|
11
|
+
* Ensures a string is well-formed Unicode by replacing lone surrogates
|
|
12
|
+
* with the Unicode replacement character (U+FFFD).
|
|
13
|
+
* Cross-browser replacement for `str.isWellFormed() ? str : str.toWellFormed()`.
|
|
14
|
+
*/
|
|
15
|
+
function ensureWellFormed(str) {
|
|
16
|
+
return str.replace(LONE_SURROGATE_RE, REPLACEMENT_CHAR);
|
|
17
|
+
}
|
|
18
|
+
//#endregion
|
|
1
19
|
//#region src/core/frecency/idb-storage.ts
|
|
2
20
|
/** Serialize a FrecencyRecord to a JSON-safe format */
|
|
3
21
|
function serializeRecord(record) {
|
|
4
22
|
return {
|
|
5
23
|
frequency: record.frequency,
|
|
6
|
-
|
|
24
|
+
lastUsedMs: record.lastUsed
|
|
7
25
|
};
|
|
8
26
|
}
|
|
9
27
|
/** Deserialize a FrecencyRecord from its JSON-safe form */
|
|
10
28
|
function deserializeRecord(serialized) {
|
|
11
29
|
return {
|
|
12
30
|
frequency: serialized.frequency,
|
|
13
|
-
lastUsed:
|
|
31
|
+
lastUsed: serialized.lastUsedMs
|
|
14
32
|
};
|
|
15
33
|
}
|
|
16
34
|
/** Serialize FrecencyData for IndexedDB storage — uses Iterator Helpers */
|
|
@@ -21,9 +39,9 @@ function serializeData(data) {
|
|
|
21
39
|
function deserializeData(serialized) {
|
|
22
40
|
return { records: new Map(serialized.records.values().map(([id, record]) => [id, deserializeRecord(record)])) };
|
|
23
41
|
}
|
|
24
|
-
/** Build the namespaced IDB key —
|
|
42
|
+
/** Build the namespaced IDB key — ensures valid Unicode in key */
|
|
25
43
|
function storageKey(namespace) {
|
|
26
|
-
return `frecency:${
|
|
44
|
+
return `frecency:${ensureWellFormed(namespace)}`;
|
|
27
45
|
}
|
|
28
46
|
let idbModule;
|
|
29
47
|
/**
|
|
@@ -103,13 +121,13 @@ var IdbFrecencyStorage = class {
|
|
|
103
121
|
};
|
|
104
122
|
//#endregion
|
|
105
123
|
//#region src/core/types.ts
|
|
106
|
-
/** Create a branded {@link ItemId} from a plain string. Ensures well-formed Unicode
|
|
124
|
+
/** Create a branded {@link ItemId} from a plain string. Ensures well-formed Unicode. */
|
|
107
125
|
function itemId(id) {
|
|
108
|
-
return
|
|
126
|
+
return ensureWellFormed(id);
|
|
109
127
|
}
|
|
110
|
-
/** Create a branded {@link GroupId} from a plain string. Ensures well-formed Unicode
|
|
128
|
+
/** Create a branded {@link GroupId} from a plain string. Ensures well-formed Unicode. */
|
|
111
129
|
function groupId(id) {
|
|
112
|
-
return
|
|
130
|
+
return ensureWellFormed(id);
|
|
113
131
|
}
|
|
114
132
|
/** Default frecency decay configuration */
|
|
115
133
|
const DEFAULT_FRECENCY_DECAY = {
|
|
@@ -137,7 +155,7 @@ function createInitialState(options) {
|
|
|
137
155
|
page: "root",
|
|
138
156
|
pageStack: [],
|
|
139
157
|
open: options?.open ?? false,
|
|
140
|
-
lastUpdated:
|
|
158
|
+
lastUpdated: Date.now()
|
|
141
159
|
};
|
|
142
160
|
}
|
|
143
161
|
//#endregion
|
|
@@ -176,11 +194,11 @@ const BUCKET_HOURS = {
|
|
|
176
194
|
};
|
|
177
195
|
/**
|
|
178
196
|
* Computes a frecency bonus for an item based on usage frequency and recency.
|
|
179
|
-
* Uses
|
|
197
|
+
* Uses hour/day/week/month bucket boundaries with
|
|
180
198
|
* configurable exponential decay weights per time bucket.
|
|
181
199
|
*/
|
|
182
|
-
function computeFrecencyBonus(history, now =
|
|
183
|
-
const hours = now
|
|
200
|
+
function computeFrecencyBonus(history, now = Date.now(), config = DEFAULT_FRECENCY_DECAY) {
|
|
201
|
+
const hours = (now - history.lastUsed) / 36e5;
|
|
184
202
|
const { hourWeight = 4, dayWeight = 2, weekWeight = 1.5, monthWeight = 1, olderWeight = .5 } = config;
|
|
185
203
|
const recencyWeight = hours < BUCKET_HOURS.hour ? hourWeight : hours < BUCKET_HOURS.day ? dayWeight : hours < BUCKET_HOURS.week ? weekWeight : hours < BUCKET_HOURS.month ? monthWeight : olderWeight;
|
|
186
204
|
return history.frequency * recencyWeight;
|
|
@@ -188,7 +206,7 @@ function computeFrecencyBonus(history, now = Temporal.Now.instant(), config = DE
|
|
|
188
206
|
/**
|
|
189
207
|
* Stateful frecency engine that tracks item selection history and provides
|
|
190
208
|
* ranking boosts for search results. Supports pluggable storage backends
|
|
191
|
-
* and automatic flush on dispose. Uses
|
|
209
|
+
* and automatic flush on dispose. Uses Date.now() for timestamps.
|
|
192
210
|
*/
|
|
193
211
|
var FrecencyEngine = class {
|
|
194
212
|
#storage;
|
|
@@ -208,7 +226,7 @@ var FrecencyEngine = class {
|
|
|
208
226
|
}
|
|
209
227
|
/** Record a selection event for an item */
|
|
210
228
|
recordSelection(itemId) {
|
|
211
|
-
const now =
|
|
229
|
+
const now = Date.now();
|
|
212
230
|
const updated = {
|
|
213
231
|
frequency: (this.#data.records.get(itemId)?.frequency ?? 0) + 1,
|
|
214
232
|
lastUsed: now
|
|
@@ -223,7 +241,7 @@ var FrecencyEngine = class {
|
|
|
223
241
|
return computeFrecencyBonus(record, now, this.#config);
|
|
224
242
|
}
|
|
225
243
|
/** Get all frecency bonuses as a map — for...of for hot ranking path (no closure overhead) */
|
|
226
|
-
getAllBonuses(now =
|
|
244
|
+
getAllBonuses(now = Date.now()) {
|
|
227
245
|
if (this.#data.records.size === 0) return EMPTY_BONUS_MAP;
|
|
228
246
|
const bonuses = /* @__PURE__ */ new Map();
|
|
229
247
|
for (const [id, record] of this.#data.records) bonuses.set(id, computeFrecencyBonus(record, now, this.#config));
|
|
@@ -259,13 +277,29 @@ var FrecencyEngine = class {
|
|
|
259
277
|
this.#storage[Symbol.dispose]();
|
|
260
278
|
}
|
|
261
279
|
[Symbol.dispose]() {
|
|
262
|
-
if (this.#dirty) Promise.
|
|
280
|
+
if (this.#dirty) Promise.resolve().then(() => this.#storage.save(this.#namespace, this.#data)).catch((err) => {
|
|
263
281
|
console.warn("[modern-cmdk] FrecencyEngine dispose flush failed:", err);
|
|
264
282
|
});
|
|
265
283
|
this.#storage[Symbol.dispose]();
|
|
266
284
|
}
|
|
267
285
|
};
|
|
268
286
|
//#endregion
|
|
287
|
+
//#region src/core/utils/group-by.ts
|
|
288
|
+
/**
|
|
289
|
+
* Groups items from an iterable into a Map keyed by the result of `keyFn`.
|
|
290
|
+
* Cross-browser replacement for `Map.groupBy()`.
|
|
291
|
+
*/
|
|
292
|
+
function mapGroupBy(items, keyFn) {
|
|
293
|
+
const map = /* @__PURE__ */ new Map();
|
|
294
|
+
for (const item of items) {
|
|
295
|
+
const key = keyFn(item);
|
|
296
|
+
const group = map.get(key);
|
|
297
|
+
if (group) group.push(item);
|
|
298
|
+
else map.set(key, [item]);
|
|
299
|
+
}
|
|
300
|
+
return map;
|
|
301
|
+
}
|
|
302
|
+
//#endregion
|
|
269
303
|
//#region src/core/keyboard/matcher.ts
|
|
270
304
|
/**
|
|
271
305
|
* Tests whether a KeyboardEvent exactly matches a ParsedShortcut,
|
|
@@ -284,11 +318,11 @@ function findMatchingShortcut(event, shortcuts) {
|
|
|
284
318
|
}
|
|
285
319
|
/**
|
|
286
320
|
* Detects conflicting shortcut bindings by grouping on their normalized form
|
|
287
|
-
* via
|
|
288
|
-
*
|
|
321
|
+
* via mapGroupBy helper. Returns only groups with two or more colliding shortcuts.
|
|
322
|
+
* mapGroupBy returns a Map directly — no Object.entries conversion needed.
|
|
289
323
|
*/
|
|
290
324
|
function detectConflicts(shortcuts) {
|
|
291
|
-
const grouped =
|
|
325
|
+
const grouped = mapGroupBy(shortcuts, (s) => s.normalized);
|
|
292
326
|
return new Map(grouped.entries().filter(([, group]) => group.length > 1));
|
|
293
327
|
}
|
|
294
328
|
//#endregion
|
|
@@ -309,10 +343,10 @@ const MODIFIER_MAP = {
|
|
|
309
343
|
};
|
|
310
344
|
/**
|
|
311
345
|
* Parse a shortcut string like "Mod+Shift+K" into a structured ParsedShortcut.
|
|
312
|
-
*
|
|
346
|
+
* Ensures valid Unicode input via ensureWellFormed helper.
|
|
313
347
|
*/
|
|
314
348
|
function parseShortcut(shortcut) {
|
|
315
|
-
const safeShortcut =
|
|
349
|
+
const safeShortcut = ensureWellFormed(shortcut);
|
|
316
350
|
const parts = safeShortcut.split("+").map((p) => p.trim().toLowerCase());
|
|
317
351
|
let meta = false;
|
|
318
352
|
let ctrl = false;
|
|
@@ -440,10 +474,54 @@ var KeyboardShortcutRegistry = class {
|
|
|
440
474
|
}
|
|
441
475
|
};
|
|
442
476
|
//#endregion
|
|
477
|
+
//#region src/core/utils/set-ops.ts
|
|
478
|
+
/** Returns a new Set containing elements present in both `a` and `b`. */
|
|
479
|
+
function setIntersection(a, b) {
|
|
480
|
+
const result = /* @__PURE__ */ new Set();
|
|
481
|
+
const [smaller, larger] = a.size <= b.size ? [a, b] : [b, a];
|
|
482
|
+
for (const item of smaller) if (larger.has(item)) result.add(item);
|
|
483
|
+
return result;
|
|
484
|
+
}
|
|
485
|
+
/** Returns a new Set containing elements in `a` but not in `b`. */
|
|
486
|
+
function setDifference(a, b) {
|
|
487
|
+
const result = /* @__PURE__ */ new Set();
|
|
488
|
+
for (const item of a) if (!b.has(item)) result.add(item);
|
|
489
|
+
return result;
|
|
490
|
+
}
|
|
491
|
+
/** Returns a new Set containing all elements from both `a` and `b`. */
|
|
492
|
+
function setUnion(a, b) {
|
|
493
|
+
const result = new Set(a);
|
|
494
|
+
for (const item of b) result.add(item);
|
|
495
|
+
return result;
|
|
496
|
+
}
|
|
497
|
+
/** Returns `true` if every element in `a` is also in `b`. */
|
|
498
|
+
function setIsSubsetOf(a, b) {
|
|
499
|
+
if (a.size > b.size) return false;
|
|
500
|
+
for (const item of a) if (!b.has(item)) return false;
|
|
501
|
+
return true;
|
|
502
|
+
}
|
|
503
|
+
/** Returns `true` if every element in `b` is also in `a`. */
|
|
504
|
+
function setIsSupersetOf(a, b) {
|
|
505
|
+
return setIsSubsetOf(b, a);
|
|
506
|
+
}
|
|
507
|
+
/** Returns `true` if `a` and `b` share no elements. */
|
|
508
|
+
function setIsDisjointFrom(a, b) {
|
|
509
|
+
const [smaller, larger] = a.size <= b.size ? [a, b] : [b, a];
|
|
510
|
+
for (const item of smaller) if (larger.has(item)) return false;
|
|
511
|
+
return true;
|
|
512
|
+
}
|
|
513
|
+
/** Returns a new Set containing elements in either `a` or `b` but not both. */
|
|
514
|
+
function setSymmetricDifference(a, b) {
|
|
515
|
+
const result = new Set(a);
|
|
516
|
+
for (const item of b) if (result.has(item)) result.delete(item);
|
|
517
|
+
else result.add(item);
|
|
518
|
+
return result;
|
|
519
|
+
}
|
|
520
|
+
//#endregion
|
|
443
521
|
//#region src/core/registry.ts
|
|
444
522
|
/**
|
|
445
523
|
* Manages registration and lookup of command items and groups.
|
|
446
|
-
* Uses
|
|
524
|
+
* Uses set operation helpers (union/intersection/difference) for efficient ID operations.
|
|
447
525
|
* All registration methods return Disposable for automatic cleanup via `using`.
|
|
448
526
|
*/
|
|
449
527
|
var CommandRegistry = class {
|
|
@@ -495,10 +573,10 @@ var CommandRegistry = class {
|
|
|
495
573
|
if (idx !== -1) this.#itemOrder = this.#itemOrder.toSpliced(idx, 1);
|
|
496
574
|
this.#invalidateCache();
|
|
497
575
|
}
|
|
498
|
-
/** Bulk unregister
|
|
576
|
+
/** Bulk unregister — for...of for hot path */
|
|
499
577
|
unregisterItems(ids) {
|
|
500
578
|
for (const id of ids) this.#items.delete(id);
|
|
501
|
-
this.#itemOrderSet = this.#itemOrderSet
|
|
579
|
+
this.#itemOrderSet = setDifference(this.#itemOrderSet, ids);
|
|
502
580
|
this.#itemOrder = this.#itemOrder.filter((id) => !ids.has(id));
|
|
503
581
|
this.#invalidateCache();
|
|
504
582
|
}
|
|
@@ -536,10 +614,9 @@ var CommandRegistry = class {
|
|
|
536
614
|
this.#cachedGroups = this.#groups.values().toArray().toSorted((a, b) => (a.priority ?? 0) - (b.priority ?? 0));
|
|
537
615
|
return this.#cachedGroups;
|
|
538
616
|
}
|
|
539
|
-
/** Get items grouped by groupId
|
|
617
|
+
/** Get items grouped by groupId — direct Map, no Object.entries conversion */
|
|
540
618
|
getGroupedItems() {
|
|
541
|
-
const
|
|
542
|
-
const grouped = Map.groupBy(items, (item) => item.groupId ?? "__ungrouped");
|
|
619
|
+
const grouped = mapGroupBy(this.getItems(), (item) => item.groupId ?? "__ungrouped");
|
|
543
620
|
const result = /* @__PURE__ */ new Map();
|
|
544
621
|
for (const group of this.getGroups()) {
|
|
545
622
|
const groupItems = grouped.get(group.id);
|
|
@@ -553,33 +630,33 @@ var CommandRegistry = class {
|
|
|
553
630
|
getItemIds() {
|
|
554
631
|
return this.#itemOrderSet;
|
|
555
632
|
}
|
|
556
|
-
/** Find intersection of registered items with a candidate set
|
|
633
|
+
/** Find intersection of registered items with a candidate set */
|
|
557
634
|
intersectWith(candidateIds) {
|
|
558
|
-
return this.#itemOrderSet
|
|
635
|
+
return setIntersection(this.#itemOrderSet, candidateIds);
|
|
559
636
|
}
|
|
560
|
-
/** Find items registered but not in a given set
|
|
637
|
+
/** Find items registered but not in a given set */
|
|
561
638
|
differenceFrom(otherIds) {
|
|
562
|
-
return this.#itemOrderSet
|
|
639
|
+
return setDifference(this.#itemOrderSet, otherIds);
|
|
563
640
|
}
|
|
564
|
-
/** Union of registered IDs with another set
|
|
641
|
+
/** Union of registered IDs with another set */
|
|
565
642
|
unionWith(otherIds) {
|
|
566
|
-
return this.#itemOrderSet
|
|
643
|
+
return setUnion(this.#itemOrderSet, otherIds);
|
|
567
644
|
}
|
|
568
|
-
/** Check if registered items are a subset of the given set
|
|
645
|
+
/** Check if registered items are a subset of the given set */
|
|
569
646
|
isSubsetOf(otherIds) {
|
|
570
|
-
return this.#itemOrderSet
|
|
647
|
+
return setIsSubsetOf(this.#itemOrderSet, otherIds);
|
|
571
648
|
}
|
|
572
|
-
/** Check if registered items are a superset of the given set
|
|
649
|
+
/** Check if registered items are a superset of the given set */
|
|
573
650
|
isSupersetOf(otherIds) {
|
|
574
|
-
return this.#itemOrderSet
|
|
651
|
+
return setIsSupersetOf(this.#itemOrderSet, otherIds);
|
|
575
652
|
}
|
|
576
|
-
/** Check if registered items share no IDs with the given set
|
|
653
|
+
/** Check if registered items share no IDs with the given set */
|
|
577
654
|
isDisjointFrom(otherIds) {
|
|
578
|
-
return this.#itemOrderSet
|
|
655
|
+
return setIsDisjointFrom(this.#itemOrderSet, otherIds);
|
|
579
656
|
}
|
|
580
|
-
/** Symmetric difference between registered IDs and another set
|
|
657
|
+
/** Symmetric difference between registered IDs and another set */
|
|
581
658
|
symmetricDifferenceWith(otherIds) {
|
|
582
|
-
return this.#itemOrderSet
|
|
659
|
+
return setSymmetricDifference(this.#itemOrderSet, otherIds);
|
|
583
660
|
}
|
|
584
661
|
/** Total number of registered items */
|
|
585
662
|
get size() {
|
|
@@ -825,7 +902,7 @@ function createSearchEngine(options) {
|
|
|
825
902
|
},
|
|
826
903
|
remove(ids) {
|
|
827
904
|
for (const id of ids) indexedEntries.delete(id);
|
|
828
|
-
previousResults = previousResults
|
|
905
|
+
previousResults = setDifference(previousResults, ids);
|
|
829
906
|
},
|
|
830
907
|
clear() {
|
|
831
908
|
indexedEntries.clear();
|
|
@@ -1009,7 +1086,7 @@ function createCommandMachine(options = {}) {
|
|
|
1009
1086
|
}
|
|
1010
1087
|
filteredIdSet = new Set(filteredIds);
|
|
1011
1088
|
filteredIdIndex = new Map(filteredIds.entries().map(([i, id]) => [id, i]));
|
|
1012
|
-
const groupedIds =
|
|
1089
|
+
const groupedIds = mapGroupBy(filteredIds, (id) => {
|
|
1013
1090
|
return registry.getItem(id)?.groupId ?? "__ungrouped";
|
|
1014
1091
|
});
|
|
1015
1092
|
const activeId = state.activeId && filteredIdSet.has(state.activeId) ? state.activeId : filteredIds[0] ?? null;
|
|
@@ -1020,7 +1097,7 @@ function createCommandMachine(options = {}) {
|
|
|
1020
1097
|
groupedIds,
|
|
1021
1098
|
filteredCount: filteredIds.length,
|
|
1022
1099
|
activeId,
|
|
1023
|
-
lastUpdated:
|
|
1100
|
+
lastUpdated: Date.now()
|
|
1024
1101
|
});
|
|
1025
1102
|
}
|
|
1026
1103
|
function navigate(direction) {
|
|
@@ -1032,7 +1109,7 @@ function createCommandMachine(options = {}) {
|
|
|
1032
1109
|
setState({
|
|
1033
1110
|
...state,
|
|
1034
1111
|
activeId: newActiveId,
|
|
1035
|
-
lastUpdated:
|
|
1112
|
+
lastUpdated: Date.now()
|
|
1036
1113
|
});
|
|
1037
1114
|
options.onActiveChange?.(newActiveId);
|
|
1038
1115
|
}
|
|
@@ -1044,7 +1121,7 @@ function createCommandMachine(options = {}) {
|
|
|
1044
1121
|
setState({
|
|
1045
1122
|
...state,
|
|
1046
1123
|
activeId: newActiveId,
|
|
1047
|
-
lastUpdated:
|
|
1124
|
+
lastUpdated: Date.now()
|
|
1048
1125
|
});
|
|
1049
1126
|
options.onActiveChange?.(newActiveId);
|
|
1050
1127
|
}
|
|
@@ -1062,7 +1139,7 @@ function createCommandMachine(options = {}) {
|
|
|
1062
1139
|
setState({
|
|
1063
1140
|
...state,
|
|
1064
1141
|
activeId: newActiveId,
|
|
1065
|
-
lastUpdated:
|
|
1142
|
+
lastUpdated: Date.now()
|
|
1066
1143
|
});
|
|
1067
1144
|
options.onActiveChange?.(newActiveId);
|
|
1068
1145
|
}
|
|
@@ -1088,7 +1165,7 @@ function createCommandMachine(options = {}) {
|
|
|
1088
1165
|
setState({
|
|
1089
1166
|
...state,
|
|
1090
1167
|
activeId: event.id,
|
|
1091
|
-
lastUpdated:
|
|
1168
|
+
lastUpdated: Date.now()
|
|
1092
1169
|
});
|
|
1093
1170
|
options.onActiveChange?.(event.id);
|
|
1094
1171
|
}
|
|
@@ -1101,7 +1178,7 @@ function createCommandMachine(options = {}) {
|
|
|
1101
1178
|
...state,
|
|
1102
1179
|
page: event.page,
|
|
1103
1180
|
pageStack: [...state.pageStack, state.page],
|
|
1104
|
-
lastUpdated:
|
|
1181
|
+
lastUpdated: Date.now()
|
|
1105
1182
|
});
|
|
1106
1183
|
break;
|
|
1107
1184
|
case "PAGE_POP": {
|
|
@@ -1110,7 +1187,7 @@ function createCommandMachine(options = {}) {
|
|
|
1110
1187
|
...state,
|
|
1111
1188
|
page: prevPage,
|
|
1112
1189
|
pageStack: state.pageStack.toSpliced(-1, 1),
|
|
1113
|
-
lastUpdated:
|
|
1190
|
+
lastUpdated: Date.now()
|
|
1114
1191
|
});
|
|
1115
1192
|
break;
|
|
1116
1193
|
}
|
|
@@ -1118,7 +1195,7 @@ function createCommandMachine(options = {}) {
|
|
|
1118
1195
|
setState({
|
|
1119
1196
|
...state,
|
|
1120
1197
|
open: true,
|
|
1121
|
-
lastUpdated:
|
|
1198
|
+
lastUpdated: Date.now()
|
|
1122
1199
|
});
|
|
1123
1200
|
emitter.emit("openChange", true);
|
|
1124
1201
|
options.onOpenChange?.(true);
|
|
@@ -1127,7 +1204,7 @@ function createCommandMachine(options = {}) {
|
|
|
1127
1204
|
setState({
|
|
1128
1205
|
...state,
|
|
1129
1206
|
open: false,
|
|
1130
|
-
lastUpdated:
|
|
1207
|
+
lastUpdated: Date.now()
|
|
1131
1208
|
});
|
|
1132
1209
|
emitter.emit("openChange", false);
|
|
1133
1210
|
options.onOpenChange?.(false);
|
|
@@ -1137,7 +1214,7 @@ function createCommandMachine(options = {}) {
|
|
|
1137
1214
|
setState({
|
|
1138
1215
|
...state,
|
|
1139
1216
|
open: newOpen,
|
|
1140
|
-
lastUpdated:
|
|
1217
|
+
lastUpdated: Date.now()
|
|
1141
1218
|
});
|
|
1142
1219
|
emitter.emit("openChange", newOpen);
|
|
1143
1220
|
options.onOpenChange?.(newOpen);
|
|
@@ -1213,26 +1290,30 @@ function createCommandMachine(options = {}) {
|
|
|
1213
1290
|
//#endregion
|
|
1214
1291
|
//#region src/core/search/fuzzy-scorer.ts
|
|
1215
1292
|
/**
|
|
1216
|
-
* Async-safe fuzzy scorer — wraps scoring in Promise
|
|
1217
|
-
* Falls back gracefully on error via
|
|
1293
|
+
* Async-safe fuzzy scorer — wraps scoring in Promise for error resilience.
|
|
1294
|
+
* Falls back gracefully on error via synchronous-throw capture.
|
|
1218
1295
|
*/
|
|
1219
1296
|
function scoreItemAsync(query, item) {
|
|
1220
|
-
return Promise
|
|
1297
|
+
return new Promise((resolve) => {
|
|
1298
|
+
resolve(scoreItem(query, item));
|
|
1299
|
+
});
|
|
1221
1300
|
}
|
|
1222
1301
|
/**
|
|
1223
|
-
* Batch score items synchronously inside a single Promise.
|
|
1302
|
+
* Batch score items synchronously inside a single Promise.
|
|
1224
1303
|
* Eliminates per-item microtask overhead from async generators — runs the
|
|
1225
1304
|
* entire scoring pipeline in one synchronous pass, wrapped for error safety.
|
|
1226
1305
|
*/
|
|
1227
1306
|
function batchScoreItems(query, items) {
|
|
1228
|
-
return Promise
|
|
1307
|
+
return new Promise((resolve) => {
|
|
1308
|
+
resolve(items.values().map((item) => scoreItem(query, item)).filter((r) => r != null).toArray().toSorted((a, b) => b.score - a.score));
|
|
1309
|
+
});
|
|
1229
1310
|
}
|
|
1230
1311
|
//#endregion
|
|
1231
1312
|
//#region src/core/telemetry.ts
|
|
1232
1313
|
/**
|
|
1233
1314
|
* Creates a telemetry middleware that measures command palette interactions.
|
|
1234
|
-
* Uses
|
|
1235
|
-
* and
|
|
1315
|
+
* Uses Date.now() for millisecond-precision timestamps
|
|
1316
|
+
* and subtraction for duration computation.
|
|
1236
1317
|
*
|
|
1237
1318
|
* @example
|
|
1238
1319
|
* ```ts
|
|
@@ -1246,19 +1327,19 @@ function createTelemetryMiddleware(hooks) {
|
|
|
1246
1327
|
let openTimestamp = null;
|
|
1247
1328
|
return {
|
|
1248
1329
|
onOpen() {
|
|
1249
|
-
openTimestamp =
|
|
1330
|
+
openTimestamp = Date.now();
|
|
1250
1331
|
hooks.onPaletteOpen?.();
|
|
1251
1332
|
},
|
|
1252
1333
|
onClose() {
|
|
1253
1334
|
if (openTimestamp != null) {
|
|
1254
|
-
const
|
|
1255
|
-
hooks.onPaletteClose?.(
|
|
1335
|
+
const durationMs = Date.now() - openTimestamp;
|
|
1336
|
+
hooks.onPaletteClose?.(durationMs);
|
|
1256
1337
|
openTimestamp = null;
|
|
1257
1338
|
}
|
|
1258
1339
|
},
|
|
1259
1340
|
onSearch(query, resultCount, startTime) {
|
|
1260
|
-
const
|
|
1261
|
-
hooks.onSearchComplete?.(query, resultCount,
|
|
1341
|
+
const durationMs = Date.now() - startTime;
|
|
1342
|
+
hooks.onSearchComplete?.(query, resultCount, durationMs);
|
|
1262
1343
|
},
|
|
1263
1344
|
onSelect(itemId, query, position) {
|
|
1264
1345
|
hooks.onItemSelected?.(itemId, query, position);
|
package/dist/react/index.d.mts
CHANGED
|
@@ -123,8 +123,8 @@ interface CommandState {
|
|
|
123
123
|
readonly pageStack: readonly string[];
|
|
124
124
|
/** Whether the command palette dialog is open */
|
|
125
125
|
readonly open: boolean;
|
|
126
|
-
/**
|
|
127
|
-
readonly lastUpdated:
|
|
126
|
+
/** Epoch-millisecond timestamp of the last state mutation */
|
|
127
|
+
readonly lastUpdated: number;
|
|
128
128
|
}
|
|
129
129
|
/** All possible events the state machine accepts */
|
|
130
130
|
type CommandEvent = {
|
|
@@ -169,7 +169,8 @@ type CommandEvent = {
|
|
|
169
169
|
/** Frecency record for a single item */
|
|
170
170
|
interface FrecencyRecord {
|
|
171
171
|
readonly frequency: number;
|
|
172
|
-
|
|
172
|
+
/** Epoch-millisecond timestamp of last usage */
|
|
173
|
+
readonly lastUsed: number;
|
|
173
174
|
}
|
|
174
175
|
/** Complete frecency data for persistence */
|
|
175
176
|
interface FrecencyData {
|
|
@@ -274,7 +275,7 @@ declare class KeyboardShortcutRegistry implements Disposable {
|
|
|
274
275
|
//#region src/core/registry.d.ts
|
|
275
276
|
/**
|
|
276
277
|
* Manages registration and lookup of command items and groups.
|
|
277
|
-
* Uses
|
|
278
|
+
* Uses set operation helpers (union/intersection/difference) for efficient ID operations.
|
|
278
279
|
* All registration methods return Disposable for automatic cleanup via `using`.
|
|
279
280
|
*/
|
|
280
281
|
declare class CommandRegistry implements Disposable {
|
|
@@ -285,7 +286,7 @@ declare class CommandRegistry implements Disposable {
|
|
|
285
286
|
registerItems(items: readonly CommandItem$1[]): Disposable;
|
|
286
287
|
/** Unregister a single item — O(1) Map/Set delete, indexOf+toSpliced for order */
|
|
287
288
|
unregisterItem(id: ItemId): void;
|
|
288
|
-
/** Bulk unregister
|
|
289
|
+
/** Bulk unregister — for...of for hot path */
|
|
289
290
|
unregisterItems(ids: ReadonlySet<ItemId>): void;
|
|
290
291
|
/** Register a group */
|
|
291
292
|
registerGroup(group: CommandGroup$1): Disposable;
|
|
@@ -299,23 +300,23 @@ declare class CommandRegistry implements Disposable {
|
|
|
299
300
|
getItems(): readonly CommandItem$1[];
|
|
300
301
|
/** Get all registered groups sorted by priority — cached between group mutations */
|
|
301
302
|
getGroups(): readonly CommandGroup$1[];
|
|
302
|
-
/** Get items grouped by groupId
|
|
303
|
+
/** Get items grouped by groupId — direct Map, no Object.entries conversion */
|
|
303
304
|
getGroupedItems(): ReadonlyMap<GroupId, readonly CommandItem$1[]>;
|
|
304
305
|
/** Get item IDs as a Set — returns internal set (O(1), no allocation) */
|
|
305
306
|
getItemIds(): ReadonlySet<ItemId>;
|
|
306
|
-
/** Find intersection of registered items with a candidate set
|
|
307
|
+
/** Find intersection of registered items with a candidate set */
|
|
307
308
|
intersectWith(candidateIds: ReadonlySet<ItemId>): ReadonlySet<ItemId>;
|
|
308
|
-
/** Find items registered but not in a given set
|
|
309
|
+
/** Find items registered but not in a given set */
|
|
309
310
|
differenceFrom(otherIds: ReadonlySet<ItemId>): ReadonlySet<ItemId>;
|
|
310
|
-
/** Union of registered IDs with another set
|
|
311
|
+
/** Union of registered IDs with another set */
|
|
311
312
|
unionWith(otherIds: ReadonlySet<ItemId>): ReadonlySet<ItemId>;
|
|
312
|
-
/** Check if registered items are a subset of the given set
|
|
313
|
+
/** Check if registered items are a subset of the given set */
|
|
313
314
|
isSubsetOf(otherIds: ReadonlySet<ItemId>): boolean;
|
|
314
|
-
/** Check if registered items are a superset of the given set
|
|
315
|
+
/** Check if registered items are a superset of the given set */
|
|
315
316
|
isSupersetOf(otherIds: ReadonlySet<ItemId>): boolean;
|
|
316
|
-
/** Check if registered items share no IDs with the given set
|
|
317
|
+
/** Check if registered items share no IDs with the given set */
|
|
317
318
|
isDisjointFrom(otherIds: ReadonlySet<ItemId>): boolean;
|
|
318
|
-
/** Symmetric difference between registered IDs and another set
|
|
319
|
+
/** Symmetric difference between registered IDs and another set */
|
|
319
320
|
symmetricDifferenceWith(otherIds: ReadonlySet<ItemId>): ReadonlySet<ItemId>;
|
|
320
321
|
/** Total number of registered items */
|
|
321
322
|
get size(): number;
|
|
@@ -593,6 +594,10 @@ interface CommandContextValue {
|
|
|
593
594
|
readonly setOptimisticActiveId: (id: ItemId | null) => void;
|
|
594
595
|
/** O(1) membership check for filtered items — avoids O(n) Array.includes per item */
|
|
595
596
|
readonly filteredIdSet: ReadonlySet<ItemId>;
|
|
597
|
+
/** IDs currently visible in virtual viewport + overscan. null = not virtualizing (render all filtered). */
|
|
598
|
+
readonly visibleIdSet: ReadonlySet<ItemId> | null;
|
|
599
|
+
/** Map of visible item IDs to their translateY pixel offsets. null = not virtualizing. */
|
|
600
|
+
readonly virtualPositionMap: ReadonlyMap<ItemId, number> | null;
|
|
596
601
|
readonly rootId: CommandRootId;
|
|
597
602
|
readonly listId: string;
|
|
598
603
|
readonly inputId: string;
|
|
@@ -613,6 +618,10 @@ interface CommandStateContextValue {
|
|
|
613
618
|
readonly state: CommandState;
|
|
614
619
|
readonly isPending: boolean;
|
|
615
620
|
readonly filteredIdSet: ReadonlySet<ItemId>;
|
|
621
|
+
/** IDs currently visible in virtual viewport + overscan. null = not virtualizing (render all filtered). */
|
|
622
|
+
readonly visibleIdSet: ReadonlySet<ItemId> | null;
|
|
623
|
+
/** Map of visible item IDs to their translateY pixel offsets. null = not virtualizing. */
|
|
624
|
+
readonly virtualPositionMap: ReadonlyMap<ItemId, number> | null;
|
|
616
625
|
}
|
|
617
626
|
declare const CommandStableContext: Context<CommandStableContextValue | null>;
|
|
618
627
|
declare const CommandStateContext: Context<CommandStateContextValue | null>;
|