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.
@@ -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;
@@ -0,0 +1,2 @@
1
+ 'use client';
2
+ export { t as CommandDialog } from "./chunk-dialog.mjs";