modern-cmdk 1.3.0 → 1.5.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 +76 -172
- package/THIRD-PARTY-NOTICES.md +303 -0
- package/dist/codemod/bin.mjs +14 -0
- package/dist/codemod/chunk-jscodeshift.mjs +36210 -0
- package/dist/codemod/chunk-print-options.mjs +43 -0
- package/dist/codemod/chunk-worker.mjs +476 -0
- package/dist/codemod/cli.mjs +298 -43659
- package/dist/codemod/transforms/data-attrs.mjs +551 -38
- package/dist/codemod/transforms/forward-ref.mjs +197 -32
- package/dist/codemod/transforms/import-rewrite.mjs +389 -30
- package/dist/codemod/transforms/should-filter.mjs +199 -10
- package/dist/codemod/worker.mjs +1 -0
- package/dist/core/index.d.mts +6 -1
- package/dist/core/index.mjs +12 -4
- package/dist/react/chunk-dialog.mjs +237 -0
- package/dist/react/dialog.d.mts +18 -0
- package/dist/react/dialog.mjs +2 -0
- package/dist/react/index.d.mts +26 -334
- package/dist/react/index.mjs +54 -1377
- package/dist/react/styles.css +2 -2
- package/package.json +12 -18
- package/dist/codemod/cli.d.mts +0 -16
- 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
|
@@ -1051,6 +1051,7 @@ function createCommandMachine(options = {}) {
|
|
|
1051
1051
|
const scheduler = createScheduler();
|
|
1052
1052
|
const emitter = new TypedEmitter();
|
|
1053
1053
|
const keyboardRegistry = new KeyboardShortcutRegistry();
|
|
1054
|
+
const pendingSources = /* @__PURE__ */ new Set();
|
|
1054
1055
|
let frecencyEngine = null;
|
|
1055
1056
|
if (options.frecency?.enabled) {
|
|
1056
1057
|
frecencyEngine = new FrecencyEngine(options.frecency);
|
|
@@ -1224,12 +1225,19 @@ function createCommandMachine(options = {}) {
|
|
|
1224
1225
|
case "ITEMS_LOADED":
|
|
1225
1226
|
registry.registerItems(event.items);
|
|
1226
1227
|
searchEngine.index(event.items);
|
|
1227
|
-
state = {
|
|
1228
|
-
...state,
|
|
1229
|
-
loading: false
|
|
1230
|
-
};
|
|
1231
1228
|
refilter(state.search);
|
|
1232
1229
|
break;
|
|
1230
|
+
case "LOADING": {
|
|
1231
|
+
if (event.pending) pendingSources.add(event.source);
|
|
1232
|
+
else pendingSources.delete(event.source);
|
|
1233
|
+
const loading = pendingSources.size > 0;
|
|
1234
|
+
if (loading !== state.loading) setState({
|
|
1235
|
+
...state,
|
|
1236
|
+
loading,
|
|
1237
|
+
lastUpdated: Date.now()
|
|
1238
|
+
});
|
|
1239
|
+
break;
|
|
1240
|
+
}
|
|
1233
1241
|
case "REGISTER_ITEM":
|
|
1234
1242
|
registry.registerItem(event.item);
|
|
1235
1243
|
searchEngine.index([event.item]);
|
|
@@ -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;
|