Skip to content

Commit cc11944

Browse files
committed
WIP(command-bar): basic api
1 parent 7ada4b1 commit cc11944

3 files changed

Lines changed: 100 additions & 33 deletions

File tree

app/components/CommandBar.vue

Lines changed: 8 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -44,34 +44,9 @@
4444
</template>
4545

4646
<script setup lang="ts">
47-
const cmdList = [
48-
{
49-
id: 'package:search',
50-
name: 'Search packages',
51-
description: 'Search for packages',
52-
handler: () => {},
53-
},
54-
{
55-
id: 'org:search',
56-
name: 'Search organizations',
57-
description: 'Search for organizations',
58-
handler: () => {},
59-
},
60-
{
61-
id: 'package:like',
62-
name: 'Like this',
63-
description: 'Like this package',
64-
handler: () => {},
65-
},
66-
{
67-
id: 'package:install',
68-
name: 'Copy install command',
69-
description: 'Copy install command to clipboard',
70-
handler: () => {},
71-
},
72-
]
47+
const { commands } = useCommandRegistry()
7348
74-
const selected = shallowRef(cmdList[0]?.id || '')
49+
const selected = shallowRef(commands.value[0]?.id || '')
7550
const inputVal = shallowRef('')
7651
const show = shallowRef(false)
7752
const triggeringId = shallowRef('')
@@ -81,13 +56,13 @@ const { focused: inputFocused } = useFocus(inputRef)
8156
8257
const filteredCmdList = computed(() => {
8358
if (!inputVal.value) {
84-
return cmdList
59+
return commands.value
8560
}
8661
const filter = inputVal.value.trim().toLowerCase()
87-
return cmdList.filter(
62+
return commands.value.filter(
8863
item =>
8964
item.name.toLowerCase().includes(filter) ||
90-
item.description.toLowerCase().includes(filter) ||
65+
item.description?.toLowerCase().includes(filter) ||
9166
item.id.includes(filter),
9267
)
9368
})
@@ -107,14 +82,14 @@ function focusInput() {
10782
10883
function open() {
10984
inputVal.value = ''
110-
selected.value = cmdList[0]?.id || ''
85+
selected.value = commands.value[0]?.id || ''
11186
show.value = true
11287
nextTick(focusInput)
11388
}
11489
11590
function close() {
11691
inputVal.value = ''
117-
selected.value = cmdList[0]?.id || ''
92+
selected.value = commands.value[0]?.id || ''
11893
show.value = false
11994
}
12095
@@ -128,7 +103,7 @@ function toggle() {
128103
129104
function triggerCommand(id: string) {
130105
const selectedItem = filteredCmdList.value.find(item => item.id === id)
131-
selectedItem?.handler?.()
106+
selectedItem?.handler?.({} as CommandContext)
132107
triggeringId.value = id
133108
setTimeout(() => {
134109
close()
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
import { computed } from 'vue'
2+
3+
export interface Command {
4+
id: string
5+
name: string
6+
description: string | undefined
7+
handler?: (ctx: CommandContext) => Promise<void>
8+
}
9+
10+
export interface CommandContext {
11+
input: (options: CommandInputOptions) => Promise<string | undefined>
12+
select: <T>(options: CommandSelectOptions<T>) => Promise<T | undefined>
13+
}
14+
15+
export interface CommandInputOptions {
16+
prompt: string
17+
}
18+
19+
export interface CommandSelectOptions<T> {
20+
prompt: string
21+
items: T[]
22+
}
23+
24+
/**
25+
* Composable for global command registry.
26+
* @public
27+
*/
28+
export const useCommandRegistry = () => {
29+
const commands = useState<Map<string, Command>>('commands', () => new Map())
30+
31+
const register = (command: Command) => {
32+
const serverCommand = {
33+
...command,
34+
handler: undefined,
35+
}
36+
if (import.meta.server) {
37+
commands.value.set(command.id, serverCommand)
38+
} else {
39+
commands.value.set(command.id, command)
40+
}
41+
return () => {
42+
commands.value.delete(command.id)
43+
}
44+
}
45+
46+
return {
47+
register,
48+
commands: computed(() => Array.from(commands.value.values())),
49+
}
50+
}
51+
52+
/**
53+
* Registers a global command.
54+
* @public
55+
*/
56+
export const registerGlobalCommand = (command: Command) => {
57+
const { register } = useCommandRegistry()
58+
return register(command)
59+
}
60+
61+
/**
62+
* Registers a command bound to the current component's lifecycle.
63+
*
64+
* The command is automatically unregistered when the component unmounts.
65+
* Use this to register commands that rely on local component state (context)
66+
* via closure capture.
67+
*
68+
* @public
69+
*/
70+
export const registerScopedCommand = (command: Command) => {
71+
const { register } = useCommandRegistry()
72+
let unregister: () => void
73+
74+
onMounted(() => {
75+
unregister = register(command)
76+
})
77+
78+
onUnmounted(() => {
79+
unregister()
80+
})
81+
}

app/plugins/commands.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
export default defineNuxtPlugin(() => {
2+
const { register } = useCommandRegistry()
3+
register({
4+
id: 'npmx:hello',
5+
name: 'Hello',
6+
description: 'Say hello to npmx',
7+
handler: async () => {
8+
console.log('Hello npmx!')
9+
},
10+
})
11+
})

0 commit comments

Comments
 (0)