@choice-ui/command 0.0.3
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 +571 -0
- package/dist/index.cjs +1309 -0
- package/dist/index.d.cts +130 -0
- package/dist/index.d.ts +130 -0
- package/dist/index.js +1300 -0
- package/package.json +50 -0
- package/src/command-score.ts +171 -0
- package/src/command.tsx +482 -0
- package/src/components/command-divider.tsx +30 -0
- package/src/components/command-empty.tsx +30 -0
- package/src/components/command-footer.tsx +22 -0
- package/src/components/command-group.tsx +76 -0
- package/src/components/command-input.tsx +66 -0
- package/src/components/command-item.tsx +165 -0
- package/src/components/command-list.tsx +77 -0
- package/src/components/command-loading.tsx +30 -0
- package/src/components/command-tabs.tsx +20 -0
- package/src/components/command-value.tsx +23 -0
- package/src/components/index.ts +10 -0
- package/src/context/command-context.ts +5 -0
- package/src/context/create-command-context.ts +140 -0
- package/src/context/index.ts +2 -0
- package/src/hooks/index.ts +10 -0
- package/src/hooks/use-as-ref.ts +12 -0
- package/src/hooks/use-command-state.ts +18 -0
- package/src/hooks/use-command.ts +10 -0
- package/src/hooks/use-schedule-layout-effect.ts +19 -0
- package/src/hooks/use-value.ts +39 -0
- package/src/index.ts +31 -0
- package/src/store/index.ts +1 -0
- package/src/tv.ts +248 -0
- package/src/types.ts +84 -0
- package/src/utils/constants.ts +7 -0
- package/src/utils/dom.ts +19 -0
- package/src/utils/helpers.ts +45 -0
- package/src/utils/index.ts +3 -0
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import React from "react"
|
|
2
|
+
import { commandScore } from "../command-score"
|
|
3
|
+
import type { CommandProps, Context, State, Store } from "../types"
|
|
4
|
+
|
|
5
|
+
interface CreateCommandContextOptions {
|
|
6
|
+
allGroups: React.MutableRefObject<Map<string, Set<string>>>
|
|
7
|
+
allItems: React.MutableRefObject<Set<string>>
|
|
8
|
+
filterItems: () => void
|
|
9
|
+
ids: React.MutableRefObject<Map<string, { keywords?: string[]; value: string }>>
|
|
10
|
+
inputId: string
|
|
11
|
+
label?: string
|
|
12
|
+
labelId: string
|
|
13
|
+
listId: string
|
|
14
|
+
listInnerRef: React.MutableRefObject<HTMLDivElement | null>
|
|
15
|
+
propsRef: React.MutableRefObject<CommandProps>
|
|
16
|
+
schedule: (id: string | number, cb: () => void) => void
|
|
17
|
+
selectFirstItem: () => void
|
|
18
|
+
size?: "default" | "large"
|
|
19
|
+
sort: () => void
|
|
20
|
+
state: React.MutableRefObject<State>
|
|
21
|
+
store: Store
|
|
22
|
+
variant?: "default" | "dark"
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function createCommandContext(options: CreateCommandContextOptions): Context {
|
|
26
|
+
const {
|
|
27
|
+
allGroups,
|
|
28
|
+
allItems,
|
|
29
|
+
filterItems,
|
|
30
|
+
ids,
|
|
31
|
+
inputId,
|
|
32
|
+
label,
|
|
33
|
+
labelId,
|
|
34
|
+
listId,
|
|
35
|
+
listInnerRef,
|
|
36
|
+
propsRef,
|
|
37
|
+
schedule,
|
|
38
|
+
selectFirstItem,
|
|
39
|
+
size,
|
|
40
|
+
sort,
|
|
41
|
+
state,
|
|
42
|
+
store,
|
|
43
|
+
variant,
|
|
44
|
+
} = options
|
|
45
|
+
|
|
46
|
+
function score(value: string, keywords?: string[]) {
|
|
47
|
+
const filter =
|
|
48
|
+
propsRef.current?.filter ??
|
|
49
|
+
((value: string, search: string, keywords?: string[]) =>
|
|
50
|
+
commandScore(value, search, keywords))
|
|
51
|
+
return value ? filter(value, state.current.search, keywords) : 0
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return {
|
|
55
|
+
// Keep id → {value, keywords} mapping up-to-date
|
|
56
|
+
value: (id, value, keywords) => {
|
|
57
|
+
if (value !== ids.current.get(id)?.value) {
|
|
58
|
+
ids.current.set(id, { value: value || "", keywords })
|
|
59
|
+
state.current.filtered.items.set(id, score(value || "", keywords))
|
|
60
|
+
schedule(2, () => {
|
|
61
|
+
sort()
|
|
62
|
+
store.emit()
|
|
63
|
+
})
|
|
64
|
+
}
|
|
65
|
+
},
|
|
66
|
+
// Track item lifecycle (mount, unmount)
|
|
67
|
+
item: (id, groupId) => {
|
|
68
|
+
allItems.current.add(id)
|
|
69
|
+
|
|
70
|
+
// Track this item within the group
|
|
71
|
+
if (groupId) {
|
|
72
|
+
if (!allGroups.current.has(groupId)) {
|
|
73
|
+
allGroups.current.set(groupId, new Set([id]))
|
|
74
|
+
} else {
|
|
75
|
+
allGroups.current.get(groupId)?.add(id)
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Batch this, multiple items can mount in one pass
|
|
80
|
+
// and we should not be filtering/sorting/emitting each time
|
|
81
|
+
schedule(3, () => {
|
|
82
|
+
filterItems()
|
|
83
|
+
sort()
|
|
84
|
+
|
|
85
|
+
// Could be initial mount, select the first item if none already selected
|
|
86
|
+
if (!state.current.value) {
|
|
87
|
+
selectFirstItem()
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
store.emit()
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
return () => {
|
|
94
|
+
ids.current.delete(id)
|
|
95
|
+
allItems.current.delete(id)
|
|
96
|
+
state.current.filtered.items.delete(id)
|
|
97
|
+
|
|
98
|
+
// Batch this, multiple items could be removed in one pass
|
|
99
|
+
schedule(4, () => {
|
|
100
|
+
filterItems()
|
|
101
|
+
|
|
102
|
+
// The item removed have been the selected one,
|
|
103
|
+
// so selection should be moved to the first
|
|
104
|
+
const ITEM_SELECTOR = `[role="option"]`
|
|
105
|
+
const selectedItem = listInnerRef.current?.querySelector(
|
|
106
|
+
`${ITEM_SELECTOR}[aria-selected="true"]`,
|
|
107
|
+
)
|
|
108
|
+
if (selectedItem?.getAttribute("id") === id) selectFirstItem()
|
|
109
|
+
|
|
110
|
+
store.emit()
|
|
111
|
+
})
|
|
112
|
+
}
|
|
113
|
+
},
|
|
114
|
+
// Track group lifecycle (mount, unmount)
|
|
115
|
+
group: (id) => {
|
|
116
|
+
if (!allGroups.current.has(id)) {
|
|
117
|
+
allGroups.current.set(id, new Set())
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return () => {
|
|
121
|
+
ids.current.delete(id)
|
|
122
|
+
allGroups.current.delete(id)
|
|
123
|
+
}
|
|
124
|
+
},
|
|
125
|
+
filter: () => {
|
|
126
|
+
return propsRef.current.shouldFilter !== false
|
|
127
|
+
},
|
|
128
|
+
label: label || propsRef.current["aria-label"],
|
|
129
|
+
getDisablePointerSelection: () => {
|
|
130
|
+
return propsRef.current.disablePointerSelection ?? false
|
|
131
|
+
},
|
|
132
|
+
listId,
|
|
133
|
+
inputId,
|
|
134
|
+
labelId,
|
|
135
|
+
listInnerRef,
|
|
136
|
+
store,
|
|
137
|
+
size,
|
|
138
|
+
variant,
|
|
139
|
+
}
|
|
140
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export * from "./use-as-ref"
|
|
2
|
+
export * from "./use-command"
|
|
3
|
+
export * from "./use-command-state"
|
|
4
|
+
export * from "./use-schedule-layout-effect"
|
|
5
|
+
export * from "./use-value"
|
|
6
|
+
|
|
7
|
+
import React from "react"
|
|
8
|
+
import { Group } from "../types"
|
|
9
|
+
|
|
10
|
+
export const GroupContext = React.createContext<Group | undefined>(undefined)
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import React, { useRef } from "react"
|
|
2
|
+
import { useIsomorphicLayoutEffect } from "usehooks-ts"
|
|
3
|
+
|
|
4
|
+
export function useAsRef<T>(data: T): React.MutableRefObject<T> {
|
|
5
|
+
const ref = useRef<T>(data)
|
|
6
|
+
|
|
7
|
+
useIsomorphicLayoutEffect(() => {
|
|
8
|
+
ref.current = data
|
|
9
|
+
})
|
|
10
|
+
|
|
11
|
+
return ref
|
|
12
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import React from "react"
|
|
2
|
+
import { State } from "../types"
|
|
3
|
+
import { StoreContext } from "../context"
|
|
4
|
+
|
|
5
|
+
export const useStore = () => {
|
|
6
|
+
const store = React.useContext(StoreContext)
|
|
7
|
+
if (!store) {
|
|
8
|
+
throw new Error("useStore must be used within a Command component")
|
|
9
|
+
}
|
|
10
|
+
return store
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/** Run a selector against the store state. */
|
|
14
|
+
export function useCommandState<T>(selector: (state: State) => T): T {
|
|
15
|
+
const store = useStore()
|
|
16
|
+
const cb = () => selector(store.snapshot())
|
|
17
|
+
return React.useSyncExternalStore(store.subscribe, cb, cb)
|
|
18
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import React from "react"
|
|
2
|
+
import { CommandContext } from "../context"
|
|
3
|
+
|
|
4
|
+
export const useCommand = () => {
|
|
5
|
+
const context = React.useContext(CommandContext)
|
|
6
|
+
if (!context) {
|
|
7
|
+
throw new Error("useCommand must be used within a Command component")
|
|
8
|
+
}
|
|
9
|
+
return context
|
|
10
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { useCallback, useState } from "react"
|
|
2
|
+
import { useIsomorphicLayoutEffect } from "usehooks-ts"
|
|
3
|
+
import { useLazyRef } from "@choice-ui/shared"
|
|
4
|
+
|
|
5
|
+
/** 在下一个 layout effect 周期内以命令式方式运行函数。 */
|
|
6
|
+
export const useScheduleLayoutEffect = () => {
|
|
7
|
+
const [updateCount, setUpdateCount] = useState(0)
|
|
8
|
+
const fns = useLazyRef(() => new Map<string | number, () => void>())
|
|
9
|
+
|
|
10
|
+
useIsomorphicLayoutEffect(() => {
|
|
11
|
+
fns.current.forEach((f) => f())
|
|
12
|
+
fns.current = new Map()
|
|
13
|
+
}, [updateCount])
|
|
14
|
+
|
|
15
|
+
return useCallback((id: string | number, cb: () => void) => {
|
|
16
|
+
fns.current.set(id, cb)
|
|
17
|
+
setUpdateCount((prev) => prev + 1)
|
|
18
|
+
}, [])
|
|
19
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import React, { useRef } from "react"
|
|
2
|
+
import { VALUE_ATTR, useLayoutEffect } from "../utils"
|
|
3
|
+
import { useCommand } from "./use-command"
|
|
4
|
+
|
|
5
|
+
export function useValue(
|
|
6
|
+
id: string,
|
|
7
|
+
ref: React.RefObject<HTMLElement>,
|
|
8
|
+
deps: (string | React.ReactNode | React.RefObject<HTMLElement>)[],
|
|
9
|
+
aliases: string[] = [],
|
|
10
|
+
) {
|
|
11
|
+
const valueRef = useRef<string>()
|
|
12
|
+
const context = useCommand()
|
|
13
|
+
|
|
14
|
+
useLayoutEffect(() => {
|
|
15
|
+
const value = (() => {
|
|
16
|
+
for (const part of deps) {
|
|
17
|
+
if (typeof part === "string") {
|
|
18
|
+
return part.trim()
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
if (typeof part === "object" && part && "current" in part) {
|
|
22
|
+
if (part.current) {
|
|
23
|
+
return part.current.textContent?.trim()
|
|
24
|
+
}
|
|
25
|
+
return valueRef.current
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
return undefined // 关键:和原始实现一致,找不到值时返回undefined
|
|
29
|
+
})()
|
|
30
|
+
|
|
31
|
+
const keywords = aliases.map((alias) => alias.trim())
|
|
32
|
+
|
|
33
|
+
context.value(id, value || "", keywords)
|
|
34
|
+
ref.current?.setAttribute(VALUE_ATTR, value || "")
|
|
35
|
+
valueRef.current = value
|
|
36
|
+
}) // 和原始实现一致:故意不使用依赖数组
|
|
37
|
+
|
|
38
|
+
return valueRef
|
|
39
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
export { Command as CommandRoot, defaultFilter } from "./command"
|
|
2
|
+
export { useCommandState } from "./hooks"
|
|
3
|
+
|
|
4
|
+
import { Command as CommandRoot } from "./command"
|
|
5
|
+
import { TabItem } from "@choice-ui/tabs"
|
|
6
|
+
import {
|
|
7
|
+
CommandDivider,
|
|
8
|
+
CommandEmpty,
|
|
9
|
+
CommandFooter,
|
|
10
|
+
CommandGroup,
|
|
11
|
+
CommandInput,
|
|
12
|
+
CommandItem,
|
|
13
|
+
CommandList,
|
|
14
|
+
CommandLoading,
|
|
15
|
+
CommandTabs,
|
|
16
|
+
CommandValue,
|
|
17
|
+
} from "./components"
|
|
18
|
+
|
|
19
|
+
export const Command = Object.assign(CommandRoot, {
|
|
20
|
+
Empty: CommandEmpty,
|
|
21
|
+
Footer: CommandFooter,
|
|
22
|
+
Group: CommandGroup,
|
|
23
|
+
Input: CommandInput,
|
|
24
|
+
Item: CommandItem,
|
|
25
|
+
List: CommandList,
|
|
26
|
+
Loading: CommandLoading,
|
|
27
|
+
Divider: CommandDivider,
|
|
28
|
+
Value: CommandValue,
|
|
29
|
+
Tabs: CommandTabs,
|
|
30
|
+
TabItem: TabItem,
|
|
31
|
+
})
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
// Store is now created inline in command.tsx, following original cmdk pattern
|
package/src/tv.ts
ADDED
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
import { tcv } from "@choice-ui/shared"
|
|
2
|
+
|
|
3
|
+
export const commandTv = tcv({
|
|
4
|
+
slots: {
|
|
5
|
+
root: "flex h-full w-full flex-col overflow-hidden",
|
|
6
|
+
divider: "my-2 h-px",
|
|
7
|
+
},
|
|
8
|
+
variants: {
|
|
9
|
+
variant: {
|
|
10
|
+
default: {
|
|
11
|
+
root: "bg-default-background text-default-foreground",
|
|
12
|
+
divider: "bg-default-boundary",
|
|
13
|
+
},
|
|
14
|
+
dark: {
|
|
15
|
+
root: "bg-menu-background text-white",
|
|
16
|
+
divider: "bg-menu-boundary",
|
|
17
|
+
},
|
|
18
|
+
},
|
|
19
|
+
},
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
export const commandInputTv = tcv({
|
|
23
|
+
slots: {
|
|
24
|
+
root: "m-2 flex items-center",
|
|
25
|
+
input: "w-full rounded-lg",
|
|
26
|
+
},
|
|
27
|
+
variants: {
|
|
28
|
+
size: {
|
|
29
|
+
default: {
|
|
30
|
+
input: "text-body-medium h-8 px-2",
|
|
31
|
+
},
|
|
32
|
+
large: {
|
|
33
|
+
input: "leading-lg tracking-lg h-10 px-4 text-body-large",
|
|
34
|
+
},
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
|
+
defaultVariants: {
|
|
38
|
+
size: "default",
|
|
39
|
+
},
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
export const commandListTv = tcv({
|
|
43
|
+
slots: {
|
|
44
|
+
root: "px-2 pb-2",
|
|
45
|
+
content: "flex flex-col",
|
|
46
|
+
},
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
export const commandGroupTv = tcv({
|
|
50
|
+
slots: {
|
|
51
|
+
root: "flex flex-col gap-1 not-first:mt-4",
|
|
52
|
+
heading: "text-body-medium px-2",
|
|
53
|
+
},
|
|
54
|
+
variants: {
|
|
55
|
+
variant: {
|
|
56
|
+
default: {
|
|
57
|
+
heading: "text-secondary-foreground",
|
|
58
|
+
},
|
|
59
|
+
dark: {
|
|
60
|
+
heading: "text-white/50",
|
|
61
|
+
},
|
|
62
|
+
},
|
|
63
|
+
},
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
export const commandItemTv = tcv({
|
|
67
|
+
slots: {
|
|
68
|
+
root: ["group/item relative flex items-center rounded-lg select-none", "focus:outline-none"],
|
|
69
|
+
icon: "flex flex-shrink-0 items-center justify-center rounded-md",
|
|
70
|
+
value: "flex-1 truncate",
|
|
71
|
+
shortcut: "text-secondary-foreground",
|
|
72
|
+
},
|
|
73
|
+
variants: {
|
|
74
|
+
variant: {
|
|
75
|
+
default: {},
|
|
76
|
+
dark: {
|
|
77
|
+
root: "text-white",
|
|
78
|
+
},
|
|
79
|
+
},
|
|
80
|
+
size: {
|
|
81
|
+
default: {
|
|
82
|
+
root: "text-body-medium min-h-8 p-1",
|
|
83
|
+
icon: "h-6 min-w-6",
|
|
84
|
+
},
|
|
85
|
+
large: {
|
|
86
|
+
root: "leading-lg tracking-lg min-h-10 p-2 text-body-large",
|
|
87
|
+
icon: "h-6 min-w-6",
|
|
88
|
+
},
|
|
89
|
+
},
|
|
90
|
+
hasPrefix: {
|
|
91
|
+
true: "",
|
|
92
|
+
false: "",
|
|
93
|
+
},
|
|
94
|
+
hasSuffix: {
|
|
95
|
+
true: "",
|
|
96
|
+
false: "",
|
|
97
|
+
},
|
|
98
|
+
selected: {
|
|
99
|
+
true: {},
|
|
100
|
+
false: {},
|
|
101
|
+
},
|
|
102
|
+
disabled: {
|
|
103
|
+
true: {
|
|
104
|
+
root: "pointer-events-none",
|
|
105
|
+
},
|
|
106
|
+
},
|
|
107
|
+
},
|
|
108
|
+
compoundVariants: [
|
|
109
|
+
{
|
|
110
|
+
hasPrefix: true,
|
|
111
|
+
size: "default",
|
|
112
|
+
class: {
|
|
113
|
+
root: "gap-1 pl-1",
|
|
114
|
+
},
|
|
115
|
+
},
|
|
116
|
+
{
|
|
117
|
+
hasPrefix: false,
|
|
118
|
+
size: "default",
|
|
119
|
+
class: {
|
|
120
|
+
root: "pl-2",
|
|
121
|
+
},
|
|
122
|
+
},
|
|
123
|
+
{
|
|
124
|
+
hasSuffix: true,
|
|
125
|
+
size: "default",
|
|
126
|
+
class: {
|
|
127
|
+
root: "gap-1 pr-1",
|
|
128
|
+
},
|
|
129
|
+
},
|
|
130
|
+
{
|
|
131
|
+
hasSuffix: false,
|
|
132
|
+
size: "default",
|
|
133
|
+
class: {
|
|
134
|
+
root: "pr-2",
|
|
135
|
+
},
|
|
136
|
+
},
|
|
137
|
+
// large
|
|
138
|
+
{
|
|
139
|
+
hasPrefix: true,
|
|
140
|
+
size: "large",
|
|
141
|
+
class: {
|
|
142
|
+
root: "gap-2 pl-2",
|
|
143
|
+
},
|
|
144
|
+
},
|
|
145
|
+
{
|
|
146
|
+
hasPrefix: false,
|
|
147
|
+
size: "large",
|
|
148
|
+
class: {
|
|
149
|
+
root: "pl-4",
|
|
150
|
+
},
|
|
151
|
+
},
|
|
152
|
+
{
|
|
153
|
+
hasSuffix: true,
|
|
154
|
+
size: "large",
|
|
155
|
+
class: {
|
|
156
|
+
root: "gap-2 pr-2",
|
|
157
|
+
},
|
|
158
|
+
},
|
|
159
|
+
{
|
|
160
|
+
hasSuffix: false,
|
|
161
|
+
size: "large",
|
|
162
|
+
class: {
|
|
163
|
+
root: "pr-4",
|
|
164
|
+
},
|
|
165
|
+
},
|
|
166
|
+
{
|
|
167
|
+
variant: "default",
|
|
168
|
+
selected: true,
|
|
169
|
+
class: {
|
|
170
|
+
root: "bg-secondary-background",
|
|
171
|
+
},
|
|
172
|
+
},
|
|
173
|
+
{
|
|
174
|
+
variant: "dark",
|
|
175
|
+
selected: true,
|
|
176
|
+
class: {
|
|
177
|
+
root: "bg-gray-700",
|
|
178
|
+
},
|
|
179
|
+
},
|
|
180
|
+
{
|
|
181
|
+
variant: "default",
|
|
182
|
+
disabled: true,
|
|
183
|
+
class: {
|
|
184
|
+
root: "text-secondary-foreground",
|
|
185
|
+
},
|
|
186
|
+
},
|
|
187
|
+
{
|
|
188
|
+
variant: "dark",
|
|
189
|
+
disabled: true,
|
|
190
|
+
class: {
|
|
191
|
+
root: "text-white/50",
|
|
192
|
+
},
|
|
193
|
+
},
|
|
194
|
+
],
|
|
195
|
+
defaultVariants: {
|
|
196
|
+
size: "default",
|
|
197
|
+
hasPrefix: false,
|
|
198
|
+
hasSuffix: false,
|
|
199
|
+
variant: "default",
|
|
200
|
+
},
|
|
201
|
+
})
|
|
202
|
+
|
|
203
|
+
export const commandFooterTv = tcv({
|
|
204
|
+
slots: {
|
|
205
|
+
root: "flex h-10 items-center justify-between border-t px-2",
|
|
206
|
+
},
|
|
207
|
+
variants: {
|
|
208
|
+
variant: {
|
|
209
|
+
default: {
|
|
210
|
+
root: "border-default-boundary",
|
|
211
|
+
},
|
|
212
|
+
dark: {
|
|
213
|
+
root: "border-menu-boundary",
|
|
214
|
+
},
|
|
215
|
+
},
|
|
216
|
+
},
|
|
217
|
+
defaultVariants: {
|
|
218
|
+
variant: "default",
|
|
219
|
+
},
|
|
220
|
+
})
|
|
221
|
+
|
|
222
|
+
export const commandTabsTv = tcv({
|
|
223
|
+
slots: {
|
|
224
|
+
tabs: "mx-2 mb-2",
|
|
225
|
+
},
|
|
226
|
+
})
|
|
227
|
+
|
|
228
|
+
export const commandEmptyTv = tcv({
|
|
229
|
+
slots: {
|
|
230
|
+
root: "py-6 text-center",
|
|
231
|
+
},
|
|
232
|
+
variants: {
|
|
233
|
+
variant: {
|
|
234
|
+
default: {
|
|
235
|
+
root: "text-secondary-foreground",
|
|
236
|
+
},
|
|
237
|
+
dark: {
|
|
238
|
+
root: "text-white/50",
|
|
239
|
+
},
|
|
240
|
+
},
|
|
241
|
+
},
|
|
242
|
+
})
|
|
243
|
+
|
|
244
|
+
export const commandLoadingTv = tcv({
|
|
245
|
+
slots: {
|
|
246
|
+
root: "flex items-center justify-center py-6 text-center",
|
|
247
|
+
},
|
|
248
|
+
})
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import React from "react"
|
|
2
|
+
|
|
3
|
+
export type CommandFilter = (value: string, search: string, keywords?: string[]) => number
|
|
4
|
+
|
|
5
|
+
export interface CommandProps extends Omit<React.HTMLAttributes<HTMLDivElement>, "onChange"> {
|
|
6
|
+
/**
|
|
7
|
+
* Optional default item value when it is initially rendered.
|
|
8
|
+
*/
|
|
9
|
+
defaultValue?: string
|
|
10
|
+
/**
|
|
11
|
+
* Optionally set to `true` to disable selection via pointer events.
|
|
12
|
+
*/
|
|
13
|
+
disablePointerSelection?: boolean
|
|
14
|
+
/**
|
|
15
|
+
* Custom filter function for whether each command menu item should matches the given search query.
|
|
16
|
+
* It should return a number between 0 and 1, with 1 being the best match and 0 being hidden entirely.
|
|
17
|
+
* By default, uses the `command-score` library.
|
|
18
|
+
*/
|
|
19
|
+
filter?: CommandFilter
|
|
20
|
+
/**
|
|
21
|
+
* Accessible label for this command menu. Not shown visibly.
|
|
22
|
+
*/
|
|
23
|
+
label?: string
|
|
24
|
+
/**
|
|
25
|
+
* Optionally set to `true` to turn on looping around when using the arrow keys.
|
|
26
|
+
*/
|
|
27
|
+
loop?: boolean
|
|
28
|
+
/**
|
|
29
|
+
* Event handler called when the selected item of the menu changes.
|
|
30
|
+
*/
|
|
31
|
+
onChange?: (value: string) => void
|
|
32
|
+
/**
|
|
33
|
+
* Optionally set to `false` to turn off the automatic filtering and sorting.
|
|
34
|
+
* If `false`, you must conditionally render valid items based on the search query yourself.
|
|
35
|
+
*/
|
|
36
|
+
shouldFilter?: boolean
|
|
37
|
+
size?: "default" | "large"
|
|
38
|
+
/**
|
|
39
|
+
* Optional controlled state of the selected command menu item.
|
|
40
|
+
*/
|
|
41
|
+
value?: string
|
|
42
|
+
variant?: "default" | "dark"
|
|
43
|
+
/**
|
|
44
|
+
* Set to `false` to disable ctrl+n/j/p/k shortcuts. Defaults to `true`.
|
|
45
|
+
*/
|
|
46
|
+
vimBindings?: boolean
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export type Context = {
|
|
50
|
+
filter: () => boolean
|
|
51
|
+
getDisablePointerSelection: () => boolean
|
|
52
|
+
group: (id: string) => () => void
|
|
53
|
+
inputId: string
|
|
54
|
+
item: (id: string, groupId?: string) => () => void
|
|
55
|
+
label?: string
|
|
56
|
+
labelId: string
|
|
57
|
+
// Ids
|
|
58
|
+
listId: string
|
|
59
|
+
// Refs
|
|
60
|
+
listInnerRef: React.MutableRefObject<HTMLDivElement | null>
|
|
61
|
+
size?: "default" | "large"
|
|
62
|
+
store: Store
|
|
63
|
+
value: (id: string, value?: string, keywords?: string[]) => void
|
|
64
|
+
variant?: "default" | "dark"
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export type State = {
|
|
68
|
+
filtered: { count: number; groups: Set<string>; items: Map<string, number> }
|
|
69
|
+
search: string
|
|
70
|
+
selectedItemId?: string
|
|
71
|
+
value: string
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export type Store = {
|
|
75
|
+
emit: () => void
|
|
76
|
+
setState: <K extends keyof State>(key: K, value: State[K], opts?: unknown) => void
|
|
77
|
+
snapshot: () => State
|
|
78
|
+
subscribe: (callback: () => void) => () => void
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export type Group = {
|
|
82
|
+
forceMount?: boolean
|
|
83
|
+
id: string
|
|
84
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export const GROUP_SELECTOR = `[role="presentation"]`
|
|
2
|
+
export const GROUP_ITEMS_SELECTOR = `[role="group"]`
|
|
3
|
+
export const GROUP_HEADING_SELECTOR = `[aria-hidden="true"]`
|
|
4
|
+
export const ITEM_SELECTOR = `[role="option"]`
|
|
5
|
+
export const VALID_ITEM_SELECTOR = `${ITEM_SELECTOR}:not([aria-disabled="true"])`
|
|
6
|
+
export const SELECT_EVENT = `cmdk-item-select`
|
|
7
|
+
export const VALUE_ATTR = `data-value`
|
package/src/utils/dom.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export function findNextSibling(el: Element, selector: string): Element | null {
|
|
2
|
+
let sibling = el.nextElementSibling
|
|
3
|
+
|
|
4
|
+
while (sibling) {
|
|
5
|
+
if (sibling.matches(selector)) return sibling
|
|
6
|
+
sibling = sibling.nextElementSibling
|
|
7
|
+
}
|
|
8
|
+
return null
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function findPreviousSibling(el: Element, selector: string): Element | null {
|
|
12
|
+
let sibling = el.previousElementSibling
|
|
13
|
+
|
|
14
|
+
while (sibling) {
|
|
15
|
+
if (sibling.matches(selector)) return sibling
|
|
16
|
+
sibling = sibling.previousElementSibling
|
|
17
|
+
}
|
|
18
|
+
return null
|
|
19
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import React from "react"
|
|
2
|
+
|
|
3
|
+
export const useLayoutEffect =
|
|
4
|
+
typeof window === "undefined" ? React.useEffect : React.useLayoutEffect
|
|
5
|
+
|
|
6
|
+
export function renderChildren(children: React.ReactElement): React.ReactNode {
|
|
7
|
+
const childrenType = children.type
|
|
8
|
+
|
|
9
|
+
// The children is a function component or class component
|
|
10
|
+
if (typeof childrenType === "function") {
|
|
11
|
+
// Try to call as function component first
|
|
12
|
+
try {
|
|
13
|
+
return (childrenType as React.FunctionComponent)(children.props)
|
|
14
|
+
} catch {
|
|
15
|
+
// If it fails, it might be a class component, return the original element
|
|
16
|
+
return children
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// The children is a component with `forwardRef`
|
|
21
|
+
if (typeof childrenType === "object" && childrenType !== null && "render" in childrenType) {
|
|
22
|
+
const forwardRefComponent = childrenType as {
|
|
23
|
+
render: (props: unknown, ref: unknown) => React.ReactNode
|
|
24
|
+
}
|
|
25
|
+
return forwardRefComponent.render(children.props, null)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// It's a string, boolean, etc.
|
|
29
|
+
return children
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function SlottableWithNestedChildren(
|
|
33
|
+
{ asChild, children }: { asChild?: boolean; children?: React.ReactNode },
|
|
34
|
+
render: (child: React.ReactNode) => JSX.Element,
|
|
35
|
+
) {
|
|
36
|
+
if (asChild && React.isValidElement(children)) {
|
|
37
|
+
const renderedChild = renderChildren(children)
|
|
38
|
+
if (React.isValidElement(renderedChild)) {
|
|
39
|
+
const childProps = children.props
|
|
40
|
+
// Use a safer approach that avoids ref type issues
|
|
41
|
+
return React.cloneElement(renderedChild, {}, render(childProps.children))
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return render(children)
|
|
45
|
+
}
|