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.
@@ -44,7 +44,7 @@ function storageKey(namespace) {
44
44
  }
45
45
  let idbModule;
46
46
  /**
47
- * IndexedDB-backed frecency storage using idb-keyval 6.2.2.
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 (ES2026) */
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
- /** Pre-computed bucket boundaries in hours — avoids Duration.total() per call */
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 exponential decay weights per time bucket.
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 via Map.groupBy (ES2026).
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 (ES2026) for result pipelines and Set.difference for pruning.
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;
@@ -0,0 +1,3 @@
1
+ 'use client';
2
+ "use client";
3
+ export { t as CommandDialog } from "./chunk-dialog.mjs";