modern-cmdk 1.2.3 → 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 +107 -52
- package/dist/codemod/cli.d.mts +16 -1
- package/dist/codemod/cli.mjs +184 -101
- 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/index.d.mts +12 -8
- package/dist/react/index.mjs +27 -14
- package/dist/react/styles.css +16 -7
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -9,18 +9,19 @@
|
|
|
9
9
|
|
|
10
10
|
<p align="center">
|
|
11
11
|
<a href="https://www.npmjs.com/package/modern-cmdk"><img alt="npm version" src="https://img.shields.io/npm/v/modern-cmdk?style=flat-square&color=dc2626&labelColor=0a0e27"/></a>
|
|
12
|
-
<a href="https://
|
|
12
|
+
<a href="https://github.com/ABCrimson/modern-cmdk/blob/main/package.json"><img alt="core size budget" src="https://img.shields.io/badge/core-%3C%206.5%20kB%20gzip-dc2626?style=flat-square&labelColor=0a0e27"/></a>
|
|
13
|
+
<a href="https://github.com/ABCrimson/modern-cmdk/blob/main/package.json"><img alt="react size budget" src="https://img.shields.io/badge/react-%3C%2012%20kB%20gzip-dc2626?style=flat-square&labelColor=0a0e27"/></a>
|
|
13
14
|
<a href="https://github.com/ABCrimson/modern-cmdk/actions/workflows/ci.yml"><img alt="CI" src="https://img.shields.io/github/actions/workflow/status/ABCrimson/modern-cmdk/ci.yml?style=flat-square&color=dc2626&labelColor=0a0e27&label=CI"/></a>
|
|
14
|
-
<a href="https://www.typescriptlang.org/"><img alt="TypeScript" src="https://img.shields.io/badge/TypeScript-7
|
|
15
|
+
<a href="https://www.typescriptlang.org/"><img alt="TypeScript" src="https://img.shields.io/badge/TypeScript-7-dc2626?style=flat-square&labelColor=0a0e27"/></a>
|
|
15
16
|
<a href="https://react.dev/"><img alt="React" src="https://img.shields.io/badge/React-19-dc2626?style=flat-square&labelColor=0a0e27"/></a>
|
|
16
17
|
<a href="https://github.com/ABCrimson/modern-cmdk/blob/main/LICENSE"><img alt="MIT License" src="https://img.shields.io/npm/l/modern-cmdk?style=flat-square&color=dc2626&labelColor=0a0e27"/></a>
|
|
17
18
|
</p>
|
|
18
19
|
|
|
19
20
|
<p align="center">
|
|
20
|
-
<a href="https://
|
|
21
|
-
<a href="https://
|
|
22
|
-
<a href="https://
|
|
23
|
-
<a href="https://
|
|
21
|
+
<a href="https://abcrimson.github.io/modern-cmdk/">Documentation</a> ·
|
|
22
|
+
<a href="https://abcrimson.github.io/modern-cmdk/guide/getting-started.html">Getting Started</a> ·
|
|
23
|
+
<a href="https://abcrimson.github.io/modern-cmdk/api/command.html">API Reference</a> ·
|
|
24
|
+
<a href="https://abcrimson.github.io/modern-cmdk/examples/basic.html">Examples</a>
|
|
24
25
|
</p>
|
|
25
26
|
|
|
26
27
|
---
|
|
@@ -33,7 +34,8 @@ npm install modern-cmdk
|
|
|
33
34
|
|
|
34
35
|
> [!NOTE]
|
|
35
36
|
> `modern-cmdk` is ESM-only and declares `engines.node >= 26.4.0`. The optional WASM search
|
|
36
|
-
> engine (`modern-cmdk-search-wasm`) is
|
|
37
|
+
> engine (`modern-cmdk-search-wasm`) is a private, unpublished workspace package (0.9.0) that you
|
|
38
|
+
> build from source; it is not on npm.
|
|
37
39
|
|
|
38
40
|
The package exports three entry points:
|
|
39
41
|
|
|
@@ -43,7 +45,11 @@ import { Command } from 'modern-cmdk/react'; // React 19 component
|
|
|
43
45
|
import 'modern-cmdk/styles.css'; // Default styles
|
|
44
46
|
```
|
|
45
47
|
|
|
46
|
-
React is an **optional** peer dependency — use the core engine standalone in any framework, or pair it with the React adapter
|
|
48
|
+
React is an **optional** peer dependency — use the core engine standalone in any framework, or pair it with the React adapter. `modern-cmdk/react` imports `react`, `react-dom` and `radix-ui`, which are optional peers that npm and pnpm do not install for you, so add them yourself (the Quick Start below needs all three):
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
npm install modern-cmdk react react-dom radix-ui
|
|
52
|
+
```
|
|
47
53
|
|
|
48
54
|
---
|
|
49
55
|
|
|
@@ -54,62 +60,78 @@ React is an **optional** peer dependency — use the core engine standalone in a
|
|
|
54
60
|
|
|
55
61
|
import { Command } from 'modern-cmdk/react';
|
|
56
62
|
import 'modern-cmdk/styles.css';
|
|
63
|
+
import { useEffect, useState } from 'react';
|
|
57
64
|
|
|
58
65
|
function CommandPalette() {
|
|
66
|
+
const [open, setOpen] = useState(false);
|
|
67
|
+
|
|
68
|
+
// Nothing opens the dialog for you: toggle it from a Mod+K listener (Cmd+K / Ctrl+K).
|
|
69
|
+
useEffect(() => {
|
|
70
|
+
const onKeyDown = (e: KeyboardEvent) => {
|
|
71
|
+
if (e.key === 'k' && (e.metaKey || e.ctrlKey)) {
|
|
72
|
+
e.preventDefault();
|
|
73
|
+
setOpen((o) => !o);
|
|
74
|
+
}
|
|
75
|
+
};
|
|
76
|
+
document.addEventListener('keydown', onKeyDown);
|
|
77
|
+
return () => document.removeEventListener('keydown', onKeyDown);
|
|
78
|
+
}, []);
|
|
79
|
+
|
|
59
80
|
return (
|
|
60
|
-
<Command.Dialog>
|
|
81
|
+
<Command.Dialog open={open} onOpenChange={setOpen} label="Command Menu">
|
|
61
82
|
<Command.Input placeholder="Type a command..." />
|
|
62
83
|
<Command.List>
|
|
63
84
|
<Command.Empty>No results found.</Command.Empty>
|
|
64
85
|
|
|
65
86
|
<Command.Group heading="Actions">
|
|
66
|
-
<Command.Item value="copy" onSelect={() =>
|
|
67
|
-
|
|
68
|
-
<Command.Shortcut
|
|
87
|
+
<Command.Item value="copy" onSelect={() => setOpen(false)}>
|
|
88
|
+
Copy to Clipboard
|
|
89
|
+
<Command.Shortcut shortcut="Mod+C" />
|
|
69
90
|
</Command.Item>
|
|
70
|
-
<Command.Item value="paste" onSelect={() =>
|
|
71
|
-
|
|
72
|
-
<Command.Shortcut
|
|
91
|
+
<Command.Item value="paste" onSelect={() => setOpen(false)}>
|
|
92
|
+
Paste
|
|
93
|
+
<Command.Shortcut shortcut="Mod+V" />
|
|
73
94
|
</Command.Item>
|
|
74
95
|
</Command.Group>
|
|
75
96
|
|
|
76
97
|
<Command.Group heading="Navigation">
|
|
77
|
-
<Command.Item value="settings" onSelect={() =>
|
|
98
|
+
<Command.Item value="settings" onSelect={() => setOpen(false)}>
|
|
78
99
|
Settings
|
|
79
100
|
<Command.Badge>New</Command.Badge>
|
|
80
101
|
</Command.Item>
|
|
81
102
|
</Command.Group>
|
|
82
103
|
</Command.List>
|
|
83
|
-
<Command.Loading>Searching...</Command.Loading>
|
|
84
104
|
</Command.Dialog>
|
|
85
105
|
);
|
|
86
106
|
}
|
|
87
107
|
```
|
|
88
108
|
|
|
89
|
-
|
|
109
|
+
`<Command label="Command menu">` takes the same children inline, with no dialog; `<Command.Highlight text={...} ranges={...} />` renders match highlighting from scorer ranges.
|
|
110
|
+
|
|
111
|
+
Or scaffold a new project:
|
|
90
112
|
|
|
91
113
|
```bash
|
|
92
|
-
npm create modern-cmdk
|
|
114
|
+
npm create modern-cmdk@latest my-palette -- --template=react-basic
|
|
93
115
|
```
|
|
94
116
|
|
|
95
117
|
---
|
|
96
118
|
|
|
97
119
|
## Why modern-cmdk?
|
|
98
120
|
|
|
99
|
-
A ground-up rewrite of `cmdk` for **React 19
|
|
121
|
+
A ground-up rewrite of `cmdk` for **React 19** and **TypeScript 7**, written in modern ECMAScript. Framework-agnostic core. Zero compromise on performance, accessibility, or developer experience.
|
|
100
122
|
|
|
101
123
|
| | cmdk | modern-cmdk |
|
|
102
124
|
|---|---|---|
|
|
103
125
|
| **Architecture** | React-only, tightly coupled | Framework-agnostic core + thin adapters |
|
|
104
|
-
| **React** | 18 | 19 (`use()`, `useOptimistic`, `useId`, `ref` as prop) |
|
|
105
|
-
| **Search** |
|
|
106
|
-
| **Ranking** | Static order | Frecency
|
|
126
|
+
| **React** | 18 / 19 | 19 (`use()`, `useOptimistic`, `useId`, `ref` as prop) |
|
|
127
|
+
| **Search** | Fuzzy (`command-score`) | Fuzzy scoring with match ranges; an experimental WASM engine (unpublished, does not work yet) |
|
|
128
|
+
| **Ranking** | Static order | Frecency: frequency x bucketed recency weights |
|
|
107
129
|
| **Virtualization** | None | Automatic variable-height, `content-visibility: auto` |
|
|
108
130
|
| **Animations** | CSS transitions | GPU-composited: `@starting-style`, `scroll-timeline`, spring easing |
|
|
109
131
|
| **Keyboard** | External | Built-in registry, `Mod` key, conflict detection |
|
|
110
132
|
| **Accessibility** | Partial ARIA | Full WAI-ARIA combobox, `forced-colors`, `prefers-contrast` |
|
|
111
|
-
| **Bundle** |
|
|
112
|
-
| **TypeScript** | 4.x/5.x | 7
|
|
133
|
+
| **Bundle** | not compared | Core ~6.4 kB, React ~9.8 kB gzip (budgets: < 6.5 kB, < 12 kB) |
|
|
134
|
+
| **TypeScript** | 4.x/5.x | 7, isolated declarations, branded types |
|
|
113
135
|
| **Cleanup** | Manual | `using`/`await using` (Explicit Resource Management) |
|
|
114
136
|
| **Error handling** | None | `CommandErrorBoundary` with fallback UI |
|
|
115
137
|
| **DevTools** | None | Built-in devtools hook for browser inspection |
|
|
@@ -118,12 +140,12 @@ A ground-up rewrite of `cmdk` for **React 19**, **ES2026**, and **TypeScript 7**
|
|
|
118
140
|
|
|
119
141
|
## Features
|
|
120
142
|
|
|
121
|
-
- **Framework-agnostic core** -- Pure TypeScript state machine. No
|
|
122
|
-
- **React 19 adapter** -- 14 compound components. `useSyncExternalStore`, `useTransition`, `useOptimistic`, `use()` for Suspense. React Compiler compatible.
|
|
123
|
-
- **Automatic virtualization** -- Variable-height virtual scrolling
|
|
124
|
-
- **Fuzzy search** -- Built-in TypeScript scorer with incremental filtering.
|
|
125
|
-
- **Frecency ranking** -- Frequency x recency
|
|
126
|
-
- **Keyboard shortcuts** -- Built-in registry with cross-platform `Mod` key, conflict detection
|
|
143
|
+
- **Framework-agnostic core** -- Pure TypeScript state machine. No React. No hard DOM dependency; DOM APIs are feature-detected and SSR-safe. Portable to any runtime. The sole runtime dependency (`idb-keyval`) is lazy-loaded only if you opt into IndexedDB frecency persistence.
|
|
144
|
+
- **React 19 adapter** -- a `Command` root with 14 compound sub-components (`Command.Input` through `Command.Activity`). `useSyncExternalStore`, `useTransition`, `useOptimistic`, `use()` for Suspense. React Compiler compatible.
|
|
145
|
+
- **Automatic virtualization** -- Variable-height virtual scrolling turns on when more than 100 items match and off again at 50 or fewer (fixed thresholds; force it with `<Command.List virtualize>`). Off-screen items use `content-visibility: auto`. The `virtualizeThreshold` option is accepted but not read yet.
|
|
146
|
+
- **Fuzzy search** -- Built-in TypeScript scorer with incremental filtering. Experimental Rust/WASM engine _(private, not published to npm)_; it does not work yet and always falls back to the TS scorer.
|
|
147
|
+
- **Frecency ranking** -- Frequency x recency, weighted by hour/day/week/month buckets. Pluggable persistence (memory or IndexedDB).
|
|
148
|
+
- **Keyboard shortcuts** -- Built-in registry with cross-platform `Mod` key, and conflict detection. Cross-browser helper functions for grouping and set operations.
|
|
127
149
|
- **Full accessibility** -- WAI-ARIA combobox pattern, `aria-live` announcements, `forced-colors`, `prefers-contrast`, `prefers-reduced-motion`.
|
|
128
150
|
- **GPU-composited animations** -- `@starting-style` entry, `scroll-timeline` progress, spring `linear()` easing. All customizable via CSS custom properties.
|
|
129
151
|
- **Error boundary** -- `CommandErrorBoundary` with static or render-function fallback.
|
|
@@ -140,10 +162,10 @@ A ground-up rewrite of `cmdk` for **React 19**, **ES2026**, and **TypeScript 7**
|
|
|
140
162
|
<Command.Input /> {/* Search input */}
|
|
141
163
|
<Command.List> {/* Scrollable list with auto-virtualization */}
|
|
142
164
|
<Command.Empty /> {/* Shown when filteredCount === 0 */}
|
|
143
|
-
<Command.Loading /> {/*
|
|
165
|
+
<Command.Loading /> {/* renders when the `loading` prop is true (or while the active-item transition is pending) */}
|
|
144
166
|
<Command.Group> {/* Labeled group of items */}
|
|
145
167
|
<Command.Item> {/* Selectable item */}
|
|
146
|
-
<Command.Highlight /> {/* Fuzzy match highlighting */}
|
|
168
|
+
<Command.Highlight text={label} ranges={ranges} /> {/* Fuzzy match highlighting */}
|
|
147
169
|
<Command.Badge /> {/* Status badge */}
|
|
148
170
|
<Command.Shortcut /> {/* Keyboard shortcut display */}
|
|
149
171
|
</Command.Item>
|
|
@@ -189,16 +211,17 @@ machine.subscribe(() => {
|
|
|
189
211
|
|
|
190
212
|
## Performance
|
|
191
213
|
|
|
192
|
-
| Benchmark | Target | Measured |
|
|
214
|
+
| Benchmark | Target | Measured (CI runner) |
|
|
193
215
|
|---|---|---|
|
|
194
|
-
| Search 10K items (TS scorer) | < 16 ms | ~
|
|
195
|
-
|
|
|
196
|
-
|
|
|
197
|
-
| State update cycle | < 4 ms | ~2.3 ms |
|
|
216
|
+
| Search 10K items (TS scorer) | < 16 ms | ~3.5 ms median |
|
|
217
|
+
| State update: `SEARCH_CHANGE` | < 4 ms | ~0.37 ms median |
|
|
218
|
+
| State update: `NAVIGATE` | < 1 ms | ~0.00075 ms median |
|
|
198
219
|
| Core bundle (gzipped) | <= 6.5 KB | ~6.4 KB |
|
|
199
220
|
| React bundle (gzipped) | <= 12 KB | ~9.8 KB |
|
|
200
221
|
|
|
201
|
-
|
|
222
|
+
Timings are medians from `benchmarks/baseline.json`: one CI run of the standalone harness, three runs of 1,000 iterations averaged, measured on a shared GitHub Actions
|
|
223
|
+
runner (noise is +/-20-40 %). CI compares new runs against that baseline with 5 % warning / 15 % failure
|
|
224
|
+
thresholds as a coarse tripwire, not a precise regression gate; the `NAVIGATE` baseline sits below the 0.01 ms noise floor and is reported, not compared, and a median above its target fails. There is no published WASM benchmark.
|
|
202
225
|
|
|
203
226
|
---
|
|
204
227
|
|
|
@@ -216,37 +239,69 @@ All animations and dimensions are customizable via CSS custom properties:
|
|
|
216
239
|
--command-dialog-max-width: 640px;
|
|
217
240
|
--command-item-height: 44px;
|
|
218
241
|
--command-highlight-color: oklch(0.85 0.15 90 / 0.3);
|
|
242
|
+
--command-muted-color: light-dark(oklch(0.45 0.015 260), oklch(0.75 0.015 260));
|
|
219
243
|
}
|
|
220
244
|
```
|
|
221
245
|
|
|
246
|
+
`--command-muted-color` colours secondary text (group headings, shortcuts, the empty state, the
|
|
247
|
+
placeholder). Its default uses `light-dark()`, which follows the used `color-scheme`: set
|
|
248
|
+
`color-scheme` on `:root` or a parent (for example `color-scheme: dark` on a dark theme), or override
|
|
249
|
+
`--command-muted-color`. A dark-themed page that sets no `color-scheme` gets the light value on its dark
|
|
250
|
+
surface (about 2.4:1 contrast).
|
|
251
|
+
|
|
222
252
|
All styles use `@layer command` for zero-specificity conflicts and CSS logical properties for full RTL support.
|
|
223
253
|
|
|
224
254
|
---
|
|
225
255
|
|
|
226
256
|
## Migration from cmdk
|
|
227
257
|
|
|
228
|
-
|
|
258
|
+
Four codemods ship as the `modern-cmdk` bin. Pass a transform name and a glob (quote the glob so your
|
|
259
|
+
shell does not expand it):
|
|
229
260
|
|
|
230
261
|
```bash
|
|
231
|
-
npx modern-cmdk
|
|
232
|
-
npx modern-cmdk
|
|
233
|
-
npx modern-cmdk
|
|
234
|
-
npx modern-cmdk should-filter ./src
|
|
262
|
+
npx modern-cmdk --help
|
|
263
|
+
npx modern-cmdk import-rewrite "./src/**/*.tsx" --dry-run
|
|
264
|
+
npx modern-cmdk import-rewrite "./src/**/*.tsx"
|
|
235
265
|
```
|
|
236
266
|
|
|
237
|
-
|
|
267
|
+
| Transform | What it does |
|
|
268
|
+
|---|---|
|
|
269
|
+
| `import-rewrite` | Rewrites `cmdk` imports to `modern-cmdk/react` |
|
|
270
|
+
| `data-attrs` | Converts `[cmdk-*]` selectors to `[data-command-*]` |
|
|
271
|
+
| `forward-ref` | Removes `React.forwardRef` wrappers (React 19) |
|
|
272
|
+
| `should-filter` | Converts the `shouldFilter` prop to `filter` |
|
|
273
|
+
|
|
274
|
+
Options: `--dry-run` lists the files that would change and writes nothing; `--concurrency=N` sets how
|
|
275
|
+
many files are processed at once (default 8); `--strict` is described below.
|
|
276
|
+
|
|
277
|
+
A run prints one `changed <file>` line per rewritten file and a summary. With `--dry-run` the lines read
|
|
278
|
+
`would change <file>` and the summary ends in `(dry run, nothing written)`:
|
|
279
|
+
|
|
280
|
+
```text
|
|
281
|
+
would change src/a.tsx
|
|
282
|
+
1 changed, 0 unchanged, 0 errors (dry run, nothing written)
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
Exit codes in 1.x are unchanged by default: `0` for `--help`, missing arguments and per-file errors, and
|
|
286
|
+
`1` for an unknown transform, a glob that matches no files, or an uncaught error. An unknown option is
|
|
287
|
+
warned about on stderr and ignored when the transform and glob are present. A malformed known option
|
|
288
|
+
(`--concurrency` with no value, `--dry-run=yes`) is a usage error that writes nothing and exits `0`.
|
|
289
|
+
With `--strict`, every usage error (including an unknown option, an unknown transform and a malformed
|
|
290
|
+
option) exits `2`, and any file error exits `1`.
|
|
291
|
+
|
|
292
|
+
See the [full migration guide](https://abcrimson.github.io/modern-cmdk/guide/migration-from-cmdk.html).
|
|
238
293
|
|
|
239
294
|
---
|
|
240
295
|
|
|
241
296
|
## Documentation
|
|
242
297
|
|
|
243
|
-
**[
|
|
298
|
+
**[abcrimson.github.io/modern-cmdk](https://abcrimson.github.io/modern-cmdk/)**
|
|
244
299
|
|
|
245
|
-
**Guides:** [Getting Started](https://
|
|
300
|
+
**Guides:** [Getting Started](https://abcrimson.github.io/modern-cmdk/guide/getting-started.html) | [Installation](https://abcrimson.github.io/modern-cmdk/guide/installation.html) | [Basic Usage](https://abcrimson.github.io/modern-cmdk/guide/basic-usage.html) | [Async Items](https://abcrimson.github.io/modern-cmdk/guide/async-items.html) | [WASM Search](https://abcrimson.github.io/modern-cmdk/guide/wasm-search.html) | [Frecency](https://abcrimson.github.io/modern-cmdk/guide/frecency.html) | [Shortcuts](https://abcrimson.github.io/modern-cmdk/guide/shortcuts.html) | [Virtualization](https://abcrimson.github.io/modern-cmdk/guide/virtualization.html) | [SSR / Next.js](https://abcrimson.github.io/modern-cmdk/guide/ssr.html) | [Theming](https://abcrimson.github.io/modern-cmdk/guide/theming.html) | [Accessibility](https://abcrimson.github.io/modern-cmdk/guide/accessibility.html)
|
|
246
301
|
|
|
247
|
-
**Recipes:** [File Picker](https://
|
|
302
|
+
**Recipes:** [File Picker](https://abcrimson.github.io/modern-cmdk/recipes/file-picker.html) | [Emoji Picker](https://abcrimson.github.io/modern-cmdk/recipes/emoji-picker.html) | [AI Chat Commands](https://abcrimson.github.io/modern-cmdk/recipes/ai-chat-commands.html) | [Nested Commands](https://abcrimson.github.io/modern-cmdk/recipes/nested-commands.html) | [Spotlight Search](https://abcrimson.github.io/modern-cmdk/recipes/spotlight-search.html)
|
|
248
303
|
|
|
249
|
-
**API:** [Core Engine](https://
|
|
304
|
+
**API:** [Core Engine](https://abcrimson.github.io/modern-cmdk/api/command.html) | [React Adapter](https://abcrimson.github.io/modern-cmdk/api/command-react.html) | [WASM Search](https://abcrimson.github.io/modern-cmdk/api/command-search-wasm.html)
|
|
250
305
|
|
|
251
306
|
---
|
|
252
307
|
|
|
@@ -254,9 +309,9 @@ See the [full migration guide](https://command.crimson.dev/guide/migration-from-
|
|
|
254
309
|
|
|
255
310
|
| Package | Description |
|
|
256
311
|
|---|---|
|
|
257
|
-
| [`create-modern-cmdk`](https://www.npmjs.com/package/create-modern-cmdk) | `npm create modern-cmdk
|
|
258
|
-
| `modern-cmdk-search-wasm` _(
|
|
259
|
-
|
|
|
312
|
+
| [`create-modern-cmdk`](https://www.npmjs.com/package/create-modern-cmdk) | `npm create modern-cmdk@latest <name>` -- scaffold a new project |
|
|
313
|
+
| `modern-cmdk-search-wasm` _(private 0.9.0, not published; build from source)_ | Experimental Rust/WASM fuzzy search |
|
|
314
|
+
| `vscode-modern-cmdk` _(not yet published)_ | VS Code snippets extension |
|
|
260
315
|
|
|
261
316
|
---
|
|
262
317
|
|
package/dist/codemod/cli.d.mts
CHANGED
|
@@ -1 +1,16 @@
|
|
|
1
|
-
|
|
1
|
+
import { API, FileInfo } from "jscodeshift";
|
|
2
|
+
type TransformFn = (fileInfo: FileInfo, api: API) => string;
|
|
3
|
+
/** The two streams the CLI writes to; `process` satisfies this, tests pass collectors. */
|
|
4
|
+
export interface CliIo {
|
|
5
|
+
readonly stdout: NodeJS.WritableStream;
|
|
6
|
+
readonly stderr: NodeJS.WritableStream;
|
|
7
|
+
}
|
|
8
|
+
/** Test seam: replace how a transform is loaded (the bundled bin imports `./transforms/<name>.mjs`). */
|
|
9
|
+
export interface CliDeps {
|
|
10
|
+
readonly loadTransform?: (name: string) => Promise<TransformFn>;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Run the codemod CLI. Resolves to the process exit code; writes only to `io`.
|
|
14
|
+
*/
|
|
15
|
+
export declare function run(argv: readonly string[], io: CliIo, deps?: CliDeps): Promise<number>;
|
|
16
|
+
export {};
|
package/dist/codemod/cli.mjs
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { createRequire } from "node:module";
|
|
3
3
|
import fsPromises, { readFile, writeFile } from "node:fs/promises";
|
|
4
|
-
import path, {
|
|
4
|
+
import path, { relative } from "node:path";
|
|
5
|
+
import { parseArgs, promisify, styleText } from "node:util";
|
|
5
6
|
import process$1 from "node:process";
|
|
6
7
|
import fs from "node:fs";
|
|
7
8
|
import "node:stream";
|
|
8
9
|
import "node:events";
|
|
9
10
|
import "node:stream/promises";
|
|
10
|
-
import { promisify } from "node:util";
|
|
11
11
|
import { execFile } from "node:child_process";
|
|
12
12
|
import { fileURLToPath } from "node:url";
|
|
13
13
|
import os from "node:os";
|
|
@@ -43584,7 +43584,7 @@ var require_core = /* @__PURE__ */ __commonJSMin(((exports, module) => {
|
|
|
43584
43584
|
}
|
|
43585
43585
|
module.exports = enrichCore(core, getParser());
|
|
43586
43586
|
}));
|
|
43587
|
-
// src/
|
|
43587
|
+
// src/codemod/cli.ts
|
|
43588
43588
|
var import_jscodeshift = /* @__PURE__ */ __toESM((/* @__PURE__ */ __commonJSMin(((exports, module) => {
|
|
43589
43589
|
/**
|
|
43590
43590
|
* Copyright (c) Facebook, Inc. and its affiliates.
|
|
@@ -43594,21 +43594,6 @@ var import_jscodeshift = /* @__PURE__ */ __toESM((/* @__PURE__ */ __commonJSMin(
|
|
|
43594
43594
|
*/
|
|
43595
43595
|
module.exports = require_core();
|
|
43596
43596
|
})))(), 1);
|
|
43597
|
-
/**
|
|
43598
|
-
* Groups items from an iterable into a plain object keyed by the string result of `keyFn`.
|
|
43599
|
-
* Cross-browser replacement for `Object.groupBy()`.
|
|
43600
|
-
*/
|
|
43601
|
-
function objectGroupBy(items, keyFn) {
|
|
43602
|
-
const result = Object.create(null);
|
|
43603
|
-
for (const item of items) {
|
|
43604
|
-
const key = keyFn(item);
|
|
43605
|
-
const existing = result[key];
|
|
43606
|
-
if (existing) existing.push(item);
|
|
43607
|
-
else result[key] = [item];
|
|
43608
|
-
}
|
|
43609
|
-
return result;
|
|
43610
|
-
}
|
|
43611
|
-
// src/codemod/cli.ts
|
|
43612
43597
|
const AVAILABLE_TRANSFORMS = /* @__PURE__ */ new Map([
|
|
43613
43598
|
["import-rewrite", "Rewrites cmdk imports to modern-cmdk/react"],
|
|
43614
43599
|
["data-attrs", "Converts [cmdk-*] selectors to [data-command-*]"],
|
|
@@ -43619,102 +43604,200 @@ const AVAILABLE_TRANSFORMS = /* @__PURE__ */ new Map([
|
|
|
43619
43604
|
* Dynamically import a transform module by name and return its default export.
|
|
43620
43605
|
*/
|
|
43621
43606
|
async function loadTransform(name) {
|
|
43622
|
-
return (await import(
|
|
43607
|
+
return (await import(
|
|
43608
|
+
/* @vite-ignore */
|
|
43609
|
+
`./transforms/${name}.mjs`
|
|
43610
|
+
)).default;
|
|
43623
43611
|
}
|
|
43624
|
-
function
|
|
43625
|
-
|
|
43612
|
+
function usageText(stream) {
|
|
43613
|
+
const bold = (s) => styleText("bold", s, { stream });
|
|
43614
|
+
const opt = (s) => styleText("cyan", s.padEnd(17), { stream });
|
|
43615
|
+
return [
|
|
43616
|
+
`${bold("Usage:")} modern-cmdk <transform> <glob> [options]`,
|
|
43617
|
+
"",
|
|
43618
|
+
bold("Transforms:"),
|
|
43619
|
+
...AVAILABLE_TRANSFORMS.entries().map(([name, description]) => ` ${opt(name)}${description}`).toArray(),
|
|
43620
|
+
"",
|
|
43621
|
+
bold("Options:"),
|
|
43622
|
+
` ${opt("--dry-run")}List the files that would change; write nothing`,
|
|
43623
|
+
` ${opt("--strict")}Exit 2 on usage errors (incl. unknown options), 1 on file errors`,
|
|
43624
|
+
` ${opt("--concurrency=N")}Files processed at once (default 8)`,
|
|
43625
|
+
` ${opt("-h, --help")}Show this help`,
|
|
43626
|
+
""
|
|
43627
|
+
].join("\n");
|
|
43626
43628
|
}
|
|
43627
|
-
|
|
43628
|
-
|
|
43629
|
-
|
|
43630
|
-
|
|
43631
|
-
|
|
43632
|
-
|
|
43633
|
-
|
|
43634
|
-
|
|
43635
|
-
|
|
43629
|
+
const OPTIONS = {
|
|
43630
|
+
help: {
|
|
43631
|
+
type: "boolean",
|
|
43632
|
+
short: "h"
|
|
43633
|
+
},
|
|
43634
|
+
strict: { type: "boolean" },
|
|
43635
|
+
"dry-run": { type: "boolean" },
|
|
43636
|
+
concurrency: { type: "string" }
|
|
43637
|
+
};
|
|
43638
|
+
function parse(argv) {
|
|
43639
|
+
let values;
|
|
43640
|
+
let positionals;
|
|
43641
|
+
let ignored = [];
|
|
43642
|
+
try {
|
|
43643
|
+
({values, positionals} = parseCli(argv));
|
|
43644
|
+
} catch (err) {
|
|
43645
|
+
const strictArg = argv.includes("--strict");
|
|
43646
|
+
if (err.code !== "ERR_PARSE_ARGS_UNKNOWN_OPTION") return {
|
|
43647
|
+
kind: "usage-error",
|
|
43648
|
+
message: err instanceof Error ? err.message : String(err),
|
|
43649
|
+
strict: strictArg
|
|
43650
|
+
};
|
|
43651
|
+
const loose = parseArgs({
|
|
43652
|
+
args: [...argv],
|
|
43653
|
+
options: OPTIONS,
|
|
43654
|
+
allowPositionals: true,
|
|
43655
|
+
strict: false,
|
|
43656
|
+
tokens: true
|
|
43657
|
+
});
|
|
43658
|
+
ignored = loose.tokens.filter((t) => t.kind === "option" && !Object.hasOwn(OPTIONS, t.name)).map((t) => t.rawName);
|
|
43659
|
+
const badValue = loose.tokens.find((t) => t.kind === "option" && Object.hasOwn(OPTIONS, t.name) && (OPTIONS[t.name].type === "boolean" ? t.value !== void 0 : t.value === void 0 || !t.inlineValue && t.value.length > 1 && t.value.startsWith("-")));
|
|
43660
|
+
const badMessage = badValue?.kind === "option" ? badValue.value === void 0 ? `Option '${badValue.rawName} <value>' argument missing` : OPTIONS[badValue.name].type === "boolean" ? `Option '${badValue.rawName}' does not take an argument` : `Option '${badValue.rawName}' argument is ambiguous.\nDid you forget to specify the option argument for '${badValue.rawName}'?\nTo specify an option argument starting with a dash use '${badValue.rawName}=-XYZ'.` : void 0;
|
|
43661
|
+
if (badMessage !== void 0) return {
|
|
43662
|
+
kind: "usage-error",
|
|
43663
|
+
message: badMessage,
|
|
43664
|
+
strict: strictArg
|
|
43665
|
+
};
|
|
43666
|
+
if (strictArg || loose.values.help === true || loose.positionals.length < 2) return {
|
|
43667
|
+
kind: "usage-error",
|
|
43668
|
+
message: `unknown option ${ignored[0]}`,
|
|
43669
|
+
strict: strictArg
|
|
43670
|
+
};
|
|
43671
|
+
values = loose.values;
|
|
43672
|
+
positionals = loose.positionals;
|
|
43673
|
+
}
|
|
43674
|
+
const strict = values.strict === true;
|
|
43675
|
+
if (values.help) return { kind: "help" };
|
|
43676
|
+
if (positionals.length < 2) return {
|
|
43677
|
+
kind: "usage-error",
|
|
43678
|
+
message: void 0,
|
|
43679
|
+
strict
|
|
43680
|
+
};
|
|
43681
|
+
let concurrency = 8;
|
|
43682
|
+
if (values.concurrency !== void 0) {
|
|
43683
|
+
if (typeof values.concurrency !== "string" || !/^[1-9]\d*$/.test(values.concurrency)) return {
|
|
43684
|
+
kind: "usage-error",
|
|
43685
|
+
message: `--concurrency must be a positive integer, got "${String(values.concurrency)}"`,
|
|
43686
|
+
strict
|
|
43687
|
+
};
|
|
43688
|
+
concurrency = Number.parseInt(values.concurrency, 10);
|
|
43689
|
+
}
|
|
43636
43690
|
return {
|
|
43637
|
-
|
|
43638
|
-
|
|
43639
|
-
|
|
43640
|
-
|
|
43691
|
+
kind: "run",
|
|
43692
|
+
transformName: positionals[0],
|
|
43693
|
+
fileGlob: positionals[1],
|
|
43694
|
+
dryRun: values["dry-run"] === true,
|
|
43695
|
+
strict,
|
|
43696
|
+
concurrency,
|
|
43697
|
+
ignored
|
|
43641
43698
|
};
|
|
43642
43699
|
}
|
|
43700
|
+
function parseCli(argv) {
|
|
43701
|
+
return parseArgs({
|
|
43702
|
+
args: [...argv],
|
|
43703
|
+
options: OPTIONS,
|
|
43704
|
+
allowPositionals: true,
|
|
43705
|
+
strict: true
|
|
43706
|
+
});
|
|
43707
|
+
}
|
|
43643
43708
|
async function* chunked(items, size) {
|
|
43644
43709
|
for (let i = 0; i < items.length; i += size) yield items.slice(i, i + size);
|
|
43645
43710
|
}
|
|
43646
43711
|
async function processFile(filePath, transformFn, apiForParser, dryRun) {
|
|
43647
|
-
|
|
43648
|
-
|
|
43649
|
-
|
|
43650
|
-
|
|
43651
|
-
|
|
43652
|
-
|
|
43653
|
-
|
|
43654
|
-
if (
|
|
43712
|
+
try {
|
|
43713
|
+
const source = await readFile(filePath, "utf-8");
|
|
43714
|
+
const parser = filePath.endsWith(".tsx") || filePath.endsWith(".jsx") ? "tsx" : "ts";
|
|
43715
|
+
const result = transformFn({
|
|
43716
|
+
path: filePath,
|
|
43717
|
+
source
|
|
43718
|
+
}, apiForParser(parser));
|
|
43719
|
+
if (result !== source) {
|
|
43720
|
+
if (!dryRun) await writeFile(filePath, result, "utf-8");
|
|
43721
|
+
return {
|
|
43722
|
+
filePath,
|
|
43723
|
+
changed: true
|
|
43724
|
+
};
|
|
43725
|
+
}
|
|
43726
|
+
return {
|
|
43727
|
+
filePath,
|
|
43728
|
+
changed: false
|
|
43729
|
+
};
|
|
43730
|
+
} catch (err) {
|
|
43655
43731
|
return {
|
|
43656
43732
|
filePath,
|
|
43657
|
-
changed:
|
|
43733
|
+
changed: false,
|
|
43734
|
+
error: err instanceof Error ? err : new Error(String(err))
|
|
43658
43735
|
};
|
|
43659
43736
|
}
|
|
43660
|
-
return {
|
|
43661
|
-
filePath,
|
|
43662
|
-
changed: false
|
|
43663
|
-
};
|
|
43664
43737
|
}
|
|
43665
|
-
|
|
43666
|
-
|
|
43667
|
-
|
|
43668
|
-
|
|
43669
|
-
|
|
43670
|
-
}
|
|
43671
|
-
const
|
|
43672
|
-
|
|
43673
|
-
|
|
43674
|
-
|
|
43675
|
-
|
|
43676
|
-
|
|
43677
|
-
|
|
43678
|
-
|
|
43679
|
-
|
|
43680
|
-
|
|
43681
|
-
|
|
43682
|
-
|
|
43683
|
-
|
|
43684
|
-
|
|
43685
|
-
|
|
43686
|
-
|
|
43687
|
-
|
|
43688
|
-
|
|
43689
|
-
|
|
43690
|
-
|
|
43691
|
-
|
|
43692
|
-
|
|
43693
|
-
|
|
43694
|
-
|
|
43695
|
-
|
|
43696
|
-
|
|
43697
|
-
else allResults.push({
|
|
43698
|
-
filePath: "(unknown)",
|
|
43699
|
-
changed: false,
|
|
43700
|
-
error: result.reason
|
|
43738
|
+
/**
|
|
43739
|
+
* Run the codemod CLI. Resolves to the process exit code; writes only to `io`.
|
|
43740
|
+
*/
|
|
43741
|
+
async function run(argv, io, deps = {}) {
|
|
43742
|
+
const out = (s) => void io.stdout.write(`${s}\n`);
|
|
43743
|
+
const err = (s) => void io.stderr.write(`${s}\n`);
|
|
43744
|
+
const fail = (s) => err(`${styleText("red", "error:", { stream: io.stderr })} ${s}`);
|
|
43745
|
+
try {
|
|
43746
|
+
const parsed = parse(argv);
|
|
43747
|
+
if (parsed.kind === "help") {
|
|
43748
|
+
out(usageText(io.stdout));
|
|
43749
|
+
return 0;
|
|
43750
|
+
}
|
|
43751
|
+
if (parsed.kind === "usage-error") {
|
|
43752
|
+
if (parsed.message !== void 0) fail(parsed.message);
|
|
43753
|
+
err(usageText(io.stderr));
|
|
43754
|
+
return parsed.strict ? 2 : 0;
|
|
43755
|
+
}
|
|
43756
|
+
const { transformName, fileGlob, dryRun, strict, concurrency, ignored } = parsed;
|
|
43757
|
+
for (const name of ignored) err(`warning: unknown option ${name} ignored (--strict makes this exit 2)`);
|
|
43758
|
+
if (!AVAILABLE_TRANSFORMS.has(transformName)) {
|
|
43759
|
+
fail(`unknown transform "${transformName}". Valid transforms: ${[...AVAILABLE_TRANSFORMS.keys()].join(", ")}`);
|
|
43760
|
+
return strict ? 2 : 1;
|
|
43761
|
+
}
|
|
43762
|
+
const files = await globby([fileGlob], {
|
|
43763
|
+
absolute: true,
|
|
43764
|
+
ignore: [
|
|
43765
|
+
"**/node_modules/**",
|
|
43766
|
+
"**/dist/**",
|
|
43767
|
+
"**/build/**",
|
|
43768
|
+
"**/.next/**"
|
|
43769
|
+
]
|
|
43701
43770
|
});
|
|
43702
|
-
|
|
43703
|
-
|
|
43704
|
-
|
|
43705
|
-
|
|
43706
|
-
|
|
43707
|
-
|
|
43708
|
-
|
|
43709
|
-
|
|
43710
|
-
|
|
43711
|
-
|
|
43712
|
-
|
|
43713
|
-
|
|
43714
|
-
|
|
43715
|
-
|
|
43771
|
+
if (files.length === 0) {
|
|
43772
|
+
fail(`no files matched "${fileGlob}"`);
|
|
43773
|
+
return 1;
|
|
43774
|
+
}
|
|
43775
|
+
const transformFn = await (deps.loadTransform ?? loadTransform)(transformName);
|
|
43776
|
+
const apiForParser = (parser) => ({
|
|
43777
|
+
jscodeshift: import_jscodeshift.default.withParser(parser),
|
|
43778
|
+
j: import_jscodeshift.default.withParser(parser),
|
|
43779
|
+
stats: () => {},
|
|
43780
|
+
report: () => {}
|
|
43781
|
+
});
|
|
43782
|
+
const results = [];
|
|
43783
|
+
for await (const batch of chunked(files, concurrency)) results.push(...await Promise.all(batch.map((filePath) => processFile(filePath, transformFn, apiForParser, dryRun))));
|
|
43784
|
+
const rel = (p) => relative(process.cwd(), p).replaceAll("\\", "/");
|
|
43785
|
+
let changed = 0;
|
|
43786
|
+
let errors = 0;
|
|
43787
|
+
for (const r of results) if (r.error) {
|
|
43788
|
+
errors++;
|
|
43789
|
+
err(`${rel(r.filePath)}: ${r.error.message}`);
|
|
43790
|
+
} else if (r.changed) {
|
|
43791
|
+
changed++;
|
|
43792
|
+
out(`${styleText("green", dryRun ? "would change" : "changed", { stream: io.stdout })} ${rel(r.filePath)}`);
|
|
43793
|
+
}
|
|
43794
|
+
const unchanged = results.length - changed - errors;
|
|
43795
|
+
out(`${changed} changed, ${unchanged} unchanged, ${errors} errors${dryRun ? " (dry run, nothing written)" : ""}`);
|
|
43796
|
+
return strict && errors > 0 ? 1 : 0;
|
|
43797
|
+
} catch (e) {
|
|
43798
|
+
fail(e instanceof Error ? e.message : String(e));
|
|
43799
|
+
return 1;
|
|
43800
|
+
}
|
|
43716
43801
|
}
|
|
43717
|
-
main
|
|
43718
|
-
|
|
43719
|
-
});
|
|
43720
|
-
export {};
|
|
43802
|
+
if (import.meta.main) process.exitCode = await run(process.argv.slice(2), process);
|
|
43803
|
+
export { run };
|
|
@@ -14,7 +14,7 @@ const ATTR_MAP = /* @__PURE__ */ new Map([
|
|
|
14
14
|
const CSS_VAR_MAP = /* @__PURE__ */ new Map([["--cmdk-list-height", "--command-list-height"]]);
|
|
15
15
|
/**
|
|
16
16
|
* Replace all cmdk attribute selectors (bracketed) and CSS vars in a string.
|
|
17
|
-
*
|
|
17
|
+
* Runs replaceAll for each ATTR_MAP entry (bracketed selectors), then for each CSS_VAR_MAP entry.
|
|
18
18
|
*/
|
|
19
19
|
function replaceInString(input) {
|
|
20
20
|
let result = input;
|
package/dist/core/index.d.mts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
interface SearchResult {
|
|
3
3
|
readonly id: ItemId;
|
|
4
4
|
readonly score: number;
|
|
5
|
-
/** Character ranges that matched (for highlighting) — [start, end
|
|
5
|
+
/** Character ranges that matched (for highlighting) — half-open [start, end) tuples, end exclusive */
|
|
6
6
|
readonly matches: ReadonlyArray<readonly [start: number, end: number]>;
|
|
7
7
|
}
|
|
8
8
|
/** A scoring function that evaluates a single item against a query */
|
|
@@ -13,7 +13,7 @@ interface SearchEngine extends Disposable {
|
|
|
13
13
|
index(items: readonly CommandItem[]): void;
|
|
14
14
|
/** Search items by query, return ordered results — uses Iterator pipeline */
|
|
15
15
|
search(query: string, items: readonly CommandItem[]): IteratorObject<SearchResult>;
|
|
16
|
-
/** Remove items from index
|
|
16
|
+
/** Remove items from index by id */
|
|
17
17
|
remove(ids: ReadonlySet<ItemId>): void;
|
|
18
18
|
/** Clear the entire index */
|
|
19
19
|
clear(): void;
|
|
@@ -162,7 +162,7 @@ interface FrecencyStorage extends Disposable {
|
|
|
162
162
|
load(namespace: string): FrecencyData | Promise<FrecencyData>;
|
|
163
163
|
save(namespace: string, data: FrecencyData): void | Promise<void>;
|
|
164
164
|
}
|
|
165
|
-
/** Frecency
|
|
165
|
+
/** Frecency step-weight configuration per recency bucket (hour/day/week/month/older) */
|
|
166
166
|
interface FrecencyDecayConfig {
|
|
167
167
|
readonly hourWeight?: number | undefined;
|
|
168
168
|
readonly dayWeight?: number | undefined;
|
|
@@ -179,7 +179,7 @@ interface FrecencyOptions {
|
|
|
179
179
|
}
|
|
180
180
|
/** Default frecency decay configuration */
|
|
181
181
|
export declare const DEFAULT_FRECENCY_DECAY: Required<FrecencyDecayConfig>;
|
|
182
|
-
/** Options for creating a command machine
|
|
182
|
+
/** Options for creating a command machine */
|
|
183
183
|
interface CommandMachineOptions {
|
|
184
184
|
/** Pre-registered items to populate on creation */
|
|
185
185
|
readonly items?: readonly CommandItem[] | undefined;
|
|
@@ -205,7 +205,7 @@ interface CommandMachineOptions {
|
|
|
205
205
|
readonly onSearchChange?: ((query: string) => void) | undefined;
|
|
206
206
|
/** Whether keyboard navigation loops from last to first item (default: true) */
|
|
207
207
|
readonly loop?: boolean | undefined;
|
|
208
|
-
/**
|
|
208
|
+
/** Accepted but not read yet: Command.List uses fixed thresholds (on above 100 matches, off at 50 or fewer) (default: 100) */
|
|
209
209
|
readonly virtualizeThreshold?: number | undefined;
|
|
210
210
|
}
|
|
211
211
|
/** Default machine options */
|
|
@@ -213,7 +213,7 @@ export declare const DEFAULT_MACHINE_OPTIONS: Partial<CommandMachineOptions>;
|
|
|
213
213
|
/** Initial state factory */
|
|
214
214
|
export declare function createInitialState(options?: CommandMachineOptions): CommandState;
|
|
215
215
|
/**
|
|
216
|
-
* IndexedDB-backed frecency storage using idb-keyval
|
|
216
|
+
* IndexedDB-backed frecency storage using idb-keyval.
|
|
217
217
|
*
|
|
218
218
|
* Persists frecency data across browser sessions. Implements both
|
|
219
219
|
* `Disposable` and `AsyncDisposable` for use with `using` and `await using`.
|
|
@@ -250,7 +250,7 @@ export declare class IdbFrecencyStorage implements FrecencyStorage, AsyncDisposa
|
|
|
250
250
|
/**
|
|
251
251
|
* Computes a frecency bonus for an item based on usage frequency and recency.
|
|
252
252
|
* Uses hour/day/week/month bucket boundaries with
|
|
253
|
-
* configurable
|
|
253
|
+
* configurable step weights per recency bucket (hour/day/week/month/older).
|
|
254
254
|
*/
|
|
255
255
|
export declare function computeFrecencyBonus(history: FrecencyRecord, now?: number, config?: FrecencyDecayConfig): number;
|
|
256
256
|
/**
|
|
@@ -290,7 +290,7 @@ export declare class MemoryFrecencyStorage implements FrecencyStorage {
|
|
|
290
290
|
#private;
|
|
291
291
|
load(namespace: string): FrecencyData;
|
|
292
292
|
save(namespace: string, data: FrecencyData): void;
|
|
293
|
-
/** Serialize all stored data — Iterator Helpers pipeline (
|
|
293
|
+
/** Serialize all stored data — Iterator Helpers pipeline (ES2025) */
|
|
294
294
|
toJSON(): Record<string, {
|
|
295
295
|
records: Array<[string, FrecencyRecord]>;
|
|
296
296
|
}>;
|
|
@@ -340,7 +340,7 @@ interface ShortcutBinding {
|
|
|
340
340
|
/**
|
|
341
341
|
* Manages keyboard shortcut bindings with global keydown listening.
|
|
342
342
|
* Supports `using` for automatic cleanup of both individual bindings and the
|
|
343
|
-
* global document listener. Detects conflicts
|
|
343
|
+
* global document listener. Detects conflicts with the local mapGroupBy helper (core/utils/group-by.ts).
|
|
344
344
|
*/
|
|
345
345
|
export declare class KeyboardShortcutRegistry implements Disposable {
|
|
346
346
|
#private;
|
|
@@ -466,7 +466,7 @@ interface SearchEngineOptions {
|
|
|
466
466
|
* Creates a search engine with pluggable scorer and incremental filtering.
|
|
467
467
|
* When the query extends the previous one, only prior matches are re-scored.
|
|
468
468
|
* Pre-caches lowercase at index time — zero toLowerCase() calls during search.
|
|
469
|
-
* Uses Iterator Helpers (
|
|
469
|
+
* Uses Iterator Helpers (ES2025) for result pipelines.
|
|
470
470
|
*/
|
|
471
471
|
export declare function createSearchEngine(options?: SearchEngineOptions): SearchEngine;
|
|
472
472
|
interface CommandTelemetryHooks {
|
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).
|
package/dist/react/index.d.mts
CHANGED
|
@@ -18,7 +18,7 @@ export declare function CommandActivity({ mode, children }: CommandActivityProps
|
|
|
18
18
|
interface SearchResult {
|
|
19
19
|
readonly id: ItemId;
|
|
20
20
|
readonly score: number;
|
|
21
|
-
/** Character ranges that matched (for highlighting) — [start, end
|
|
21
|
+
/** Character ranges that matched (for highlighting) — half-open [start, end) tuples, end exclusive */
|
|
22
22
|
readonly matches: ReadonlyArray<readonly [start: number, end: number]>;
|
|
23
23
|
}
|
|
24
24
|
/** Search engine interface — pluggable into the state machine */
|
|
@@ -27,7 +27,7 @@ interface SearchEngine extends Disposable {
|
|
|
27
27
|
index(items: readonly CommandItem$1[]): void;
|
|
28
28
|
/** Search items by query, return ordered results — uses Iterator pipeline */
|
|
29
29
|
search(query: string, items: readonly CommandItem$1[]): IteratorObject<SearchResult>;
|
|
30
|
-
/** Remove items from index
|
|
30
|
+
/** Remove items from index by id */
|
|
31
31
|
remove(ids: ReadonlySet<ItemId>): void;
|
|
32
32
|
/** Clear the entire index */
|
|
33
33
|
clear(): void;
|
|
@@ -172,7 +172,7 @@ interface FrecencyStorage extends Disposable {
|
|
|
172
172
|
load(namespace: string): FrecencyData | Promise<FrecencyData>;
|
|
173
173
|
save(namespace: string, data: FrecencyData): void | Promise<void>;
|
|
174
174
|
}
|
|
175
|
-
/** Frecency
|
|
175
|
+
/** Frecency step-weight configuration per recency bucket (hour/day/week/month/older) */
|
|
176
176
|
interface FrecencyDecayConfig {
|
|
177
177
|
readonly hourWeight?: number | undefined;
|
|
178
178
|
readonly dayWeight?: number | undefined;
|
|
@@ -187,7 +187,7 @@ interface FrecencyOptions {
|
|
|
187
187
|
readonly namespace?: string | undefined;
|
|
188
188
|
readonly decayConfig?: FrecencyDecayConfig | undefined;
|
|
189
189
|
}
|
|
190
|
-
/** Options for creating a command machine
|
|
190
|
+
/** Options for creating a command machine */
|
|
191
191
|
interface CommandMachineOptions {
|
|
192
192
|
/** Pre-registered items to populate on creation */
|
|
193
193
|
readonly items?: readonly CommandItem$1[] | undefined;
|
|
@@ -213,7 +213,7 @@ interface CommandMachineOptions {
|
|
|
213
213
|
readonly onSearchChange?: ((query: string) => void) | undefined;
|
|
214
214
|
/** Whether keyboard navigation loops from last to first item (default: true) */
|
|
215
215
|
readonly loop?: boolean | undefined;
|
|
216
|
-
/**
|
|
216
|
+
/** Accepted but not read yet: Command.List uses fixed thresholds (on above 100 matches, off at 50 or fewer) (default: 100) */
|
|
217
217
|
readonly virtualizeThreshold?: number | undefined;
|
|
218
218
|
}
|
|
219
219
|
/**
|
|
@@ -237,7 +237,7 @@ interface ShortcutBinding {
|
|
|
237
237
|
/**
|
|
238
238
|
* Manages keyboard shortcut bindings with global keydown listening.
|
|
239
239
|
* Supports `using` for automatic cleanup of both individual bindings and the
|
|
240
|
-
* global document listener. Detects conflicts
|
|
240
|
+
* global document listener. Detects conflicts with the local mapGroupBy helper (core/utils/group-by.ts).
|
|
241
241
|
*/
|
|
242
242
|
declare class KeyboardShortcutRegistry implements Disposable {
|
|
243
243
|
#private;
|
|
@@ -371,7 +371,7 @@ export declare function CommandGroup({ ref, children, heading, priority, forceId
|
|
|
371
371
|
interface CommandHighlightProps {
|
|
372
372
|
/** The full text to display */
|
|
373
373
|
readonly text: string;
|
|
374
|
-
/** Match ranges from search scorer — [start, end
|
|
374
|
+
/** Match ranges from the search scorer — half-open [start, end) tuples, end exclusive */
|
|
375
375
|
readonly ranges: ReadonlyArray<readonly [start: number, end: number]>;
|
|
376
376
|
/** Custom className for highlighted segments */
|
|
377
377
|
readonly highlightClassName?: string;
|
|
@@ -463,6 +463,7 @@ type CommandRootId = string & {
|
|
|
463
463
|
interface CommandContextValue {
|
|
464
464
|
readonly machine: CommandMachine;
|
|
465
465
|
readonly state: CommandState;
|
|
466
|
+
/** True while an optimistic active-item transition is in flight (search updates never set it). */
|
|
466
467
|
readonly isPending: boolean;
|
|
467
468
|
readonly updateSearch: (query: string) => void;
|
|
468
469
|
readonly setOptimisticActiveId: (id: ItemId | null) => void;
|
|
@@ -490,6 +491,7 @@ interface CommandStableContextValue {
|
|
|
490
491
|
/** State context — values that change on every search/navigation */
|
|
491
492
|
interface CommandStateContextValue {
|
|
492
493
|
readonly state: CommandState;
|
|
494
|
+
/** True while an optimistic active-item transition is in flight (search updates never set it). */
|
|
493
495
|
readonly isPending: boolean;
|
|
494
496
|
readonly filteredIdSet: ReadonlySet<ItemId>;
|
|
495
497
|
/** IDs currently visible in virtual viewport + overscan. null = not virtualizing (render all filtered). */
|
|
@@ -520,14 +522,16 @@ export declare class CommandErrorBoundary extends Component<CommandErrorBoundary
|
|
|
520
522
|
}
|
|
521
523
|
interface UseCommandReturn {
|
|
522
524
|
readonly state: CommandState;
|
|
525
|
+
/** True while an optimistic active-item transition is in flight. Search updates never set it: the machine applies them on its own scheduler tick, outside React. */
|
|
523
526
|
readonly isPending: boolean;
|
|
527
|
+
/** Queue a search query on the machine. Not a React update: Command.Input owns the urgent input value. */
|
|
524
528
|
readonly updateSearch: (query: string) => void;
|
|
525
529
|
readonly setOptimisticActiveId: (id: ItemId | null) => void;
|
|
526
530
|
/** O(1) membership set for filtered items — used by Item component */
|
|
527
531
|
readonly filteredIdSet: ReadonlySet<ItemId>;
|
|
528
532
|
readonly id: CommandRootId;
|
|
529
533
|
}
|
|
530
|
-
/** Core hook that binds a CommandMachine to React via useSyncExternalStore,
|
|
534
|
+
/** Core hook that binds a CommandMachine to React via useSyncExternalStore, useOptimistic, and a transition for optimistic active-item updates. */
|
|
531
535
|
export declare function useCommand(machine: CommandMachine): UseCommandReturn;
|
|
532
536
|
/** Get the full command state with proper subscription */
|
|
533
537
|
export declare function useCommandState(): CommandState;
|
package/dist/react/index.mjs
CHANGED
|
@@ -139,7 +139,7 @@ var MemoryFrecencyStorage = class {
|
|
|
139
139
|
save(namespace, data) {
|
|
140
140
|
this.#store.set(namespace, data);
|
|
141
141
|
}
|
|
142
|
-
/** Serialize all stored data — Iterator Helpers pipeline (
|
|
142
|
+
/** Serialize all stored data — Iterator Helpers pipeline (ES2025) */
|
|
143
143
|
toJSON() {
|
|
144
144
|
return Object.fromEntries(this.#store.entries().map(([namespace, data]) => [namespace, { records: data.records.entries().toArray() }]));
|
|
145
145
|
}
|
|
@@ -150,7 +150,7 @@ var MemoryFrecencyStorage = class {
|
|
|
150
150
|
// src/core/frecency/index.ts
|
|
151
151
|
/** Shared empty bonus map — avoids allocation when no frecency records exist */
|
|
152
152
|
const EMPTY_BONUS_MAP = /* @__PURE__ */ new Map();
|
|
153
|
-
/**
|
|
153
|
+
/** Bucket boundaries as hour constants — no per-call unit conversion */
|
|
154
154
|
const BUCKET_HOURS = {
|
|
155
155
|
hour: 1,
|
|
156
156
|
day: 24,
|
|
@@ -160,7 +160,7 @@ const BUCKET_HOURS = {
|
|
|
160
160
|
/**
|
|
161
161
|
* Computes a frecency bonus for an item based on usage frequency and recency.
|
|
162
162
|
* Uses hour/day/week/month bucket boundaries with
|
|
163
|
-
* configurable
|
|
163
|
+
* configurable step weights per recency bucket (hour/day/week/month/older).
|
|
164
164
|
*/
|
|
165
165
|
function computeFrecencyBonus(history, now = Date.now(), config = DEFAULT_FRECENCY_DECAY) {
|
|
166
166
|
const hours = (now - history.lastUsed) / 36e5;
|
|
@@ -362,7 +362,7 @@ function formatShortcut(parsed) {
|
|
|
362
362
|
/**
|
|
363
363
|
* Manages keyboard shortcut bindings with global keydown listening.
|
|
364
364
|
* Supports `using` for automatic cleanup of both individual bindings and the
|
|
365
|
-
* global document listener. Detects conflicts
|
|
365
|
+
* global document listener. Detects conflicts with the local mapGroupBy helper (core/utils/group-by.ts).
|
|
366
366
|
*/
|
|
367
367
|
var KeyboardShortcutRegistry = class {
|
|
368
368
|
#bindings = /* @__PURE__ */ new Map();
|
|
@@ -780,7 +780,7 @@ if (isDev && typeof window !== "undefined") leakRegistry = new FinalizationRegis
|
|
|
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;
|
|
@@ -889,9 +889,10 @@ var TypedEmitter = class {
|
|
|
889
889
|
// src/core/utils/scheduler.ts
|
|
890
890
|
/** Whether we're in a browser environment with rAF available */
|
|
891
891
|
const hasRAF = typeof requestAnimationFrame === "function";
|
|
892
|
-
/** 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). */
|
|
893
894
|
const canYield = typeof globalThis.scheduler?.yield === "function";
|
|
894
|
-
const hasInputPending = typeof navigator?.scheduling?.isInputPending === "function";
|
|
895
|
+
const hasInputPending = typeof globalThis.navigator?.scheduling?.isInputPending === "function";
|
|
895
896
|
/**
|
|
896
897
|
* Creates a batched update scheduler. Coalesces rapid state updates into a single
|
|
897
898
|
* execution pass using rAF (browser) or queueMicrotask (Node.js).
|
|
@@ -1214,7 +1215,7 @@ function createCommandMachine(options = {}) {
|
|
|
1214
1215
|
};
|
|
1215
1216
|
}
|
|
1216
1217
|
// src/react/hooks/use-command.ts
|
|
1217
|
-
/** Core hook that binds a CommandMachine to React via useSyncExternalStore,
|
|
1218
|
+
/** Core hook that binds a CommandMachine to React via useSyncExternalStore, useOptimistic, and a transition for optimistic active-item updates. */
|
|
1218
1219
|
function useCommand(machine) {
|
|
1219
1220
|
const id = useId();
|
|
1220
1221
|
const [isPending, startTransition] = useTransition();
|
|
@@ -1227,11 +1228,9 @@ function useCommand(machine) {
|
|
|
1227
1228
|
});
|
|
1228
1229
|
}, [rawSetOptimisticActiveId]);
|
|
1229
1230
|
const updateSearch = useCallback((query) => {
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
query
|
|
1234
|
-
});
|
|
1231
|
+
machine.send({
|
|
1232
|
+
type: "SEARCH_CHANGE",
|
|
1233
|
+
query
|
|
1235
1234
|
});
|
|
1236
1235
|
}, [machine]);
|
|
1237
1236
|
return {
|
|
@@ -1587,8 +1586,22 @@ function CommandHighlight({ text, ranges, highlightClassName }) {
|
|
|
1587
1586
|
function CommandInput({ ref, placeholder = "Search...", onValueChange, ...props }) {
|
|
1588
1587
|
const stable = useStableContext("Command.Input");
|
|
1589
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]);
|
|
1590
1601
|
const handleChange = useCallback((event) => {
|
|
1591
1602
|
const { value } = event.target;
|
|
1603
|
+
inFlight.current.push(value);
|
|
1604
|
+
setDraft(value);
|
|
1592
1605
|
stable.updateSearch(value);
|
|
1593
1606
|
onValueChange?.(value);
|
|
1594
1607
|
}, [stable.updateSearch, onValueChange]);
|
|
@@ -1607,7 +1620,7 @@ function CommandInput({ ref, placeholder = "Search...", onValueChange, ...props
|
|
|
1607
1620
|
autoCorrect: "off",
|
|
1608
1621
|
spellCheck: false,
|
|
1609
1622
|
placeholder,
|
|
1610
|
-
value:
|
|
1623
|
+
value: draft,
|
|
1611
1624
|
onChange: handleChange,
|
|
1612
1625
|
...props
|
|
1613
1626
|
});
|
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] {
|