ark-cmdk 0.0.0 → 0.1.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 +21 -0
- package/README.md +55 -0
- package/dist/index.d.mts +128 -0
- package/dist/index.mjs +422 -0
- package/docs/context.md +121 -0
- package/examples/controlled-state.tsx +36 -0
- package/examples/dialog.tsx +32 -0
- package/examples/inline.tsx +30 -0
- package/package.json +50 -10
- package/readme.md +0 -1
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) Alec Larson
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# ark-cmdk
|
|
2
|
+
|
|
3
|
+
## Purpose
|
|
4
|
+
|
|
5
|
+
`ark-cmdk` is a small, data-driven command menu for React 19 built on Ark UI.
|
|
6
|
+
|
|
7
|
+
It ships one first-class API for:
|
|
8
|
+
|
|
9
|
+
- inline command menus with `Command.Root`
|
|
10
|
+
- modal command palettes with `Command.Dialog`
|
|
11
|
+
- explicit item identity, search ranking, and active-item state
|
|
12
|
+
|
|
13
|
+
## Installation
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
pnpm add ark-cmdk react react-dom
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
`ark-cmdk` targets React 19, browser-side CSR, and ESM only.
|
|
20
|
+
|
|
21
|
+
## Quick Example
|
|
22
|
+
|
|
23
|
+
```tsx
|
|
24
|
+
import { Command } from 'ark-cmdk'
|
|
25
|
+
|
|
26
|
+
const items = [
|
|
27
|
+
{ id: 'open', label: 'Open File' },
|
|
28
|
+
{ id: 'save', label: 'Save File' },
|
|
29
|
+
]
|
|
30
|
+
|
|
31
|
+
export function Example() {
|
|
32
|
+
return (
|
|
33
|
+
<Command.Root
|
|
34
|
+
itemToString={(item) => item.label}
|
|
35
|
+
itemToValue={(item) => item.id}
|
|
36
|
+
items={items}
|
|
37
|
+
label="Command Menu"
|
|
38
|
+
>
|
|
39
|
+
<Command.Input placeholder="Search commands..." />
|
|
40
|
+
<Command.List empty={<div>No results.</div>}>
|
|
41
|
+
{(item) => <div>{item.label}</div>}
|
|
42
|
+
</Command.List>
|
|
43
|
+
</Command.Root>
|
|
44
|
+
)
|
|
45
|
+
}
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## Documentation Map
|
|
49
|
+
|
|
50
|
+
- Concepts and recommended patterns: [docs/context.md](docs/context.md)
|
|
51
|
+
- Inline usage example: [examples/inline.tsx](examples/inline.tsx)
|
|
52
|
+
- Dialog usage example: [examples/dialog.tsx](examples/dialog.tsx)
|
|
53
|
+
- Controlled state example: [examples/controlled-state.tsx](examples/controlled-state.tsx)
|
|
54
|
+
- Exact exported signatures: [dist/index.d.mts](dist/index.d.mts)
|
|
55
|
+
- API contract: [docs/v0.1-api.md](docs/v0.1-api.md)
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { ReactElement, RefAttributes } from "react";
|
|
3
|
+
|
|
4
|
+
//#region src/command/types.d.ts
|
|
5
|
+
/**
|
|
6
|
+
* Scores an item against the current search string.
|
|
7
|
+
*
|
|
8
|
+
* Return a value less than or equal to `0` to hide the item. Higher scores rank
|
|
9
|
+
* earlier in the visible list.
|
|
10
|
+
*
|
|
11
|
+
* @param itemText The trimmed searchable text derived from `itemToString`.
|
|
12
|
+
* @param search The trimmed current search string.
|
|
13
|
+
* @param keywords The trimmed keyword aliases derived from `itemToKeywords`.
|
|
14
|
+
*/
|
|
15
|
+
type CommandFilter = (itemText: string, search: string, keywords: readonly string[]) => number;
|
|
16
|
+
/**
|
|
17
|
+
* Props for the inline command surface.
|
|
18
|
+
*
|
|
19
|
+
* `Command.Root` owns item normalization, filtering, active-item state, and
|
|
20
|
+
* selection for a data-driven item array. It never inspects arbitrary child
|
|
21
|
+
* trees to discover items.
|
|
22
|
+
*/
|
|
23
|
+
interface CommandRootProps<TItem = any> extends Omit<React.ComponentPropsWithoutRef<'div'>, 'children' | 'defaultValue' | 'onSelect'> {
|
|
24
|
+
activeValue?: string | null;
|
|
25
|
+
children: React.ReactNode;
|
|
26
|
+
class?: string;
|
|
27
|
+
defaultActiveValue?: string | null;
|
|
28
|
+
defaultSearch?: string;
|
|
29
|
+
filter?: CommandFilter;
|
|
30
|
+
itemToDisabled?: (item: TItem) => boolean | undefined;
|
|
31
|
+
itemToKeywords?: (item: TItem) => readonly string[] | undefined;
|
|
32
|
+
itemToString: (item: TItem) => string;
|
|
33
|
+
itemToValue: (item: TItem) => string;
|
|
34
|
+
items: readonly TItem[];
|
|
35
|
+
label: string;
|
|
36
|
+
onActiveValueChange?: (value: string | null) => void;
|
|
37
|
+
onSearchChange?: (search: string) => void;
|
|
38
|
+
onSelect?: (item: TItem) => void;
|
|
39
|
+
search?: string;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Props for the search input part.
|
|
43
|
+
*
|
|
44
|
+
* The rendered input value is bound to the parent command's `search` state.
|
|
45
|
+
*/
|
|
46
|
+
interface CommandInputProps extends Omit<React.ComponentPropsWithoutRef<'input'>, 'children' | 'defaultValue' | 'onChange' | 'type' | 'value'> {
|
|
47
|
+
class?: string;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Derived state passed to `Command.List` item render functions.
|
|
51
|
+
*/
|
|
52
|
+
interface CommandListItemState {
|
|
53
|
+
active: boolean;
|
|
54
|
+
disabled: boolean;
|
|
55
|
+
value: string;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Props for the visible item list.
|
|
59
|
+
*
|
|
60
|
+
* `children` is a render function over the current visible items, not an item
|
|
61
|
+
* registration API.
|
|
62
|
+
*/
|
|
63
|
+
interface CommandListProps<TItem = any> extends Omit<React.ComponentPropsWithoutRef<'div'>, 'children'> {
|
|
64
|
+
children: (item: TItem, state: CommandListItemState) => React.ReactNode;
|
|
65
|
+
class?: string;
|
|
66
|
+
empty?: React.ReactNode;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Props for the modal command wrapper.
|
|
70
|
+
*
|
|
71
|
+
* `Command.Dialog` reuses the same item, search, active-item, and selection
|
|
72
|
+
* semantics as `Command.Root`, while delegating overlay behavior to Ark
|
|
73
|
+
* `Dialog`.
|
|
74
|
+
*/
|
|
75
|
+
interface CommandDialogProps<TItem = any> extends CommandRootProps<TItem> {
|
|
76
|
+
contentClass?: string;
|
|
77
|
+
contentClassName?: string;
|
|
78
|
+
onOpenChange: (open: boolean) => void;
|
|
79
|
+
open: boolean;
|
|
80
|
+
overlayClass?: string;
|
|
81
|
+
overlayClassName?: string;
|
|
82
|
+
}
|
|
83
|
+
//#endregion
|
|
84
|
+
//#region src/command/dialog.d.ts
|
|
85
|
+
interface CommandDialogComponent {
|
|
86
|
+
<TItem = any>(props: CommandDialogProps<TItem> & RefAttributes<HTMLDivElement>): ReactElement;
|
|
87
|
+
}
|
|
88
|
+
//#endregion
|
|
89
|
+
//#region src/command/list.d.ts
|
|
90
|
+
interface CommandListComponent {
|
|
91
|
+
<TItem = any>(props: CommandListProps<TItem> & RefAttributes<HTMLDivElement>): ReactElement;
|
|
92
|
+
}
|
|
93
|
+
//#endregion
|
|
94
|
+
//#region src/command/root.d.ts
|
|
95
|
+
interface CommandRootComponent {
|
|
96
|
+
<TItem = any>(props: CommandRootProps<TItem> & RefAttributes<HTMLDivElement>): ReactElement;
|
|
97
|
+
}
|
|
98
|
+
//#endregion
|
|
99
|
+
//#region src/index.d.ts
|
|
100
|
+
/**
|
|
101
|
+
* Public namespace for `ark-cmdk`.
|
|
102
|
+
*
|
|
103
|
+
* Use `Command.Root` for inline command menus and `Command.Dialog` for modal
|
|
104
|
+
* command palettes. Pair either root with `Command.Input` and `Command.List`.
|
|
105
|
+
*
|
|
106
|
+
* @example
|
|
107
|
+
* ```tsx
|
|
108
|
+
* import { Command } from 'ark-cmdk'
|
|
109
|
+
*
|
|
110
|
+
* <Command.Root
|
|
111
|
+
* itemToString={(item) => item.label}
|
|
112
|
+
* itemToValue={(item) => item.id}
|
|
113
|
+
* items={items}
|
|
114
|
+
* label="Command Menu"
|
|
115
|
+
* >
|
|
116
|
+
* <Command.Input />
|
|
117
|
+
* <Command.List>{(item) => <div>{item.label}</div>}</Command.List>
|
|
118
|
+
* </Command.Root>
|
|
119
|
+
* ```
|
|
120
|
+
*/
|
|
121
|
+
declare const Command: {
|
|
122
|
+
readonly Dialog: CommandDialogComponent;
|
|
123
|
+
readonly Input: React.ForwardRefExoticComponent<CommandInputProps & React.RefAttributes<HTMLInputElement>>;
|
|
124
|
+
readonly List: CommandListComponent;
|
|
125
|
+
readonly Root: CommandRootComponent;
|
|
126
|
+
};
|
|
127
|
+
//#endregion
|
|
128
|
+
export { Command, type CommandDialogProps, type CommandFilter, type CommandInputProps, type CommandListItemState, type CommandListProps, type CommandRootProps };
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,422 @@
|
|
|
1
|
+
import { Dialog } from "@ark-ui/react/dialog";
|
|
2
|
+
import { Portal } from "@ark-ui/react/portal";
|
|
3
|
+
import { createContext, forwardRef, useCallback, useContext, useEffect, useEffectEvent, useMemo, useRef, useState } from "react";
|
|
4
|
+
import { Combobox, createListCollection } from "@ark-ui/react/combobox";
|
|
5
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
6
|
+
//#region src/command/state.ts
|
|
7
|
+
function composeRefs(...refs) {
|
|
8
|
+
return (value) => {
|
|
9
|
+
for (const ref of refs) {
|
|
10
|
+
if (!ref) continue;
|
|
11
|
+
if (typeof ref === "function") {
|
|
12
|
+
ref(value);
|
|
13
|
+
continue;
|
|
14
|
+
}
|
|
15
|
+
ref.current = value;
|
|
16
|
+
}
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
function mergeClassName(className, classProp) {
|
|
20
|
+
const merged = [classProp, className].filter(Boolean).join(" ").trim();
|
|
21
|
+
return merged === "" ? void 0 : merged;
|
|
22
|
+
}
|
|
23
|
+
function useCommandActiveState({ activeValue, defaultActiveValue = null, onActiveValueChange, search, visibleItems }) {
|
|
24
|
+
const changeActiveValue = useEffectEvent((value) => {
|
|
25
|
+
onActiveValueChange?.(value);
|
|
26
|
+
});
|
|
27
|
+
const [internalActiveValue, setInternalActiveValue] = useState(() => resolveInitialActiveValue(visibleItems, defaultActiveValue));
|
|
28
|
+
const isControlled = activeValue !== void 0;
|
|
29
|
+
const currentActiveValue = isControlled ? activeValue : internalActiveValue;
|
|
30
|
+
const previousSearchRef = useRef(search);
|
|
31
|
+
const previousValueOrderRef = useRef(visibleItems.map((item) => item.value));
|
|
32
|
+
const isFirstRenderRef = useRef(true);
|
|
33
|
+
const setActiveValue = useCallback((value) => {
|
|
34
|
+
if (Object.is(currentActiveValue, value)) return;
|
|
35
|
+
if (!isControlled) setInternalActiveValue(value);
|
|
36
|
+
changeActiveValue(value);
|
|
37
|
+
}, [
|
|
38
|
+
changeActiveValue,
|
|
39
|
+
currentActiveValue,
|
|
40
|
+
isControlled
|
|
41
|
+
]);
|
|
42
|
+
useEffect(() => {
|
|
43
|
+
if (isFirstRenderRef.current) {
|
|
44
|
+
isFirstRenderRef.current = false;
|
|
45
|
+
previousSearchRef.current = search;
|
|
46
|
+
previousValueOrderRef.current = visibleItems.map((item) => item.value);
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
const nextActiveValue = reconcileActiveValue({
|
|
50
|
+
currentActiveValue,
|
|
51
|
+
previousValueOrder: previousValueOrderRef.current,
|
|
52
|
+
searchChanged: search !== previousSearchRef.current,
|
|
53
|
+
visibleItems
|
|
54
|
+
});
|
|
55
|
+
previousSearchRef.current = search;
|
|
56
|
+
previousValueOrderRef.current = visibleItems.map((item) => item.value);
|
|
57
|
+
if (Object.is(nextActiveValue, currentActiveValue)) return;
|
|
58
|
+
if (!isControlled) setInternalActiveValue(nextActiveValue);
|
|
59
|
+
changeActiveValue(nextActiveValue);
|
|
60
|
+
}, [
|
|
61
|
+
changeActiveValue,
|
|
62
|
+
currentActiveValue,
|
|
63
|
+
isControlled,
|
|
64
|
+
search,
|
|
65
|
+
visibleItems
|
|
66
|
+
]);
|
|
67
|
+
return {
|
|
68
|
+
activeValue: isVisibleEnabledValue(visibleItems, currentActiveValue) ? currentActiveValue : null,
|
|
69
|
+
setActiveValue
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
function useControllableState({ defaultValue, onChange, value }) {
|
|
73
|
+
const changeValue = useEffectEvent((nextValue) => {
|
|
74
|
+
onChange?.(nextValue);
|
|
75
|
+
});
|
|
76
|
+
const [internalValue, setInternalValue] = useState(defaultValue);
|
|
77
|
+
const isControlled = value !== void 0;
|
|
78
|
+
const currentValue = isControlled ? value : internalValue;
|
|
79
|
+
return {
|
|
80
|
+
isControlled,
|
|
81
|
+
setValue: useCallback((nextValue) => {
|
|
82
|
+
if (Object.is(currentValue, nextValue)) return;
|
|
83
|
+
if (!isControlled) setInternalValue(nextValue);
|
|
84
|
+
changeValue(nextValue);
|
|
85
|
+
}, [
|
|
86
|
+
changeValue,
|
|
87
|
+
currentValue,
|
|
88
|
+
isControlled
|
|
89
|
+
]),
|
|
90
|
+
value: currentValue
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
function getFirstVisibleEnabledValue(items) {
|
|
94
|
+
for (const item of items) if (!item.disabled) return item.value;
|
|
95
|
+
return null;
|
|
96
|
+
}
|
|
97
|
+
function reconcileActiveValue({ currentActiveValue, previousValueOrder, searchChanged, visibleItems }) {
|
|
98
|
+
if (visibleItems.length === 0) return null;
|
|
99
|
+
if (searchChanged) return getFirstVisibleEnabledValue(visibleItems);
|
|
100
|
+
if (currentActiveValue == null) return getFirstVisibleEnabledValue(visibleItems);
|
|
101
|
+
if (isVisibleEnabledValue(visibleItems, currentActiveValue)) return currentActiveValue;
|
|
102
|
+
return getNextFallbackValue(previousValueOrder, visibleItems, currentActiveValue);
|
|
103
|
+
}
|
|
104
|
+
function resolveInitialActiveValue(items, defaultActiveValue) {
|
|
105
|
+
if (defaultActiveValue && isVisibleEnabledValue(items, defaultActiveValue)) return defaultActiveValue;
|
|
106
|
+
return getFirstVisibleEnabledValue(items);
|
|
107
|
+
}
|
|
108
|
+
function getNextFallbackValue(previousValueOrder, visibleItems, removedValue) {
|
|
109
|
+
const enabledValues = new Set(visibleItems.filter((item) => !item.disabled).map((item) => item.value));
|
|
110
|
+
if (enabledValues.size === 0) return null;
|
|
111
|
+
const removedIndex = previousValueOrder.indexOf(removedValue);
|
|
112
|
+
if (removedIndex === -1) return getFirstVisibleEnabledValue(visibleItems);
|
|
113
|
+
for (let index = removedIndex + 1; index < previousValueOrder.length; index += 1) {
|
|
114
|
+
const candidate = previousValueOrder[index];
|
|
115
|
+
if (enabledValues.has(candidate)) return candidate;
|
|
116
|
+
}
|
|
117
|
+
for (let index = removedIndex - 1; index >= 0; index -= 1) {
|
|
118
|
+
const candidate = previousValueOrder[index];
|
|
119
|
+
if (enabledValues.has(candidate)) return candidate;
|
|
120
|
+
}
|
|
121
|
+
return getFirstVisibleEnabledValue(visibleItems);
|
|
122
|
+
}
|
|
123
|
+
function isVisibleEnabledValue(items, value) {
|
|
124
|
+
if (!value) return false;
|
|
125
|
+
return items.some((item) => item.value === value && !item.disabled);
|
|
126
|
+
}
|
|
127
|
+
//#endregion
|
|
128
|
+
//#region src/command/context.tsx
|
|
129
|
+
const CommandContext = createContext(null);
|
|
130
|
+
function CommandContextProvider({ children, value }) {
|
|
131
|
+
return /* @__PURE__ */ jsx(CommandContext.Provider, {
|
|
132
|
+
value,
|
|
133
|
+
children
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
function useCommandContext() {
|
|
137
|
+
const value = useContext(CommandContext);
|
|
138
|
+
if (!value) throw new Error("Command parts must be rendered inside Command.Root or Command.Dialog.");
|
|
139
|
+
return value;
|
|
140
|
+
}
|
|
141
|
+
//#endregion
|
|
142
|
+
//#region src/command/filter.ts
|
|
143
|
+
const defaultFilter = (itemText, search, keywords) => {
|
|
144
|
+
const normalizedItemText = normalizeText(itemText);
|
|
145
|
+
const normalizedSearch = normalizeText(search);
|
|
146
|
+
if (normalizedSearch === "") return 1;
|
|
147
|
+
return commandScore(normalizedItemText, normalizedSearch, [...keywords]);
|
|
148
|
+
};
|
|
149
|
+
function getVisibleItems(items, search, filter) {
|
|
150
|
+
const normalizedSearch = normalizeText(search);
|
|
151
|
+
if (normalizedSearch === "") return items.map((item) => ({
|
|
152
|
+
...item,
|
|
153
|
+
score: 1
|
|
154
|
+
}));
|
|
155
|
+
return items.map((item) => ({
|
|
156
|
+
...item,
|
|
157
|
+
score: filter(item.text, normalizedSearch, item.keywords)
|
|
158
|
+
})).filter((item) => item.score > 0).sort((left, right) => {
|
|
159
|
+
if (left.score !== right.score) return right.score - left.score;
|
|
160
|
+
return left.index - right.index;
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
function normalizeKeywords(keywords) {
|
|
164
|
+
if (!keywords) return [];
|
|
165
|
+
return keywords.map((keyword) => normalizeText(keyword)).filter((keyword) => keyword.length > 0);
|
|
166
|
+
}
|
|
167
|
+
function normalizeText(value) {
|
|
168
|
+
return value.trim();
|
|
169
|
+
}
|
|
170
|
+
const SCORE_CONTINUE_MATCH = 1;
|
|
171
|
+
const SCORE_SPACE_WORD_JUMP = .9;
|
|
172
|
+
const SCORE_NON_SPACE_WORD_JUMP = .8;
|
|
173
|
+
const SCORE_CHARACTER_JUMP = .17;
|
|
174
|
+
const SCORE_TRANSPOSITION = .1;
|
|
175
|
+
const PENALTY_SKIPPED = .999;
|
|
176
|
+
const PENALTY_CASE_MISMATCH = .9999;
|
|
177
|
+
const PENALTY_NOT_COMPLETE = .99;
|
|
178
|
+
const isGap = /[\\/_+.#"@\x5b({&]/;
|
|
179
|
+
const gapMatches = /[\\/_+.#"@\x5b({&]/g;
|
|
180
|
+
const isSpace = /[\s-]/;
|
|
181
|
+
const spaceMatches = /[\s-]/g;
|
|
182
|
+
function commandScore(string, abbreviation, aliases) {
|
|
183
|
+
const haystack = aliases.length > 0 ? `${string} ${aliases.join(" ")}` : string;
|
|
184
|
+
return commandScoreInner(haystack, abbreviation, formatInput(haystack), formatInput(abbreviation), 0, 0, {});
|
|
185
|
+
}
|
|
186
|
+
function commandScoreInner(string, abbreviation, lowerString, lowerAbbreviation, stringIndex, abbreviationIndex, memoizedResults) {
|
|
187
|
+
if (abbreviationIndex === abbreviation.length) {
|
|
188
|
+
if (stringIndex === string.length) return SCORE_CONTINUE_MATCH;
|
|
189
|
+
return PENALTY_NOT_COMPLETE;
|
|
190
|
+
}
|
|
191
|
+
const memoizeKey = `${stringIndex},${abbreviationIndex}`;
|
|
192
|
+
const memoizedValue = memoizedResults[memoizeKey];
|
|
193
|
+
if (memoizedValue !== void 0) return memoizedValue;
|
|
194
|
+
const abbreviationChar = lowerAbbreviation.charAt(abbreviationIndex);
|
|
195
|
+
let index = lowerString.indexOf(abbreviationChar, stringIndex);
|
|
196
|
+
let highScore = 0;
|
|
197
|
+
while (index >= 0) {
|
|
198
|
+
let score = commandScoreInner(string, abbreviation, lowerString, lowerAbbreviation, index + 1, abbreviationIndex + 1, memoizedResults);
|
|
199
|
+
if (score > highScore) {
|
|
200
|
+
if (index === stringIndex) score *= SCORE_CONTINUE_MATCH;
|
|
201
|
+
else if (isGap.test(string.charAt(index - 1))) {
|
|
202
|
+
score *= SCORE_NON_SPACE_WORD_JUMP;
|
|
203
|
+
const wordBreaks = string.slice(stringIndex, index - 1).match(gapMatches);
|
|
204
|
+
if (wordBreaks && stringIndex > 0) score *= Math.pow(PENALTY_SKIPPED, wordBreaks.length);
|
|
205
|
+
} else if (isSpace.test(string.charAt(index - 1))) {
|
|
206
|
+
score *= SCORE_SPACE_WORD_JUMP;
|
|
207
|
+
const spaceBreaks = string.slice(stringIndex, index - 1).match(spaceMatches);
|
|
208
|
+
if (spaceBreaks && stringIndex > 0) score *= Math.pow(PENALTY_SKIPPED, spaceBreaks.length);
|
|
209
|
+
} else {
|
|
210
|
+
score *= SCORE_CHARACTER_JUMP;
|
|
211
|
+
if (stringIndex > 0) score *= Math.pow(PENALTY_SKIPPED, index - stringIndex);
|
|
212
|
+
}
|
|
213
|
+
if (string.charAt(index) !== abbreviation.charAt(abbreviationIndex)) score *= PENALTY_CASE_MISMATCH;
|
|
214
|
+
}
|
|
215
|
+
const transposed = lowerString.charAt(index - 1) === lowerAbbreviation.charAt(abbreviationIndex + 1) || lowerAbbreviation.charAt(abbreviationIndex + 1) === lowerAbbreviation.charAt(abbreviationIndex) && lowerString.charAt(index - 1) !== lowerAbbreviation.charAt(abbreviationIndex);
|
|
216
|
+
if (score < SCORE_TRANSPOSITION && transposed) {
|
|
217
|
+
const transposedScore = commandScoreInner(string, abbreviation, lowerString, lowerAbbreviation, index + 1, abbreviationIndex + 2, memoizedResults);
|
|
218
|
+
if (transposedScore * SCORE_TRANSPOSITION > score) score = transposedScore * SCORE_TRANSPOSITION;
|
|
219
|
+
}
|
|
220
|
+
if (score > highScore) highScore = score;
|
|
221
|
+
index = lowerString.indexOf(abbreviationChar, index + 1);
|
|
222
|
+
}
|
|
223
|
+
memoizedResults[memoizeKey] = highScore;
|
|
224
|
+
return highScore;
|
|
225
|
+
}
|
|
226
|
+
function formatInput(value) {
|
|
227
|
+
return value.toLowerCase().replace(spaceMatches, " ");
|
|
228
|
+
}
|
|
229
|
+
//#endregion
|
|
230
|
+
//#region src/command/root.tsx
|
|
231
|
+
const EMPTY_SELECTION = [];
|
|
232
|
+
const CommandRootInternal = forwardRef(function CommandRootImpl(props, ref) {
|
|
233
|
+
const { activeValue, children, class: classProp, className, defaultActiveValue, defaultSearch = "", filter = defaultFilter, itemToDisabled, itemToKeywords, itemToString, itemToValue, items, label, onActiveValueChange, onSearchChange, onSelect, search, inputRef: inputRefProp, ...rootProps } = props;
|
|
234
|
+
const fallbackInputRef = useRef(null);
|
|
235
|
+
const inputRef = inputRefProp ?? fallbackInputRef;
|
|
236
|
+
const emitSelect = useEffectEvent((value) => {
|
|
237
|
+
const selectedItem = normalizedItems.find((item) => item.value === value)?.item;
|
|
238
|
+
if (selectedItem) onSelect?.(selectedItem);
|
|
239
|
+
});
|
|
240
|
+
const searchState = useControllableState({
|
|
241
|
+
defaultValue: defaultSearch,
|
|
242
|
+
onChange: onSearchChange,
|
|
243
|
+
value: search
|
|
244
|
+
});
|
|
245
|
+
const normalizedItems = useMemo(() => {
|
|
246
|
+
const seenValues = /* @__PURE__ */ new Set();
|
|
247
|
+
return items.map((item, index) => {
|
|
248
|
+
const value = itemToValue(item);
|
|
249
|
+
if (value.trim() === "") throw new Error("Command items require a non-empty string value.");
|
|
250
|
+
if (seenValues.has(value)) throw new Error(`Command item values must be unique. Received "${value}".`);
|
|
251
|
+
seenValues.add(value);
|
|
252
|
+
return {
|
|
253
|
+
disabled: itemToDisabled?.(item) ?? false,
|
|
254
|
+
index,
|
|
255
|
+
item,
|
|
256
|
+
keywords: normalizeKeywords(itemToKeywords?.(item)),
|
|
257
|
+
text: normalizeText(itemToString(item)),
|
|
258
|
+
value
|
|
259
|
+
};
|
|
260
|
+
});
|
|
261
|
+
}, [
|
|
262
|
+
itemToDisabled,
|
|
263
|
+
itemToKeywords,
|
|
264
|
+
itemToString,
|
|
265
|
+
itemToValue,
|
|
266
|
+
items
|
|
267
|
+
]);
|
|
268
|
+
const visibleItems = useMemo(() => getVisibleItems(normalizedItems, searchState.value, filter), [
|
|
269
|
+
filter,
|
|
270
|
+
normalizedItems,
|
|
271
|
+
searchState.value
|
|
272
|
+
]);
|
|
273
|
+
const activeState = useCommandActiveState({
|
|
274
|
+
activeValue,
|
|
275
|
+
defaultActiveValue,
|
|
276
|
+
onActiveValueChange,
|
|
277
|
+
search: searchState.value,
|
|
278
|
+
visibleItems
|
|
279
|
+
});
|
|
280
|
+
const collection = useMemo(() => createListCollection({
|
|
281
|
+
isItemDisabled: (item) => item.disabled,
|
|
282
|
+
itemToString: (item) => item.text,
|
|
283
|
+
itemToValue: (item) => item.value,
|
|
284
|
+
items: visibleItems
|
|
285
|
+
}), [visibleItems]);
|
|
286
|
+
return /* @__PURE__ */ jsx(CommandContextProvider, {
|
|
287
|
+
value: {
|
|
288
|
+
inputRef,
|
|
289
|
+
label,
|
|
290
|
+
visibleActiveValue: activeState.activeValue,
|
|
291
|
+
visibleItems
|
|
292
|
+
},
|
|
293
|
+
children: /* @__PURE__ */ jsx(Combobox.Root, {
|
|
294
|
+
...rootProps,
|
|
295
|
+
ref,
|
|
296
|
+
className: mergeClassName(className, classProp),
|
|
297
|
+
closeOnSelect: false,
|
|
298
|
+
collection,
|
|
299
|
+
disableLayer: true,
|
|
300
|
+
highlightedValue: activeState.activeValue,
|
|
301
|
+
inputBehavior: "none",
|
|
302
|
+
inputValue: searchState.value,
|
|
303
|
+
loopFocus: false,
|
|
304
|
+
onHighlightChange: ({ highlightedValue }) => {
|
|
305
|
+
activeState.setActiveValue(highlightedValue);
|
|
306
|
+
},
|
|
307
|
+
onInputValueChange: ({ inputValue }) => {
|
|
308
|
+
searchState.setValue(inputValue);
|
|
309
|
+
},
|
|
310
|
+
onOpenChange: () => {},
|
|
311
|
+
onSelect: ({ itemValue }) => {
|
|
312
|
+
emitSelect(itemValue);
|
|
313
|
+
},
|
|
314
|
+
open: true,
|
|
315
|
+
openOnChange: false,
|
|
316
|
+
openOnClick: false,
|
|
317
|
+
openOnKeyPress: false,
|
|
318
|
+
selectionBehavior: "preserve",
|
|
319
|
+
value: EMPTY_SELECTION,
|
|
320
|
+
children
|
|
321
|
+
})
|
|
322
|
+
});
|
|
323
|
+
});
|
|
324
|
+
const CommandRoot = CommandRootInternal;
|
|
325
|
+
//#endregion
|
|
326
|
+
//#region src/command/dialog.tsx
|
|
327
|
+
const CommandDialog = forwardRef(function CommandDialog(props, ref) {
|
|
328
|
+
const { contentClass, contentClassName, onOpenChange, open, overlayClass, overlayClassName, ...rootProps } = props;
|
|
329
|
+
const inputRef = useRef(null);
|
|
330
|
+
useEffect(() => {
|
|
331
|
+
if (open) inputRef.current?.focus();
|
|
332
|
+
}, [open]);
|
|
333
|
+
return /* @__PURE__ */ jsx(Dialog.Root, {
|
|
334
|
+
"aria-label": rootProps.label,
|
|
335
|
+
initialFocusEl: () => inputRef.current,
|
|
336
|
+
onOpenChange: ({ open }) => {
|
|
337
|
+
onOpenChange(open);
|
|
338
|
+
},
|
|
339
|
+
open,
|
|
340
|
+
unmountOnExit: true,
|
|
341
|
+
children: /* @__PURE__ */ jsxs(Portal, { children: [/* @__PURE__ */ jsx(Dialog.Backdrop, { className: mergeClassName(overlayClassName, overlayClass) }), /* @__PURE__ */ jsx(Dialog.Positioner, { children: /* @__PURE__ */ jsx(Dialog.Content, {
|
|
342
|
+
className: mergeClassName(contentClassName, contentClass),
|
|
343
|
+
ref,
|
|
344
|
+
children: /* @__PURE__ */ jsx(CommandRootInternal, {
|
|
345
|
+
...rootProps,
|
|
346
|
+
inputRef
|
|
347
|
+
})
|
|
348
|
+
}) })] })
|
|
349
|
+
});
|
|
350
|
+
});
|
|
351
|
+
//#endregion
|
|
352
|
+
//#region src/command/input.tsx
|
|
353
|
+
const CommandInput = forwardRef(function CommandInput(props, ref) {
|
|
354
|
+
const { "aria-label": ariaLabel, "aria-labelledby": ariaLabelledby, class: classProp, className, ...inputProps } = props;
|
|
355
|
+
const { inputRef, label } = useCommandContext();
|
|
356
|
+
return /* @__PURE__ */ jsx(Combobox.Input, {
|
|
357
|
+
...inputProps,
|
|
358
|
+
"aria-label": ariaLabelledby ? void 0 : ariaLabel ?? label,
|
|
359
|
+
"aria-labelledby": ariaLabelledby,
|
|
360
|
+
className: mergeClassName(className, classProp),
|
|
361
|
+
ref: composeRefs(ref, inputRef)
|
|
362
|
+
});
|
|
363
|
+
});
|
|
364
|
+
//#endregion
|
|
365
|
+
//#region src/command/list.tsx
|
|
366
|
+
const CommandList = forwardRef(function CommandList(props, ref) {
|
|
367
|
+
const { children, class: classProp, className, empty, onPointerMoveCapture, ...listProps } = props;
|
|
368
|
+
const { visibleActiveValue, visibleItems } = useCommandContext();
|
|
369
|
+
return /* @__PURE__ */ jsx(Combobox.Content, { children: /* @__PURE__ */ jsxs(Combobox.List, {
|
|
370
|
+
...listProps,
|
|
371
|
+
className: mergeClassName(className, classProp),
|
|
372
|
+
onPointerMoveCapture: callAll(onPointerMoveCapture, stopPointerHighlightCapture),
|
|
373
|
+
ref,
|
|
374
|
+
children: [visibleItems.length === 0 ? empty : null, visibleItems.map((item) => /* @__PURE__ */ jsx(Combobox.Item, {
|
|
375
|
+
item,
|
|
376
|
+
children: children(item.item, {
|
|
377
|
+
active: item.value === visibleActiveValue,
|
|
378
|
+
disabled: item.disabled,
|
|
379
|
+
value: item.value
|
|
380
|
+
})
|
|
381
|
+
}, item.value))]
|
|
382
|
+
}) });
|
|
383
|
+
});
|
|
384
|
+
function callAll(...handlers) {
|
|
385
|
+
return (event) => {
|
|
386
|
+
for (const handler of handlers) handler?.(event);
|
|
387
|
+
};
|
|
388
|
+
}
|
|
389
|
+
function stopPointerHighlightCapture(event) {
|
|
390
|
+
if (event.target !== event.currentTarget) event.stopPropagation();
|
|
391
|
+
}
|
|
392
|
+
//#endregion
|
|
393
|
+
//#region src/index.ts
|
|
394
|
+
/**
|
|
395
|
+
* Public namespace for `ark-cmdk`.
|
|
396
|
+
*
|
|
397
|
+
* Use `Command.Root` for inline command menus and `Command.Dialog` for modal
|
|
398
|
+
* command palettes. Pair either root with `Command.Input` and `Command.List`.
|
|
399
|
+
*
|
|
400
|
+
* @example
|
|
401
|
+
* ```tsx
|
|
402
|
+
* import { Command } from 'ark-cmdk'
|
|
403
|
+
*
|
|
404
|
+
* <Command.Root
|
|
405
|
+
* itemToString={(item) => item.label}
|
|
406
|
+
* itemToValue={(item) => item.id}
|
|
407
|
+
* items={items}
|
|
408
|
+
* label="Command Menu"
|
|
409
|
+
* >
|
|
410
|
+
* <Command.Input />
|
|
411
|
+
* <Command.List>{(item) => <div>{item.label}</div>}</Command.List>
|
|
412
|
+
* </Command.Root>
|
|
413
|
+
* ```
|
|
414
|
+
*/
|
|
415
|
+
const Command = {
|
|
416
|
+
Dialog: CommandDialog,
|
|
417
|
+
Input: CommandInput,
|
|
418
|
+
List: CommandList,
|
|
419
|
+
Root: CommandRoot
|
|
420
|
+
};
|
|
421
|
+
//#endregion
|
|
422
|
+
export { Command };
|
package/docs/context.md
ADDED
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
# Overview
|
|
2
|
+
|
|
3
|
+
`ark-cmdk` is a small, data-driven command menu for React 19 built on Ark UI.
|
|
4
|
+
|
|
5
|
+
The library keeps one first-class model:
|
|
6
|
+
|
|
7
|
+
- the consumer owns an explicit item array
|
|
8
|
+
- `ark-cmdk` derives visibility, ranking, active-item state, and selection from that data
|
|
9
|
+
- Ark UI owns combobox and dialog accessibility behavior
|
|
10
|
+
|
|
11
|
+
# When to Use
|
|
12
|
+
|
|
13
|
+
Use `ark-cmdk` when you want:
|
|
14
|
+
|
|
15
|
+
- an inline command menu or modal command palette in a React 19 client app
|
|
16
|
+
- explicit item identity instead of DOM-driven item discovery
|
|
17
|
+
- built-in search ranking over a data array
|
|
18
|
+
- controlled or uncontrolled search state
|
|
19
|
+
- controlled or uncontrolled active-item state
|
|
20
|
+
- unstyled parts with consumer-owned item rendering
|
|
21
|
+
|
|
22
|
+
# When Not to Use
|
|
23
|
+
|
|
24
|
+
`ark-cmdk` is the wrong fit when you need:
|
|
25
|
+
|
|
26
|
+
- `cmdk` drop-in compatibility
|
|
27
|
+
- manual filtering or manual ordering control
|
|
28
|
+
- groups or loading helpers
|
|
29
|
+
- DOM-driven child inspection
|
|
30
|
+
- pointer-hover activation, loop navigation, Vim bindings, or Alt-based group jumps
|
|
31
|
+
- React Server Component usage or React versions older than 19
|
|
32
|
+
|
|
33
|
+
# Core Abstractions
|
|
34
|
+
|
|
35
|
+
- `Command.Root`: the inline state owner for item normalization, filtering, active-item state, and selection
|
|
36
|
+
- `Command.Input`: the search input bound to the root's `search` state
|
|
37
|
+
- `Command.List`: the visible item list rendered through a function child
|
|
38
|
+
- `Command.Dialog`: a thin modal wrapper over the same command semantics
|
|
39
|
+
- `search`: the current text query
|
|
40
|
+
- `active item`: the highlighted visible enabled item used for keyboard navigation and `aria-activedescendant`
|
|
41
|
+
- `committed selection`: the item passed to `onSelect`
|
|
42
|
+
|
|
43
|
+
# Data Flow / Lifecycle
|
|
44
|
+
|
|
45
|
+
1. The consumer passes `items` plus `itemToValue` and `itemToString`.
|
|
46
|
+
2. `ark-cmdk` normalizes item text, keywords, and disabled state from that data.
|
|
47
|
+
3. The current `search` string is scored against normalized items.
|
|
48
|
+
4. Visible items are derived from scores and sorted by rank.
|
|
49
|
+
5. The active item is reconciled against the visible enabled set.
|
|
50
|
+
6. Keyboard navigation updates the active item.
|
|
51
|
+
7. `Enter` or click commits the current item through `onSelect`.
|
|
52
|
+
|
|
53
|
+
The DOM is a projection of derived state. It is not the source of truth for item identity, ordering, or filtering.
|
|
54
|
+
|
|
55
|
+
# Common Tasks -> Recommended APIs
|
|
56
|
+
|
|
57
|
+
- Inline command menu: `Command.Root` + `Command.Input` + `Command.List`
|
|
58
|
+
- Modal command palette: `Command.Dialog`
|
|
59
|
+
- Controlled search: `search` + `onSearchChange`
|
|
60
|
+
- Controlled active item: `activeValue` + `onActiveValueChange`
|
|
61
|
+
- Search aliases: `itemToKeywords`
|
|
62
|
+
- Disabled-item handling: `itemToDisabled`
|
|
63
|
+
- Empty states: `Command.List` `empty`
|
|
64
|
+
- Custom row rendering: `Command.List` render function
|
|
65
|
+
- Styling: `className` on `Command.Root`, `Command.Input`, and `Command.List`, plus `overlayClassName` and `contentClassName` on `Command.Dialog`
|
|
66
|
+
|
|
67
|
+
# Recommended Patterns
|
|
68
|
+
|
|
69
|
+
- Keep `itemToValue` stable, unique, and non-empty across renders.
|
|
70
|
+
- Treat `search` and `activeValue` as separate concerns.
|
|
71
|
+
- Keep side effects in `onSelect`, not in active-item change handlers.
|
|
72
|
+
- Use `itemToKeywords` for aliases instead of concatenating extra text into rendered labels.
|
|
73
|
+
- Render item UI entirely inside the `Command.List` render function.
|
|
74
|
+
- Use examples in [examples/inline.tsx](../examples/inline.tsx), [examples/dialog.tsx](../examples/dialog.tsx), and [examples/controlled-state.tsx](../examples/controlled-state.tsx) as the canonical usage patterns.
|
|
75
|
+
|
|
76
|
+
# Patterns to Avoid
|
|
77
|
+
|
|
78
|
+
- Deriving item identity from rendered text or DOM structure
|
|
79
|
+
- Rebuilding item values with random or unstable IDs
|
|
80
|
+
- Treating active-item movement as a committed selection
|
|
81
|
+
- Assuming hidden or disabled items remain keyboard targets
|
|
82
|
+
- Depending on undocumented selector compatibility with `cmdk`
|
|
83
|
+
- Reintroducing manual filtering as an implicit second mode
|
|
84
|
+
|
|
85
|
+
# Invariants and Constraints
|
|
86
|
+
|
|
87
|
+
- React 19 only
|
|
88
|
+
- browser-side CSR only
|
|
89
|
+
- ESM only
|
|
90
|
+
- one first-class data-driven API
|
|
91
|
+
- item identity is explicit through `itemToValue`
|
|
92
|
+
- `Command.List` renders only the current visible item set
|
|
93
|
+
- disabled items are skipped by active-item selection and committed selection
|
|
94
|
+
- dialog behavior remains Ark-owned
|
|
95
|
+
|
|
96
|
+
# Error Model
|
|
97
|
+
|
|
98
|
+
- An empty `itemToValue` result throws during render.
|
|
99
|
+
- Duplicate `itemToValue` results throw during render.
|
|
100
|
+
- Returning an active value that does not resolve to a visible enabled item leaves the command with no rendered active item.
|
|
101
|
+
- A filter score less than or equal to `0` hides the item from the visible list.
|
|
102
|
+
|
|
103
|
+
# Terminology
|
|
104
|
+
|
|
105
|
+
- `search`: the current query string
|
|
106
|
+
- `visible item`: an item whose filter score is greater than `0`, or any item when search is empty
|
|
107
|
+
- `active item`: the highlighted visible enabled item
|
|
108
|
+
- `committed selection`: the item passed to `onSelect`
|
|
109
|
+
- `keywords`: alias terms used during scoring
|
|
110
|
+
|
|
111
|
+
# Non-Goals
|
|
112
|
+
|
|
113
|
+
- `cmdk` parity
|
|
114
|
+
- manual filtering mode
|
|
115
|
+
- groups
|
|
116
|
+
- loading helpers
|
|
117
|
+
- pointer-hover activation
|
|
118
|
+
- loop navigation
|
|
119
|
+
- Vim bindings
|
|
120
|
+
- Alt-based group jumps
|
|
121
|
+
- selector compatibility shims
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { useState } from 'react'
|
|
2
|
+
|
|
3
|
+
import { Command } from 'ark-cmdk'
|
|
4
|
+
|
|
5
|
+
const items = [
|
|
6
|
+
{ id: 'alpha', label: 'Alpha' },
|
|
7
|
+
{ id: 'bravo', label: 'Bravo' },
|
|
8
|
+
]
|
|
9
|
+
|
|
10
|
+
export function ControlledStateExample() {
|
|
11
|
+
const [search, setSearch] = useState('')
|
|
12
|
+
const [activeValue, setActiveValue] = useState<string | null>('alpha')
|
|
13
|
+
|
|
14
|
+
return (
|
|
15
|
+
<Command.Root
|
|
16
|
+
activeValue={activeValue}
|
|
17
|
+
itemToString={(item) => item.label}
|
|
18
|
+
itemToValue={(item) => item.id}
|
|
19
|
+
items={items}
|
|
20
|
+
label="Command Menu"
|
|
21
|
+
onActiveValueChange={setActiveValue}
|
|
22
|
+
onSearchChange={setSearch}
|
|
23
|
+
search={search}
|
|
24
|
+
>
|
|
25
|
+
<Command.Input />
|
|
26
|
+
<Command.List empty={<div>No results.</div>}>
|
|
27
|
+
{(item, state) => (
|
|
28
|
+
<div>
|
|
29
|
+
{state.active ? 'Active: ' : null}
|
|
30
|
+
{item.label}
|
|
31
|
+
</div>
|
|
32
|
+
)}
|
|
33
|
+
</Command.List>
|
|
34
|
+
</Command.Root>
|
|
35
|
+
)
|
|
36
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { useState } from 'react'
|
|
2
|
+
|
|
3
|
+
import { Command } from 'ark-cmdk'
|
|
4
|
+
|
|
5
|
+
const items = [
|
|
6
|
+
{ id: 'open', label: 'Open File' },
|
|
7
|
+
{ id: 'save', label: 'Save File' },
|
|
8
|
+
]
|
|
9
|
+
|
|
10
|
+
export function DialogExample() {
|
|
11
|
+
const [open, setOpen] = useState(false)
|
|
12
|
+
|
|
13
|
+
return (
|
|
14
|
+
<Command.Dialog
|
|
15
|
+
contentClassName="command-dialog"
|
|
16
|
+
itemToString={(item) => item.label}
|
|
17
|
+
itemToValue={(item) => item.id}
|
|
18
|
+
items={items}
|
|
19
|
+
label="Command Menu"
|
|
20
|
+
onOpenChange={setOpen}
|
|
21
|
+
open={open}
|
|
22
|
+
overlayClassName="command-overlay"
|
|
23
|
+
>
|
|
24
|
+
<Command.Input placeholder="Type a command..." />
|
|
25
|
+
<Command.List empty={<div>No results.</div>}>
|
|
26
|
+
{(item, state) => (
|
|
27
|
+
<div className={state.active ? 'is-active' : undefined}>{item.label}</div>
|
|
28
|
+
)}
|
|
29
|
+
</Command.List>
|
|
30
|
+
</Command.Dialog>
|
|
31
|
+
)
|
|
32
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { Command } from 'ark-cmdk'
|
|
2
|
+
|
|
3
|
+
const items = [
|
|
4
|
+
{ id: 'open', label: 'Open File', keywords: ['file'] },
|
|
5
|
+
{ id: 'save', label: 'Save File', keywords: ['write'] },
|
|
6
|
+
{ id: 'close', disabled: true, label: 'Close Window' },
|
|
7
|
+
]
|
|
8
|
+
|
|
9
|
+
export function InlineExample() {
|
|
10
|
+
return (
|
|
11
|
+
<Command.Root
|
|
12
|
+
itemToDisabled={(item) => item.disabled}
|
|
13
|
+
itemToKeywords={(item) => item.keywords}
|
|
14
|
+
itemToString={(item) => item.label}
|
|
15
|
+
itemToValue={(item) => item.id}
|
|
16
|
+
items={items}
|
|
17
|
+
label="Command Menu"
|
|
18
|
+
onSelect={(item) => {
|
|
19
|
+
console.log(item.id)
|
|
20
|
+
}}
|
|
21
|
+
>
|
|
22
|
+
<Command.Input placeholder="Search commands..." />
|
|
23
|
+
<Command.List empty={<div>No results.</div>}>
|
|
24
|
+
{(item, state) => (
|
|
25
|
+
<div className={state.active ? 'is-active' : undefined}>{item.label}</div>
|
|
26
|
+
)}
|
|
27
|
+
</Command.List>
|
|
28
|
+
</Command.Root>
|
|
29
|
+
)
|
|
30
|
+
}
|
package/package.json
CHANGED
|
@@ -1,13 +1,53 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ark-cmdk",
|
|
3
|
-
"
|
|
4
|
-
"
|
|
5
|
-
"main": "index.js",
|
|
6
|
-
"scripts": {
|
|
7
|
-
"test": "echo \"Error: no test specified\" && exit 1"
|
|
8
|
-
},
|
|
9
|
-
"keywords": [],
|
|
10
|
-
"author": "",
|
|
3
|
+
"description": "A small, data-driven command menu for React 19 built on Ark UI",
|
|
4
|
+
"version": "0.1.0",
|
|
11
5
|
"license": "MIT",
|
|
12
|
-
"
|
|
13
|
-
|
|
6
|
+
"author": "Alec Larson",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "https://github.com/aleclarson/ark-cmdk.git"
|
|
10
|
+
},
|
|
11
|
+
"files": [
|
|
12
|
+
"dist",
|
|
13
|
+
"docs",
|
|
14
|
+
"examples"
|
|
15
|
+
],
|
|
16
|
+
"type": "module",
|
|
17
|
+
"exports": {
|
|
18
|
+
".": {
|
|
19
|
+
"types": "./dist/index.d.mts",
|
|
20
|
+
"import": "./dist/index.mjs"
|
|
21
|
+
}
|
|
22
|
+
},
|
|
23
|
+
"devDependencies": {
|
|
24
|
+
"@testing-library/react": "^16.3.2",
|
|
25
|
+
"@testing-library/user-event": "^14.6.1",
|
|
26
|
+
"@types/node": "^25.6.0",
|
|
27
|
+
"@types/react": "^19.2.14",
|
|
28
|
+
"@types/react-dom": "^19.2.3",
|
|
29
|
+
"jsdom": "^29.0.2",
|
|
30
|
+
"oxfmt": "^0.44.0",
|
|
31
|
+
"oxlint": "^1.59.0",
|
|
32
|
+
"radashi": "^12.7.2",
|
|
33
|
+
"react": "^19.2.5",
|
|
34
|
+
"react-dom": "^19.2.5",
|
|
35
|
+
"tsdown": "^0.21.7",
|
|
36
|
+
"tsnapi": "^0.1.1",
|
|
37
|
+
"typescript": "^6.0.2",
|
|
38
|
+
"vitest": "^4.1.4"
|
|
39
|
+
},
|
|
40
|
+
"peerDependencies": {
|
|
41
|
+
"@ark-ui/react": ">=5",
|
|
42
|
+
"react": ">=19"
|
|
43
|
+
},
|
|
44
|
+
"scripts": {
|
|
45
|
+
"dev": "tsdown --sourcemap --watch",
|
|
46
|
+
"build": "tsdown",
|
|
47
|
+
"format": "oxfmt .",
|
|
48
|
+
"lint": "oxlint src",
|
|
49
|
+
"typecheck": "tsc --noEmit && tsc -p test --noEmit",
|
|
50
|
+
"test": "vitest run",
|
|
51
|
+
"test:watch": "vitest"
|
|
52
|
+
}
|
|
53
|
+
}
|
package/readme.md
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
Coming soon...
|