modern-cmdk 1.2.3 → 1.4.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/LICENSE +1 -1
- package/README.md +109 -154
- package/dist/codemod/cli.mjs +205 -120
- package/dist/codemod/transforms/data-attrs.mjs +1 -1
- package/dist/core/index.d.mts +10 -10
- package/dist/core/index.mjs +9 -8
- package/dist/react/chunk-dialog.mjs +237 -0
- package/dist/react/dialog.d.mts +18 -0
- package/dist/react/dialog.mjs +3 -0
- package/dist/react/index.d.mts +11 -333
- package/dist/react/index.mjs +34 -1373
- package/dist/react/styles.css +18 -9
- package/package.json +8 -12
- package/dist/codemod/cli.d.mts +0 -1
- package/dist/codemod/transforms/data-attrs.d.mts +0 -6
- package/dist/codemod/transforms/forward-ref.d.mts +0 -6
- package/dist/codemod/transforms/import-rewrite.d.mts +0 -6
- package/dist/codemod/transforms/should-filter.d.mts +0 -5
package/dist/core/index.mjs
CHANGED
|
@@ -44,7 +44,7 @@ function storageKey(namespace) {
|
|
|
44
44
|
}
|
|
45
45
|
let idbModule;
|
|
46
46
|
/**
|
|
47
|
-
* IndexedDB-backed frecency storage using idb-keyval
|
|
47
|
+
* IndexedDB-backed frecency storage using idb-keyval.
|
|
48
48
|
*
|
|
49
49
|
* Persists frecency data across browser sessions. Implements both
|
|
50
50
|
* `Disposable` and `AsyncDisposable` for use with `using` and `await using`.
|
|
@@ -170,7 +170,7 @@ var MemoryFrecencyStorage = class {
|
|
|
170
170
|
save(namespace, data) {
|
|
171
171
|
this.#store.set(namespace, data);
|
|
172
172
|
}
|
|
173
|
-
/** Serialize all stored data — Iterator Helpers pipeline (
|
|
173
|
+
/** Serialize all stored data — Iterator Helpers pipeline (ES2025) */
|
|
174
174
|
toJSON() {
|
|
175
175
|
return Object.fromEntries(this.#store.entries().map(([namespace, data]) => [namespace, { records: data.records.entries().toArray() }]));
|
|
176
176
|
}
|
|
@@ -181,7 +181,7 @@ var MemoryFrecencyStorage = class {
|
|
|
181
181
|
// src/core/frecency/index.ts
|
|
182
182
|
/** Shared empty bonus map — avoids allocation when no frecency records exist */
|
|
183
183
|
const EMPTY_BONUS_MAP = /* @__PURE__ */ new Map();
|
|
184
|
-
/**
|
|
184
|
+
/** Bucket boundaries as hour constants — no per-call unit conversion */
|
|
185
185
|
const BUCKET_HOURS = {
|
|
186
186
|
hour: 1,
|
|
187
187
|
day: 24,
|
|
@@ -191,7 +191,7 @@ const BUCKET_HOURS = {
|
|
|
191
191
|
/**
|
|
192
192
|
* Computes a frecency bonus for an item based on usage frequency and recency.
|
|
193
193
|
* Uses hour/day/week/month bucket boundaries with
|
|
194
|
-
* configurable
|
|
194
|
+
* configurable step weights per recency bucket (hour/day/week/month/older).
|
|
195
195
|
*/
|
|
196
196
|
function computeFrecencyBonus(history, now = Date.now(), config = DEFAULT_FRECENCY_DECAY) {
|
|
197
197
|
const hours = (now - history.lastUsed) / 36e5;
|
|
@@ -397,7 +397,7 @@ function formatShortcut(parsed) {
|
|
|
397
397
|
/**
|
|
398
398
|
* Manages keyboard shortcut bindings with global keydown listening.
|
|
399
399
|
* Supports `using` for automatic cleanup of both individual bindings and the
|
|
400
|
-
* global document listener. Detects conflicts
|
|
400
|
+
* global document listener. Detects conflicts with the local mapGroupBy helper (core/utils/group-by.ts).
|
|
401
401
|
*/
|
|
402
402
|
var KeyboardShortcutRegistry = class {
|
|
403
403
|
#bindings = /* @__PURE__ */ new Map();
|
|
@@ -855,7 +855,7 @@ if (isDev && typeof window !== "undefined") leakRegistry = new FinalizationRegis
|
|
|
855
855
|
* Creates a search engine with pluggable scorer and incremental filtering.
|
|
856
856
|
* When the query extends the previous one, only prior matches are re-scored.
|
|
857
857
|
* Pre-caches lowercase at index time — zero toLowerCase() calls during search.
|
|
858
|
-
* Uses Iterator Helpers (
|
|
858
|
+
* Uses Iterator Helpers (ES2025) for result pipelines.
|
|
859
859
|
*/
|
|
860
860
|
function createSearchEngine(options) {
|
|
861
861
|
const customScorer = options?.scorer;
|
|
@@ -964,9 +964,10 @@ var TypedEmitter = class {
|
|
|
964
964
|
// src/core/utils/scheduler.ts
|
|
965
965
|
/** Whether we're in a browser environment with rAF available */
|
|
966
966
|
const hasRAF = typeof requestAnimationFrame === "function";
|
|
967
|
-
/** Feature detection for cutting-edge scheduling APIs
|
|
967
|
+
/** Feature detection for cutting-edge scheduling APIs, read through globalThis: typeof guards only a bare
|
|
968
|
+
* identifier, so `typeof navigator?.scheduling` would throw where navigator is undeclared (runtimes without one). */
|
|
968
969
|
const canYield = typeof globalThis.scheduler?.yield === "function";
|
|
969
|
-
const hasInputPending = typeof navigator?.scheduling?.isInputPending === "function";
|
|
970
|
+
const hasInputPending = typeof globalThis.navigator?.scheduling?.isInputPending === "function";
|
|
970
971
|
/**
|
|
971
972
|
* Creates a batched update scheduler. Coalesces rapid state updates into a single
|
|
972
973
|
* execution pass using rAF (browser) or queueMicrotask (Node.js).
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { createContext, use, useCallback, useEffect, useId, useMemo, useOptimistic, useRef, useSyncExternalStore, useTransition } from "react";
|
|
3
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
4
|
+
import { createCommandMachine } from "modern-cmdk";
|
|
5
|
+
import { Dialog } from "radix-ui";
|
|
6
|
+
// src/react/context.ts
|
|
7
|
+
const CommandStableContext = createContext(null);
|
|
8
|
+
CommandStableContext.displayName = "CommandStableContext";
|
|
9
|
+
const CommandStateContext = createContext(null);
|
|
10
|
+
CommandStateContext.displayName = "CommandStateContext";
|
|
11
|
+
/** Consume stable context with a descriptive error — replaces 3-line boilerplate in every component */
|
|
12
|
+
function useStableContext(component) {
|
|
13
|
+
const ctx = use(CommandStableContext);
|
|
14
|
+
if (!ctx) throw new Error(`${component} must be used within a <Command> component`);
|
|
15
|
+
return ctx;
|
|
16
|
+
}
|
|
17
|
+
/** Consume state context with a descriptive error — replaces 3-line boilerplate in every component */
|
|
18
|
+
function useStateContext(component) {
|
|
19
|
+
const ctx = use(CommandStateContext);
|
|
20
|
+
if (!ctx) throw new Error(`${component} must be used within a <Command> component`);
|
|
21
|
+
return ctx;
|
|
22
|
+
}
|
|
23
|
+
const CommandGroupContext = createContext(null);
|
|
24
|
+
CommandGroupContext.displayName = "CommandGroupContext";
|
|
25
|
+
/** Context for the status container rendered outside the listbox — used by Command.Empty */
|
|
26
|
+
const CommandListStatusContext = createContext(null);
|
|
27
|
+
CommandListStatusContext.displayName = "CommandListStatusContext";
|
|
28
|
+
// src/react/hooks/use-command.ts
|
|
29
|
+
/** Core hook that binds a CommandMachine to React via useSyncExternalStore, useOptimistic, and a transition for optimistic active-item updates. */
|
|
30
|
+
function useCommand(machine) {
|
|
31
|
+
const id = useId();
|
|
32
|
+
const [isPending, startTransition] = useTransition();
|
|
33
|
+
const state = useSyncExternalStore(machine.subscribe, machine.getState, machine.getState);
|
|
34
|
+
const filteredIdSet = useMemo(() => machine.getFilteredIdSet(), [state.filteredIds, machine]);
|
|
35
|
+
const [optimisticActiveId, rawSetOptimisticActiveId] = useOptimistic(state.activeId);
|
|
36
|
+
const setOptimisticActiveId = useCallback((id) => {
|
|
37
|
+
startTransition(() => {
|
|
38
|
+
rawSetOptimisticActiveId(id);
|
|
39
|
+
});
|
|
40
|
+
}, [rawSetOptimisticActiveId]);
|
|
41
|
+
const updateSearch = useCallback((query) => {
|
|
42
|
+
machine.send({
|
|
43
|
+
type: "SEARCH_CHANGE",
|
|
44
|
+
query
|
|
45
|
+
});
|
|
46
|
+
}, [machine]);
|
|
47
|
+
return {
|
|
48
|
+
state: optimisticActiveId !== state.activeId ? {
|
|
49
|
+
...state,
|
|
50
|
+
activeId: optimisticActiveId
|
|
51
|
+
} : state,
|
|
52
|
+
isPending,
|
|
53
|
+
updateSearch,
|
|
54
|
+
setOptimisticActiveId,
|
|
55
|
+
filteredIdSet,
|
|
56
|
+
id
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
// src/react/hooks/use-keyboard.ts
|
|
60
|
+
/** Navigation key to machine direction mapping */
|
|
61
|
+
const NAVIGATION_KEYS = {
|
|
62
|
+
ArrowDown: "next",
|
|
63
|
+
ArrowUp: "prev",
|
|
64
|
+
Home: "first",
|
|
65
|
+
End: "last"
|
|
66
|
+
};
|
|
67
|
+
/** Create a keydown handler for command palette keyboard navigation */
|
|
68
|
+
function createKeydownHandler(machine, getState) {
|
|
69
|
+
return (event) => {
|
|
70
|
+
const { key } = event;
|
|
71
|
+
const state = getState();
|
|
72
|
+
const direction = NAVIGATION_KEYS[key];
|
|
73
|
+
if (direction) {
|
|
74
|
+
event.preventDefault();
|
|
75
|
+
machine.send({
|
|
76
|
+
type: "NAVIGATE",
|
|
77
|
+
direction
|
|
78
|
+
});
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
switch (key) {
|
|
82
|
+
case "Enter": {
|
|
83
|
+
event.preventDefault();
|
|
84
|
+
const { activeId } = state;
|
|
85
|
+
if (activeId) machine.send({
|
|
86
|
+
type: "ITEM_SELECT",
|
|
87
|
+
id: activeId
|
|
88
|
+
});
|
|
89
|
+
break;
|
|
90
|
+
}
|
|
91
|
+
case "Backspace":
|
|
92
|
+
if (state.search === "" && state.pageStack.length > 0) {
|
|
93
|
+
event.preventDefault();
|
|
94
|
+
machine.send({ type: "PAGE_POP" });
|
|
95
|
+
}
|
|
96
|
+
break;
|
|
97
|
+
case "Escape":
|
|
98
|
+
event.preventDefault();
|
|
99
|
+
machine.send({ type: "CLOSE" });
|
|
100
|
+
}
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
// src/react/hooks/use-command-setup.ts
|
|
104
|
+
/**
|
|
105
|
+
* Shared setup for Command and Command.Dialog — creates machine once,
|
|
106
|
+
* subscribes via useSyncExternalStore, memoizes context values, and
|
|
107
|
+
* registers disposal on unmount.
|
|
108
|
+
*
|
|
109
|
+
* @param machineFactory - Called exactly once (first render) to create the machine.
|
|
110
|
+
* Subsequent renders skip it via ref guard.
|
|
111
|
+
* @param label - Accessible label for the command palette (included in stable context).
|
|
112
|
+
*/
|
|
113
|
+
function useCommandSetup(machineFactory, label) {
|
|
114
|
+
const machineRef = useRef(null);
|
|
115
|
+
if (machineRef.current === null) machineRef.current = machineFactory();
|
|
116
|
+
const machine = machineRef.current;
|
|
117
|
+
const { state, isPending, updateSearch, setOptimisticActiveId, filteredIdSet, id } = useCommand(machine);
|
|
118
|
+
const listId = `${id}-list`;
|
|
119
|
+
const inputId = `${id}-input`;
|
|
120
|
+
useEffect(() => {
|
|
121
|
+
return () => {
|
|
122
|
+
machineRef.current?.[Symbol.dispose]();
|
|
123
|
+
machineRef.current = null;
|
|
124
|
+
};
|
|
125
|
+
}, []);
|
|
126
|
+
return {
|
|
127
|
+
machine,
|
|
128
|
+
state,
|
|
129
|
+
isPending,
|
|
130
|
+
listId,
|
|
131
|
+
inputId,
|
|
132
|
+
handleKeyDown: useMemo(() => createKeydownHandler(machine, () => machine.getState()), [machine]),
|
|
133
|
+
stableContextValue: useMemo(() => ({
|
|
134
|
+
machine,
|
|
135
|
+
rootId: id,
|
|
136
|
+
listId,
|
|
137
|
+
inputId,
|
|
138
|
+
label,
|
|
139
|
+
updateSearch,
|
|
140
|
+
setOptimisticActiveId
|
|
141
|
+
}), [
|
|
142
|
+
machine,
|
|
143
|
+
id,
|
|
144
|
+
listId,
|
|
145
|
+
inputId,
|
|
146
|
+
label,
|
|
147
|
+
updateSearch,
|
|
148
|
+
setOptimisticActiveId
|
|
149
|
+
]),
|
|
150
|
+
stateContextValue: useMemo(() => ({
|
|
151
|
+
state,
|
|
152
|
+
isPending,
|
|
153
|
+
filteredIdSet,
|
|
154
|
+
visibleIdSet: null,
|
|
155
|
+
virtualPositionMap: null
|
|
156
|
+
}), [
|
|
157
|
+
state,
|
|
158
|
+
isPending,
|
|
159
|
+
filteredIdSet
|
|
160
|
+
])
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
// src/react/dialog.tsx
|
|
164
|
+
/** Dialog variant that wraps the command palette in a Radix UI Dialog with focus trap and overlay. */
|
|
165
|
+
function CommandDialog({ children, className, overlayClassName, contentClassName, label = "Command palette", description = "Type a command or search...", open: controlledOpen, onOpenChange, container, ...machineOptions }) {
|
|
166
|
+
const onOpenChangeRef = useRef(onOpenChange);
|
|
167
|
+
onOpenChangeRef.current = onOpenChange;
|
|
168
|
+
const { machine, state, handleKeyDown, stableContextValue, stateContextValue, inputId } = useCommandSetup(() => createCommandMachine({
|
|
169
|
+
...machineOptions,
|
|
170
|
+
open: controlledOpen ?? false,
|
|
171
|
+
onOpenChange: (open) => {
|
|
172
|
+
onOpenChangeRef.current?.(open);
|
|
173
|
+
}
|
|
174
|
+
}), label);
|
|
175
|
+
useEffect(() => {
|
|
176
|
+
if (!state.open) return;
|
|
177
|
+
document.addEventListener("keydown", handleKeyDown);
|
|
178
|
+
return () => {
|
|
179
|
+
document.removeEventListener("keydown", handleKeyDown);
|
|
180
|
+
};
|
|
181
|
+
}, [handleKeyDown, state.open]);
|
|
182
|
+
useEffect(() => {
|
|
183
|
+
if (controlledOpen !== void 0) machine.send({ type: controlledOpen ? "OPEN" : "CLOSE" });
|
|
184
|
+
}, [controlledOpen, machine]);
|
|
185
|
+
const handleOpenAutoFocus = useCallback((e) => {
|
|
186
|
+
e.preventDefault();
|
|
187
|
+
document.getElementById(inputId)?.focus();
|
|
188
|
+
}, [inputId]);
|
|
189
|
+
const handleRadixOpenChange = useCallback((open) => {
|
|
190
|
+
machine.send({ type: open ? "OPEN" : "CLOSE" });
|
|
191
|
+
}, [machine]);
|
|
192
|
+
return /* @__PURE__ */ jsx(Dialog.Root, {
|
|
193
|
+
open: state.open,
|
|
194
|
+
onOpenChange: handleRadixOpenChange,
|
|
195
|
+
children: /* @__PURE__ */ jsxs(Dialog.Portal, {
|
|
196
|
+
container,
|
|
197
|
+
children: [/* @__PURE__ */ jsx(Dialog.Overlay, {
|
|
198
|
+
"data-command-overlay": "",
|
|
199
|
+
"data-state": state.open ? "open" : "closed",
|
|
200
|
+
className: overlayClassName
|
|
201
|
+
}), /* @__PURE__ */ jsxs(Dialog.Content, {
|
|
202
|
+
"data-command-dialog": "",
|
|
203
|
+
"data-state": state.open ? "open" : "closed",
|
|
204
|
+
className,
|
|
205
|
+
"aria-label": label,
|
|
206
|
+
onOpenAutoFocus: handleOpenAutoFocus,
|
|
207
|
+
children: [
|
|
208
|
+
/* @__PURE__ */ jsx(Dialog.Title, {
|
|
209
|
+
className: "sr-only",
|
|
210
|
+
children: label
|
|
211
|
+
}),
|
|
212
|
+
/* @__PURE__ */ jsx(Dialog.Description, {
|
|
213
|
+
className: "sr-only",
|
|
214
|
+
children: description
|
|
215
|
+
}),
|
|
216
|
+
/* @__PURE__ */ jsx(CommandStableContext, {
|
|
217
|
+
value: stableContextValue,
|
|
218
|
+
children: /* @__PURE__ */ jsx(CommandStateContext, {
|
|
219
|
+
value: stateContextValue,
|
|
220
|
+
children: /* @__PURE__ */ jsx("div", {
|
|
221
|
+
"data-command-dialog-content": "",
|
|
222
|
+
className: contentClassName,
|
|
223
|
+
children: /* @__PURE__ */ jsx("div", {
|
|
224
|
+
"data-command-root": "",
|
|
225
|
+
role: "search",
|
|
226
|
+
"aria-label": label,
|
|
227
|
+
children
|
|
228
|
+
})
|
|
229
|
+
})
|
|
230
|
+
})
|
|
231
|
+
})
|
|
232
|
+
]
|
|
233
|
+
})]
|
|
234
|
+
})
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
export { CommandGroupContext as a, CommandStateContext as c, useCommand as i, useStableContext as l, useCommandSetup as n, CommandListStatusContext as o, createKeydownHandler as r, CommandStableContext as s, CommandDialog as t, useStateContext as u };
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
|
|
2
|
+
import { ReactNode } from "react";
|
|
3
|
+
import { CommandMachineOptions } from "modern-cmdk";
|
|
4
|
+
export interface CommandDialogProps extends CommandMachineOptions {
|
|
5
|
+
readonly children?: ReactNode;
|
|
6
|
+
readonly className?: string;
|
|
7
|
+
readonly overlayClassName?: string;
|
|
8
|
+
readonly contentClassName?: string;
|
|
9
|
+
readonly label?: string;
|
|
10
|
+
readonly open?: boolean;
|
|
11
|
+
readonly onOpenChange?: (open: boolean) => void;
|
|
12
|
+
/** Accessible description for screen readers (default: 'Type a command or search...') */
|
|
13
|
+
readonly description?: string;
|
|
14
|
+
/** Container element for the portal (default: document.body) */
|
|
15
|
+
readonly container?: HTMLElement | null;
|
|
16
|
+
}
|
|
17
|
+
/** Dialog variant that wraps the command palette in a Radix UI Dialog with focus trap and overlay. */
|
|
18
|
+
export declare function CommandDialog({ children, className, overlayClassName, contentClassName, label, description, open: controlledOpen, onOpenChange, container, ...machineOptions }: CommandDialogProps): ReactNode;
|