modern-cmdk 1.2.2 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +109 -54
- package/dist/codemod/cli.d.mts +16 -1
- package/dist/codemod/cli.mjs +14173 -60921
- package/dist/codemod/transforms/data-attrs.d.mts +1 -3
- package/dist/codemod/transforms/data-attrs.mjs +2 -3
- package/dist/codemod/transforms/forward-ref.d.mts +1 -3
- package/dist/codemod/transforms/forward-ref.mjs +1 -2
- package/dist/codemod/transforms/import-rewrite.d.mts +1 -3
- package/dist/codemod/transforms/import-rewrite.mjs +1 -2
- package/dist/codemod/transforms/should-filter.d.mts +1 -3
- package/dist/codemod/transforms/should-filter.mjs +1 -2
- package/dist/core/index.d.mts +10 -42
- package/dist/core/index.mjs +46 -48
- package/dist/react/index.d.mts +18 -70
- package/dist/react/index.mjs +92 -101
- package/dist/react/styles.css +16 -7
- package/package.json +4 -4
- package/CHANGELOG.md +0 -98
package/dist/react/index.mjs
CHANGED
|
@@ -4,10 +4,10 @@ import { Component, Suspense, createContext, use, useCallback, useEffect, useId,
|
|
|
4
4
|
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
5
5
|
import { Dialog } from "radix-ui";
|
|
6
6
|
import { createPortal } from "react-dom";
|
|
7
|
-
|
|
7
|
+
// src/react/activity.tsx
|
|
8
8
|
const ActivityComponent = "Activity" in React ? React.Activity : null;
|
|
9
9
|
/**
|
|
10
|
-
* Wraps content in React
|
|
10
|
+
* Wraps content in React's <Activity> API (React 19.2+).
|
|
11
11
|
* When mode="hidden", the component tree is preserved in memory but not rendered.
|
|
12
12
|
* Falls back to conditional rendering if Activity is unavailable.
|
|
13
13
|
*/
|
|
@@ -19,8 +19,7 @@ function CommandActivity({ mode, children }) {
|
|
|
19
19
|
if (mode === "hidden") return null;
|
|
20
20
|
return /* @__PURE__ */ jsx(Fragment, { children });
|
|
21
21
|
}
|
|
22
|
-
|
|
23
|
-
//#region src/react/context.ts
|
|
22
|
+
// src/react/context.ts
|
|
24
23
|
const CommandStableContext = createContext(null);
|
|
25
24
|
CommandStableContext.displayName = "CommandStableContext";
|
|
26
25
|
const CommandStateContext = createContext(null);
|
|
@@ -42,8 +41,7 @@ CommandGroupContext.displayName = "CommandGroupContext";
|
|
|
42
41
|
/** Context for the status container rendered outside the listbox — used by Command.Empty */
|
|
43
42
|
const CommandListStatusContext = createContext(null);
|
|
44
43
|
CommandListStatusContext.displayName = "CommandListStatusContext";
|
|
45
|
-
|
|
46
|
-
//#region src/react/async-items.tsx
|
|
44
|
+
// src/react/async-items.tsx
|
|
47
45
|
/** Inner component that suspends via use() until items resolve */
|
|
48
46
|
function AsyncItemsInner({ items: itemsPromise, children }) {
|
|
49
47
|
const stable = useStableContext("Command.AsyncItems");
|
|
@@ -68,8 +66,7 @@ function CommandAsyncItems({ items, fallback, children }) {
|
|
|
68
66
|
})
|
|
69
67
|
});
|
|
70
68
|
}
|
|
71
|
-
|
|
72
|
-
//#region src/react/badge.tsx
|
|
69
|
+
// src/react/badge.tsx
|
|
73
70
|
/** Inline badge for categorization or status indicators on command items. */
|
|
74
71
|
function CommandBadge({ ref, children, ...props }) {
|
|
75
72
|
return /* @__PURE__ */ jsx("span", {
|
|
@@ -79,8 +76,7 @@ function CommandBadge({ ref, children, ...props }) {
|
|
|
79
76
|
children
|
|
80
77
|
});
|
|
81
78
|
}
|
|
82
|
-
|
|
83
|
-
//#region src/core/utils/string-wellformed.ts
|
|
79
|
+
// src/core/utils/string-wellformed.ts
|
|
84
80
|
/**
|
|
85
81
|
* Regex matching lone surrogates (unpaired high or low surrogates).
|
|
86
82
|
* A lone high surrogate (U+D800–U+DBFF) not followed by a low surrogate,
|
|
@@ -97,8 +93,7 @@ const REPLACEMENT_CHAR = "�";
|
|
|
97
93
|
function ensureWellFormed(str) {
|
|
98
94
|
return str.replace(LONE_SURROGATE_RE, REPLACEMENT_CHAR);
|
|
99
95
|
}
|
|
100
|
-
|
|
101
|
-
//#region src/core/types.ts
|
|
96
|
+
// src/core/types.ts
|
|
102
97
|
/** Create a branded {@link ItemId} from a plain string. Ensures well-formed Unicode. */
|
|
103
98
|
function itemId(id) {
|
|
104
99
|
return ensureWellFormed(id);
|
|
@@ -130,8 +125,7 @@ function createInitialState(options) {
|
|
|
130
125
|
lastUpdated: Date.now()
|
|
131
126
|
};
|
|
132
127
|
}
|
|
133
|
-
|
|
134
|
-
//#region src/core/frecency/memory-storage.ts
|
|
128
|
+
// src/core/frecency/memory-storage.ts
|
|
135
129
|
/**
|
|
136
130
|
* In-memory implementation of FrecencyStorage using a Map.
|
|
137
131
|
* Data does not persist across page reloads. Suitable for ephemeral
|
|
@@ -145,7 +139,7 @@ var MemoryFrecencyStorage = class {
|
|
|
145
139
|
save(namespace, data) {
|
|
146
140
|
this.#store.set(namespace, data);
|
|
147
141
|
}
|
|
148
|
-
/** Serialize all stored data — Iterator Helpers pipeline (
|
|
142
|
+
/** Serialize all stored data — Iterator Helpers pipeline (ES2025) */
|
|
149
143
|
toJSON() {
|
|
150
144
|
return Object.fromEntries(this.#store.entries().map(([namespace, data]) => [namespace, { records: data.records.entries().toArray() }]));
|
|
151
145
|
}
|
|
@@ -153,11 +147,10 @@ var MemoryFrecencyStorage = class {
|
|
|
153
147
|
this.#store.clear();
|
|
154
148
|
}
|
|
155
149
|
};
|
|
156
|
-
|
|
157
|
-
//#region src/core/frecency/index.ts
|
|
150
|
+
// src/core/frecency/index.ts
|
|
158
151
|
/** Shared empty bonus map — avoids allocation when no frecency records exist */
|
|
159
152
|
const EMPTY_BONUS_MAP = /* @__PURE__ */ new Map();
|
|
160
|
-
/**
|
|
153
|
+
/** Bucket boundaries as hour constants — no per-call unit conversion */
|
|
161
154
|
const BUCKET_HOURS = {
|
|
162
155
|
hour: 1,
|
|
163
156
|
day: 24,
|
|
@@ -167,7 +160,7 @@ const BUCKET_HOURS = {
|
|
|
167
160
|
/**
|
|
168
161
|
* Computes a frecency bonus for an item based on usage frequency and recency.
|
|
169
162
|
* Uses hour/day/week/month bucket boundaries with
|
|
170
|
-
* configurable
|
|
163
|
+
* configurable step weights per recency bucket (hour/day/week/month/older).
|
|
171
164
|
*/
|
|
172
165
|
function computeFrecencyBonus(history, now = Date.now(), config = DEFAULT_FRECENCY_DECAY) {
|
|
173
166
|
const hours = (now - history.lastUsed) / 36e5;
|
|
@@ -256,8 +249,7 @@ var FrecencyEngine = class {
|
|
|
256
249
|
this.#storage[Symbol.dispose]();
|
|
257
250
|
}
|
|
258
251
|
};
|
|
259
|
-
|
|
260
|
-
//#region src/core/utils/group-by.ts
|
|
252
|
+
// src/core/utils/group-by.ts
|
|
261
253
|
/**
|
|
262
254
|
* Groups items from an iterable into a Map keyed by the result of `keyFn`.
|
|
263
255
|
* Cross-browser replacement for `Map.groupBy()`.
|
|
@@ -272,8 +264,7 @@ function mapGroupBy(items, keyFn) {
|
|
|
272
264
|
}
|
|
273
265
|
return map;
|
|
274
266
|
}
|
|
275
|
-
|
|
276
|
-
//#region src/core/keyboard/matcher.ts
|
|
267
|
+
// src/core/keyboard/matcher.ts
|
|
277
268
|
/**
|
|
278
269
|
* Tests whether a KeyboardEvent exactly matches a ParsedShortcut,
|
|
279
270
|
* comparing all four modifier keys (meta, ctrl, shift, alt) and the primary key.
|
|
@@ -294,8 +285,7 @@ function detectConflicts(shortcuts) {
|
|
|
294
285
|
const grouped = mapGroupBy(shortcuts, (s) => s.normalized);
|
|
295
286
|
return new Map(grouped.entries().filter(([, group]) => group.length > 1));
|
|
296
287
|
}
|
|
297
|
-
|
|
298
|
-
//#region src/core/keyboard/parser.ts
|
|
288
|
+
// src/core/keyboard/parser.ts
|
|
299
289
|
const isMac = typeof navigator !== "undefined" && (navigator.userAgentData?.platform === "macOS" || /Mac|iPod|iPhone|iPad/.test(navigator.platform));
|
|
300
290
|
/** Modifier aliases — cross-platform Mod resolution */
|
|
301
291
|
const MODIFIER_MAP = {
|
|
@@ -368,12 +358,11 @@ function formatShortcut(parsed) {
|
|
|
368
358
|
parts.push(displayKey);
|
|
369
359
|
return isMac ? parts.join("") : parts.join("+");
|
|
370
360
|
}
|
|
371
|
-
|
|
372
|
-
//#region src/core/keyboard/index.ts
|
|
361
|
+
// src/core/keyboard/index.ts
|
|
373
362
|
/**
|
|
374
363
|
* Manages keyboard shortcut bindings with global keydown listening.
|
|
375
364
|
* Supports `using` for automatic cleanup of both individual bindings and the
|
|
376
|
-
* global document listener. Detects conflicts
|
|
365
|
+
* global document listener. Detects conflicts with the local mapGroupBy helper (core/utils/group-by.ts).
|
|
377
366
|
*/
|
|
378
367
|
var KeyboardShortcutRegistry = class {
|
|
379
368
|
#bindings = /* @__PURE__ */ new Map();
|
|
@@ -440,8 +429,7 @@ var KeyboardShortcutRegistry = class {
|
|
|
440
429
|
this.#bindings.clear();
|
|
441
430
|
}
|
|
442
431
|
};
|
|
443
|
-
|
|
444
|
-
//#region src/core/utils/set-ops.ts
|
|
432
|
+
// src/core/utils/set-ops.ts
|
|
445
433
|
/** Returns a new Set containing elements present in both `a` and `b`. */
|
|
446
434
|
function setIntersection(a, b) {
|
|
447
435
|
const result = /* @__PURE__ */ new Set();
|
|
@@ -484,8 +472,7 @@ function setSymmetricDifference(a, b) {
|
|
|
484
472
|
else result.add(item);
|
|
485
473
|
return result;
|
|
486
474
|
}
|
|
487
|
-
|
|
488
|
-
//#region src/core/registry.ts
|
|
475
|
+
// src/core/registry.ts
|
|
489
476
|
/**
|
|
490
477
|
* Manages registration and lookup of command items and groups.
|
|
491
478
|
* Uses set operation helpers (union/intersection/difference) for efficient ID operations.
|
|
@@ -646,8 +633,23 @@ var CommandRegistry = class {
|
|
|
646
633
|
this.clear();
|
|
647
634
|
}
|
|
648
635
|
};
|
|
649
|
-
|
|
650
|
-
|
|
636
|
+
// src/core/utils/env.ts
|
|
637
|
+
/**
|
|
638
|
+
* `true` unless `process.env.NODE_ENV` is `'production'`.
|
|
639
|
+
*
|
|
640
|
+
* Consumer bundlers (Vite, webpack, Next, esbuild) replace `process.env.NODE_ENV` statically,
|
|
641
|
+
* so the dev-only branches guarded by this flag fold away in production builds. A browser
|
|
642
|
+
* running the unbundled module throws a ReferenceError on the bare `process` reference;
|
|
643
|
+
* the catch turns that into the production value (`false`), so dev-only code stays off.
|
|
644
|
+
*/
|
|
645
|
+
const isDev = (() => {
|
|
646
|
+
try {
|
|
647
|
+
return process.env.NODE_ENV !== "production";
|
|
648
|
+
} catch {
|
|
649
|
+
return false;
|
|
650
|
+
}
|
|
651
|
+
})();
|
|
652
|
+
// src/core/search/default-scorer.ts
|
|
651
653
|
function isWordSeparator(code) {
|
|
652
654
|
return code === 32 || code === 9 || code === 45 || code === 95 || code === 46 || code === 47 || code === 92;
|
|
653
655
|
}
|
|
@@ -769,18 +771,16 @@ function scoreFuzzy(query, qLen, lowerTarget, tLen) {
|
|
|
769
771
|
matches
|
|
770
772
|
};
|
|
771
773
|
}
|
|
772
|
-
|
|
773
|
-
//#region src/core/search/index.ts
|
|
774
|
-
const __DEV__$1 = globalThis.process ? globalThis.process.env.NODE_ENV !== "production" : false;
|
|
774
|
+
// src/core/search/index.ts
|
|
775
775
|
let leakRegistry;
|
|
776
|
-
if (
|
|
776
|
+
if (isDev && typeof window !== "undefined") leakRegistry = new FinalizationRegistry((label) => {
|
|
777
777
|
console.warn(`[modern-cmdk] ${label} was garbage collected without being disposed. Use \`using\` or call [Symbol.dispose]() to prevent leaks.`);
|
|
778
778
|
});
|
|
779
779
|
/**
|
|
780
780
|
* Creates a search engine with pluggable scorer and incremental filtering.
|
|
781
781
|
* When the query extends the previous one, only prior matches are re-scored.
|
|
782
782
|
* Pre-caches lowercase at index time — zero toLowerCase() calls during search.
|
|
783
|
-
* Uses Iterator Helpers (
|
|
783
|
+
* Uses Iterator Helpers (ES2025) for result pipelines.
|
|
784
784
|
*/
|
|
785
785
|
function createSearchEngine(options) {
|
|
786
786
|
const customScorer = options?.scorer;
|
|
@@ -789,7 +789,7 @@ function createSearchEngine(options) {
|
|
|
789
789
|
let previousQuery = "";
|
|
790
790
|
let previousResults = /* @__PURE__ */ new Set();
|
|
791
791
|
const sentinel = {};
|
|
792
|
-
if (
|
|
792
|
+
if (leakRegistry) leakRegistry.register(sentinel, "SearchEngine", sentinel);
|
|
793
793
|
return {
|
|
794
794
|
index(items) {
|
|
795
795
|
for (const item of items) indexedEntries.set(item.id, {
|
|
@@ -839,12 +839,11 @@ function createSearchEngine(options) {
|
|
|
839
839
|
[Symbol.dispose]() {
|
|
840
840
|
indexedEntries.clear();
|
|
841
841
|
previousResults.clear();
|
|
842
|
-
if (
|
|
842
|
+
if (leakRegistry) leakRegistry.unregister(sentinel);
|
|
843
843
|
}
|
|
844
844
|
};
|
|
845
845
|
}
|
|
846
|
-
|
|
847
|
-
//#region src/core/utils/event-emitter.ts
|
|
846
|
+
// src/core/utils/event-emitter.ts
|
|
848
847
|
/**
|
|
849
848
|
* Type-safe event emitter with strong references and Disposable subscriptions.
|
|
850
849
|
* Returns Disposable from `on()` for automatic unsubscription via `using`.
|
|
@@ -887,13 +886,13 @@ var TypedEmitter = class {
|
|
|
887
886
|
this.#listeners.clear();
|
|
888
887
|
}
|
|
889
888
|
};
|
|
890
|
-
|
|
891
|
-
//#region src/core/utils/scheduler.ts
|
|
889
|
+
// src/core/utils/scheduler.ts
|
|
892
890
|
/** Whether we're in a browser environment with rAF available */
|
|
893
891
|
const hasRAF = typeof requestAnimationFrame === "function";
|
|
894
|
-
/** Feature detection for cutting-edge scheduling APIs
|
|
892
|
+
/** Feature detection for cutting-edge scheduling APIs, read through globalThis: typeof guards only a bare
|
|
893
|
+
* identifier, so `typeof navigator?.scheduling` would throw where navigator is undeclared (runtimes without one). */
|
|
895
894
|
const canYield = typeof globalThis.scheduler?.yield === "function";
|
|
896
|
-
const hasInputPending = typeof navigator?.scheduling?.isInputPending === "function";
|
|
895
|
+
const hasInputPending = typeof globalThis.navigator?.scheduling?.isInputPending === "function";
|
|
897
896
|
/**
|
|
898
897
|
* Creates a batched update scheduler. Coalesces rapid state updates into a single
|
|
899
898
|
* execution pass using rAF (browser) or queueMicrotask (Node.js).
|
|
@@ -949,8 +948,7 @@ function createScheduler() {
|
|
|
949
948
|
}
|
|
950
949
|
};
|
|
951
950
|
}
|
|
952
|
-
|
|
953
|
-
//#region src/core/machine.ts
|
|
951
|
+
// src/core/machine.ts
|
|
954
952
|
/**
|
|
955
953
|
* Creates a headless command palette state machine with search, frecency,
|
|
956
954
|
* and keyboard shortcut support. Implements Disposable for `using` cleanup.
|
|
@@ -1216,9 +1214,8 @@ function createCommandMachine(options = {}) {
|
|
|
1216
1214
|
}
|
|
1217
1215
|
};
|
|
1218
1216
|
}
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
/** Core hook that binds a CommandMachine to React via useSyncExternalStore, useTransition, and useOptimistic. */
|
|
1217
|
+
// src/react/hooks/use-command.ts
|
|
1218
|
+
/** Core hook that binds a CommandMachine to React via useSyncExternalStore, useOptimistic, and a transition for optimistic active-item updates. */
|
|
1222
1219
|
function useCommand(machine) {
|
|
1223
1220
|
const id = useId();
|
|
1224
1221
|
const [isPending, startTransition] = useTransition();
|
|
@@ -1231,11 +1228,9 @@ function useCommand(machine) {
|
|
|
1231
1228
|
});
|
|
1232
1229
|
}, [rawSetOptimisticActiveId]);
|
|
1233
1230
|
const updateSearch = useCallback((query) => {
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
query
|
|
1238
|
-
});
|
|
1231
|
+
machine.send({
|
|
1232
|
+
type: "SEARCH_CHANGE",
|
|
1233
|
+
query
|
|
1239
1234
|
});
|
|
1240
1235
|
}, [machine]);
|
|
1241
1236
|
return {
|
|
@@ -1250,8 +1245,7 @@ function useCommand(machine) {
|
|
|
1250
1245
|
id
|
|
1251
1246
|
};
|
|
1252
1247
|
}
|
|
1253
|
-
|
|
1254
|
-
//#region src/react/hooks/use-keyboard.ts
|
|
1248
|
+
// src/react/hooks/use-keyboard.ts
|
|
1255
1249
|
/** Navigation key to machine direction mapping */
|
|
1256
1250
|
const NAVIGATION_KEYS = {
|
|
1257
1251
|
ArrowDown: "next",
|
|
@@ -1295,8 +1289,7 @@ function createKeydownHandler(machine, getState) {
|
|
|
1295
1289
|
}
|
|
1296
1290
|
};
|
|
1297
1291
|
}
|
|
1298
|
-
|
|
1299
|
-
//#region src/react/hooks/use-command-setup.ts
|
|
1292
|
+
// src/react/hooks/use-command-setup.ts
|
|
1300
1293
|
/**
|
|
1301
1294
|
* Shared setup for Command and Command.Dialog — creates machine once,
|
|
1302
1295
|
* subscribes via useSyncExternalStore, memoizes context values, and
|
|
@@ -1356,8 +1349,7 @@ function useCommandSetup(machineFactory, label) {
|
|
|
1356
1349
|
])
|
|
1357
1350
|
};
|
|
1358
1351
|
}
|
|
1359
|
-
|
|
1360
|
-
//#region src/react/dialog.tsx
|
|
1352
|
+
// src/react/dialog.tsx
|
|
1361
1353
|
/** Dialog variant that wraps the command palette in a Radix UI Dialog with focus trap and overlay. */
|
|
1362
1354
|
function CommandDialog({ children, className, overlayClassName, contentClassName, label = "Command palette", description = "Type a command or search...", open: controlledOpen, onOpenChange, container, ...machineOptions }) {
|
|
1363
1355
|
const onOpenChangeRef = useRef(onOpenChange);
|
|
@@ -1431,8 +1423,7 @@ function CommandDialog({ children, className, overlayClassName, contentClassName
|
|
|
1431
1423
|
})
|
|
1432
1424
|
});
|
|
1433
1425
|
}
|
|
1434
|
-
|
|
1435
|
-
//#region src/react/empty.tsx
|
|
1426
|
+
// src/react/empty.tsx
|
|
1436
1427
|
/** Rendered when no items match the current search query. */
|
|
1437
1428
|
function CommandEmpty({ ref, children, ...props }) {
|
|
1438
1429
|
const stateCtx = useStateContext("Command.Empty");
|
|
@@ -1449,8 +1440,7 @@ function CommandEmpty({ ref, children, ...props }) {
|
|
|
1449
1440
|
if (statusContainerRef?.current) return createPortal(content, statusContainerRef.current);
|
|
1450
1441
|
return content;
|
|
1451
1442
|
}
|
|
1452
|
-
|
|
1453
|
-
//#region src/react/hooks/use-register.ts
|
|
1443
|
+
// src/react/hooks/use-register.ts
|
|
1454
1444
|
/** Register a command item with the machine — auto-deregisters on unmount */
|
|
1455
1445
|
function useRegisterItem(value, options) {
|
|
1456
1446
|
const stable = useStableContext("useRegisterItem");
|
|
@@ -1522,8 +1512,7 @@ function useRegisterGroup(heading, options) {
|
|
|
1522
1512
|
]);
|
|
1523
1513
|
return id;
|
|
1524
1514
|
}
|
|
1525
|
-
|
|
1526
|
-
//#region src/react/group.tsx
|
|
1515
|
+
// src/react/group.tsx
|
|
1527
1516
|
/** Module-level constant — avoids allocating a new object on every render of hidden groups */
|
|
1528
1517
|
const HIDDEN_STYLE = { display: "none" };
|
|
1529
1518
|
/** Visual grouping container with an optional heading that hides when no items match. */
|
|
@@ -1567,8 +1556,7 @@ function CommandGroup({ ref, children, heading, priority, forceId, ...props }) {
|
|
|
1567
1556
|
})
|
|
1568
1557
|
});
|
|
1569
1558
|
}
|
|
1570
|
-
|
|
1571
|
-
//#region src/react/highlight.tsx
|
|
1559
|
+
// src/react/highlight.tsx
|
|
1572
1560
|
/** Renders search match highlighting by splitting text at scored match ranges. */
|
|
1573
1561
|
function CommandHighlight({ text, ranges, highlightClassName }) {
|
|
1574
1562
|
return useMemo(() => {
|
|
@@ -1593,14 +1581,27 @@ function CommandHighlight({ text, ranges, highlightClassName }) {
|
|
|
1593
1581
|
highlightClassName
|
|
1594
1582
|
]);
|
|
1595
1583
|
}
|
|
1596
|
-
|
|
1597
|
-
//#region src/react/input.tsx
|
|
1584
|
+
// src/react/input.tsx
|
|
1598
1585
|
/** Combobox input that drives the command palette search state. */
|
|
1599
1586
|
function CommandInput({ ref, placeholder = "Search...", onValueChange, ...props }) {
|
|
1600
1587
|
const stable = useStableContext("Command.Input");
|
|
1601
1588
|
const stateCtx = useStateContext("Command.Input");
|
|
1589
|
+
const machineState = useSyncExternalStore(stable.machine.subscribe, stable.machine.getState, stable.machine.getState);
|
|
1590
|
+
const [draft, setDraft] = useState(machineState.search);
|
|
1591
|
+
const inFlight = useRef([]);
|
|
1592
|
+
useLayoutEffect(() => {
|
|
1593
|
+
const { search } = machineState;
|
|
1594
|
+
const echoed = inFlight.current.lastIndexOf(search);
|
|
1595
|
+
if (echoed !== -1) inFlight.current.splice(0, echoed + 1);
|
|
1596
|
+
else {
|
|
1597
|
+
inFlight.current = [];
|
|
1598
|
+
setDraft(search);
|
|
1599
|
+
}
|
|
1600
|
+
}, [machineState]);
|
|
1602
1601
|
const handleChange = useCallback((event) => {
|
|
1603
1602
|
const { value } = event.target;
|
|
1603
|
+
inFlight.current.push(value);
|
|
1604
|
+
setDraft(value);
|
|
1604
1605
|
stable.updateSearch(value);
|
|
1605
1606
|
onValueChange?.(value);
|
|
1606
1607
|
}, [stable.updateSearch, onValueChange]);
|
|
@@ -1619,13 +1620,12 @@ function CommandInput({ ref, placeholder = "Search...", onValueChange, ...props
|
|
|
1619
1620
|
autoCorrect: "off",
|
|
1620
1621
|
spellCheck: false,
|
|
1621
1622
|
placeholder,
|
|
1622
|
-
value:
|
|
1623
|
+
value: draft,
|
|
1623
1624
|
onChange: handleChange,
|
|
1624
1625
|
...props
|
|
1625
1626
|
});
|
|
1626
1627
|
}
|
|
1627
|
-
|
|
1628
|
-
//#region src/react/item.tsx
|
|
1628
|
+
// src/react/item.tsx
|
|
1629
1629
|
/** Individual selectable item in the command list with keyboard and pointer activation. */
|
|
1630
1630
|
function CommandItem({ ref, children, value, keywords, onSelect, disabled = false, shortcut, forceId, ...props }) {
|
|
1631
1631
|
const stable = useStableContext("Command.Item");
|
|
@@ -1695,8 +1695,7 @@ function CommandItem({ ref, children, value, keywords, onSelect, disabled = fals
|
|
|
1695
1695
|
children
|
|
1696
1696
|
});
|
|
1697
1697
|
}
|
|
1698
|
-
|
|
1699
|
-
//#region src/react/hooks/use-virtualizer.ts
|
|
1698
|
+
// src/react/hooks/use-virtualizer.ts
|
|
1700
1699
|
/** Lightweight virtualizer for large item lists using ResizeObserver and scroll tracking. */
|
|
1701
1700
|
function useVirtualizer(options) {
|
|
1702
1701
|
const { count, estimateSize, overscan = 8, scrollElement, enabled = true } = options;
|
|
@@ -1800,8 +1799,7 @@ function useVirtualizer(options) {
|
|
|
1800
1799
|
}, [enabled])
|
|
1801
1800
|
};
|
|
1802
1801
|
}
|
|
1803
|
-
|
|
1804
|
-
//#region src/react/list.tsx
|
|
1802
|
+
// src/react/list.tsx
|
|
1805
1803
|
/** Scrollable list container with auto-virtualization and aria-live announcements. */
|
|
1806
1804
|
function CommandList({ ref, children, virtualize, estimateSize = 44, overscan = 8, announceResults, style, ...props }) {
|
|
1807
1805
|
const stable = useStableContext("Command.List");
|
|
@@ -1941,8 +1939,7 @@ function CommandList({ ref, children, virtualize, estimateSize = 44, overscan =
|
|
|
1941
1939
|
]
|
|
1942
1940
|
});
|
|
1943
1941
|
}
|
|
1944
|
-
|
|
1945
|
-
//#region src/react/loading.tsx
|
|
1942
|
+
// src/react/loading.tsx
|
|
1946
1943
|
/** Shown during async operations, with configurable loading state override. */
|
|
1947
1944
|
function CommandLoading({ ref, children, loading: loadingOverride, ...props }) {
|
|
1948
1945
|
const stateCtx = useStateContext("Command.Loading");
|
|
@@ -1957,8 +1954,7 @@ function CommandLoading({ ref, children, loading: loadingOverride, ...props }) {
|
|
|
1957
1954
|
children
|
|
1958
1955
|
});
|
|
1959
1956
|
}
|
|
1960
|
-
|
|
1961
|
-
//#region src/react/page.tsx
|
|
1957
|
+
// src/react/page.tsx
|
|
1962
1958
|
/** Page navigation container that only renders when its ID matches the active page. */
|
|
1963
1959
|
function CommandPage({ id, children }) {
|
|
1964
1960
|
if (useStateContext("Command.Page").state.page !== id) return null;
|
|
@@ -1968,8 +1964,7 @@ function CommandPage({ id, children }) {
|
|
|
1968
1964
|
children
|
|
1969
1965
|
});
|
|
1970
1966
|
}
|
|
1971
|
-
|
|
1972
|
-
//#region src/react/separator.tsx
|
|
1967
|
+
// src/react/separator.tsx
|
|
1973
1968
|
/** Visual divider between items or groups, rendered as a presentational element. */
|
|
1974
1969
|
function CommandSeparator({ ref, ...props }) {
|
|
1975
1970
|
return /* @__PURE__ */ jsx("div", {
|
|
@@ -1979,8 +1974,7 @@ function CommandSeparator({ ref, ...props }) {
|
|
|
1979
1974
|
...props
|
|
1980
1975
|
});
|
|
1981
1976
|
}
|
|
1982
|
-
|
|
1983
|
-
//#region src/react/shortcut.tsx
|
|
1977
|
+
// src/react/shortcut.tsx
|
|
1984
1978
|
/** Keyboard shortcut display with platform-aware formatting (e.g., Cmd vs Ctrl). */
|
|
1985
1979
|
function CommandShortcut({ ref, shortcut, children, ...props }) {
|
|
1986
1980
|
const formattedShortcut = useMemo(() => shortcut ? formatShortcut(parseShortcut(shortcut)) : null, [shortcut]);
|
|
@@ -1991,8 +1985,7 @@ function CommandShortcut({ ref, shortcut, children, ...props }) {
|
|
|
1991
1985
|
children: formattedShortcut ?? children
|
|
1992
1986
|
});
|
|
1993
1987
|
}
|
|
1994
|
-
|
|
1995
|
-
//#region src/react/command.tsx
|
|
1988
|
+
// src/react/command.tsx
|
|
1996
1989
|
/** Root compound component that provides the command state machine context to all children. */
|
|
1997
1990
|
function CommandRoot({ children, className, label = "Command palette", ...machineOptions }) {
|
|
1998
1991
|
const rootRef = useRef(null);
|
|
@@ -2041,8 +2034,7 @@ const Command = Object.assign(CommandRoot, {
|
|
|
2041
2034
|
AsyncItems: CommandAsyncItems,
|
|
2042
2035
|
Activity: CommandActivity
|
|
2043
2036
|
});
|
|
2044
|
-
|
|
2045
|
-
//#region src/react/error-boundary.tsx
|
|
2037
|
+
// src/react/error-boundary.tsx
|
|
2046
2038
|
/** Initial state — no error captured */
|
|
2047
2039
|
const INITIAL_STATE = { error: null };
|
|
2048
2040
|
/** Error boundary for graceful failure recovery in command subtrees. */
|
|
@@ -2064,8 +2056,7 @@ var CommandErrorBoundary = class extends Component {
|
|
|
2064
2056
|
return this.props.children;
|
|
2065
2057
|
}
|
|
2066
2058
|
};
|
|
2067
|
-
|
|
2068
|
-
//#region src/react/hooks/use-command-state.ts
|
|
2059
|
+
// src/react/hooks/use-command-state.ts
|
|
2069
2060
|
function useCommandState(selector) {
|
|
2070
2061
|
const stable = useStableContext("useCommandState");
|
|
2071
2062
|
const selectorRef = useRef(selector);
|
|
@@ -2073,19 +2064,20 @@ function useCommandState(selector) {
|
|
|
2073
2064
|
const getSnapshot = useCallback(() => selectorRef.current ? selectorRef.current(stable.machine.getState()) : stable.machine.getState(), [stable.machine]);
|
|
2074
2065
|
return useSyncExternalStore(stable.machine.subscribe, getSnapshot, getSnapshot);
|
|
2075
2066
|
}
|
|
2076
|
-
|
|
2077
|
-
//#region src/react/hooks/use-devtools.ts
|
|
2067
|
+
// src/react/hooks/use-devtools.ts
|
|
2078
2068
|
/** Branded key for the global devtools instance map */
|
|
2079
2069
|
const DEVTOOLS_KEY = "__CRIMSON_COMMAND_DEVTOOLS__";
|
|
2080
2070
|
/**
|
|
2081
2071
|
* Exposes command palette internals to browser devtools.
|
|
2082
|
-
*
|
|
2072
|
+
* Development only: the effect is gated on `process.env.NODE_ENV` and returns early in
|
|
2073
|
+
* production builds (the hook itself still ships). Call it from a component rendered inside
|
|
2074
|
+
* `<Command>`: outside one there is no machine to inspect and the hook does nothing.
|
|
2083
2075
|
*
|
|
2084
2076
|
* @example
|
|
2085
2077
|
* ```tsx
|
|
2086
|
-
* function
|
|
2078
|
+
* function DevtoolsBridge() {
|
|
2087
2079
|
* useCommandDevtools('main');
|
|
2088
|
-
* return
|
|
2080
|
+
* return null;
|
|
2089
2081
|
* }
|
|
2090
2082
|
* ```
|
|
2091
2083
|
*/
|
|
@@ -2094,7 +2086,7 @@ function useCommandDevtools(label = "default") {
|
|
|
2094
2086
|
const stateCtx = use(CommandStateContext);
|
|
2095
2087
|
const stateRef = useRef(null);
|
|
2096
2088
|
useEffect(() => {
|
|
2097
|
-
if (!
|
|
2089
|
+
if (!isDev) return;
|
|
2098
2090
|
if (!stable || !stateCtx) return;
|
|
2099
2091
|
const update = () => {
|
|
2100
2092
|
const s = stateCtx.state;
|
|
@@ -2129,5 +2121,4 @@ function useCommandDevtools(label = "default") {
|
|
|
2129
2121
|
label
|
|
2130
2122
|
]);
|
|
2131
2123
|
}
|
|
2132
|
-
//#endregion
|
|
2133
2124
|
export { Command, CommandActivity, CommandAsyncItems, CommandBadge, CommandDialog, CommandEmpty, CommandErrorBoundary, CommandGroup, CommandHighlight, CommandInput, CommandItem, CommandList, CommandLoading, CommandPage, CommandSeparator, CommandShortcut, CommandStableContext, CommandStateContext, createKeydownHandler, useCommand, useCommandDevtools, useCommandState, useRegisterGroup, useRegisterItem, useVirtualizer };
|
package/dist/react/styles.css
CHANGED
|
@@ -6,6 +6,12 @@
|
|
|
6
6
|
/* CSS Logical Properties: full RTL/LTR support */
|
|
7
7
|
/* CSS Nesting, @layer cascade control, oklch() color space */
|
|
8
8
|
/* Container queries for responsive item layout */
|
|
9
|
+
/* Secondary text (group headings, shortcuts, empty state, placeholder) is coloured with
|
|
10
|
+
--command-muted-color, never faded with opacity: opacity stacks on the consumer's text
|
|
11
|
+
colour and cannot be checked against it. The default is light-dark(), so it follows the
|
|
12
|
+
used color-scheme of the page (set color-scheme on :root or a parent, or override the
|
|
13
|
+
token); it is 4.5:1 or better on white, black and the usual near-white / near-black
|
|
14
|
+
surfaces. Only [data-disabled] items keep opacity (WCAG 1.4.3 exempts disabled controls). */
|
|
9
15
|
|
|
10
16
|
/* ─── CSS Custom Properties ─── */
|
|
11
17
|
|
|
@@ -40,7 +46,7 @@
|
|
|
40
46
|
}
|
|
41
47
|
|
|
42
48
|
/* ─── User-Customizable Animation Properties ─── */
|
|
43
|
-
/* Override these on [data-command-root] or :root
|
|
49
|
+
/* Override these with unlayered CSS on [data-command-root] loaded after this file, or on a more specific unlayered selector; this block sits outside every @layer, so a normal (non-!important) value set inside a layer loses here whatever its selector, and a :root value is shadowed by this declaration */
|
|
44
50
|
|
|
45
51
|
[data-command-root] {
|
|
46
52
|
--command-spring-easing: linear(
|
|
@@ -99,10 +105,11 @@
|
|
|
99
105
|
--command-item-height: 44px;
|
|
100
106
|
--command-fade-edge-size: 16px;
|
|
101
107
|
--command-highlight-color: oklch(0.85 0.15 90 / 0.3);
|
|
108
|
+
--command-muted-color: light-dark(oklch(0.45 0.015 260), oklch(0.75 0.015 260));
|
|
102
109
|
}
|
|
103
110
|
|
|
104
111
|
/* ─── Spring Easing ─── */
|
|
105
|
-
/* linear()
|
|
112
|
+
/* linear() spring approximations: --command-spring-easing settles without overshoot; --command-spring-easing-bouncy peaks at 1.033 before settling */
|
|
106
113
|
/* Generated from spring physics: x(t) = 1 - e^(-zt)(cos(wd*t) + (zw/wd)sin(wd*t)) */
|
|
107
114
|
|
|
108
115
|
@layer command.base, command.layout, command.animation, command.a11y;
|
|
@@ -173,7 +180,9 @@
|
|
|
173
180
|
font: inherit;
|
|
174
181
|
|
|
175
182
|
&::placeholder {
|
|
176
|
-
|
|
183
|
+
color: var(--command-muted-color);
|
|
184
|
+
/* Firefox fades ::placeholder by default; the colour token carries the contrast */
|
|
185
|
+
opacity: 1;
|
|
177
186
|
}
|
|
178
187
|
}
|
|
179
188
|
|
|
@@ -232,7 +241,7 @@
|
|
|
232
241
|
font-size: 0.75em;
|
|
233
242
|
text-transform: uppercase;
|
|
234
243
|
letter-spacing: 0.05em;
|
|
235
|
-
|
|
244
|
+
color: var(--command-muted-color);
|
|
236
245
|
}
|
|
237
246
|
|
|
238
247
|
/* ─── Separator ─── */
|
|
@@ -255,7 +264,7 @@
|
|
|
255
264
|
|
|
256
265
|
[data-command-empty] {
|
|
257
266
|
text-align: center;
|
|
258
|
-
|
|
267
|
+
color: var(--command-muted-color);
|
|
259
268
|
}
|
|
260
269
|
|
|
261
270
|
/* ─── Shortcut Badge ─── */
|
|
@@ -263,7 +272,7 @@
|
|
|
263
272
|
[data-command-shortcut] {
|
|
264
273
|
font-family: inherit;
|
|
265
274
|
font-size: 0.75em;
|
|
266
|
-
|
|
275
|
+
color: var(--command-muted-color);
|
|
267
276
|
letter-spacing: 0.05em;
|
|
268
277
|
}
|
|
269
278
|
|
|
@@ -543,7 +552,7 @@
|
|
|
543
552
|
}
|
|
544
553
|
|
|
545
554
|
[data-command-group-heading] {
|
|
546
|
-
|
|
555
|
+
color: inherit;
|
|
547
556
|
}
|
|
548
557
|
|
|
549
558
|
[data-command-highlight] {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "modern-cmdk",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "The definitive command palette engine for the modern web",
|
|
6
6
|
"license": "MIT",
|
|
@@ -62,7 +62,7 @@
|
|
|
62
62
|
"devDependencies": {
|
|
63
63
|
"globby": "16.2.4",
|
|
64
64
|
"jscodeshift": "17.4.0",
|
|
65
|
-
"radix-ui": "1.
|
|
65
|
+
"radix-ui": "1.8.2-rc.1791984254107",
|
|
66
66
|
"@types/jscodeshift": "17.3.0"
|
|
67
67
|
},
|
|
68
68
|
"keywords": [
|
|
@@ -80,8 +80,8 @@
|
|
|
80
80
|
"directory": "packages/modern-cmdk"
|
|
81
81
|
},
|
|
82
82
|
"scripts": {
|
|
83
|
-
"build": "tsdown
|
|
84
|
-
"dev": "tsdown
|
|
83
|
+
"build": "tsdown",
|
|
84
|
+
"dev": "tsdown --watch",
|
|
85
85
|
"typecheck": "tsc --noEmit"
|
|
86
86
|
}
|
|
87
87
|
}
|