Skip to content

Commit 95639ea

Browse files
committed
feat(command-bar): context api (input, select, etc.)
1 parent 4d8e6aa commit 95639ea

5 files changed

Lines changed: 134 additions & 22 deletions

File tree

app/components/CommandBar.vue

Lines changed: 68 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,28 @@
11
<template>
22
<Transition name="fade">
33
<div
4-
class="fixed inset-0 z-[1000] flex items-center justify-center bg-black/50 backdrop-blur-sm"
4+
class="fixed inset-0 z-[1000] flex items-start justify-center bg-black/50 backdrop-blur-sm"
55
v-show="show"
66
>
77
<div
8-
class="cmdbar-container flex items-center justify-center border border-border shadow-lg rounded-xl bg-bg p2 flex-col gap-2"
8+
class="cmdbar-container flex items-center justify-center border border-border shadow-lg rounded-xl bg-bg p2 flex-col gap-2 mt-5rem"
99
>
1010
<label for="command-input" class="sr-only">command-input</label>
1111

1212
<search class="relative w-xl h-12 flex items-center">
1313
<span class="absolute inset-is-4 text-fg-subtle font-mono pointer-events-none"> > </span>
1414
<input
1515
type="text"
16-
label="Enter command..."
1716
v-model="inputVal"
1817
id="command-input"
1918
ref="inputRef"
2019
class="w-full h-full px-4 pl-8 text-fg outline-none bg-bg-subtle border border-border rounded-md"
21-
placeholder="Enter command..."
20+
:placeholder="placeholderText"
2221
@keydown="handleKeydown"
2322
/>
2423
</search>
2524

26-
<div class="w-xl h-lg overflow-auto">
25+
<div class="w-xl max-h-lg overflow-auto" v-if="view.type != 'INPUT'">
2726
<div
2827
v-for="item in filteredCmdList"
2928
:key="item.id"
@@ -32,7 +31,7 @@
3231
'bg-bg-subtle': item.id === selected,
3332
'trigger-anim': item.id === triggeringId,
3433
}"
35-
@click="triggerCommand(item.id)"
34+
@click="onTrigger(item.id)"
3635
>
3736
<div class="text-fg">{{ item.name }}</div>
3837
<div class="text-fg-subtle text-sm">{{ item.description }}</div>
@@ -44,6 +43,27 @@
4443
</template>
4544

4645
<script setup lang="ts">
46+
const { t } = useI18n()
47+
48+
type ViewState =
49+
| { type: 'ROOT' }
50+
| { type: 'INPUT'; prompt: string; resolve: (val: string) => void }
51+
| { type: 'SELECT'; prompt: string; items: any[]; resolve: (val: any) => void }
52+
const view = ref<ViewState>({ type: 'ROOT' })
53+
54+
const cmdCtx: CommandContext = {
55+
async input(options) {
56+
return new Promise(resolve => {
57+
view.value = { type: 'INPUT', prompt: options.prompt, resolve }
58+
})
59+
},
60+
async select(options) {
61+
return new Promise(resolve => {
62+
view.value = { type: 'SELECT', prompt: options.prompt, items: options.items, resolve }
63+
})
64+
},
65+
}
66+
4767
const { commands } = useCommandRegistry()
4868
4969
const selected = shallowRef(commands.value[0]?.id || '')
@@ -54,13 +74,26 @@ const inputRef = useTemplateRef('inputRef')
5474
5575
const { focused: inputFocused } = useFocus(inputRef)
5676
77+
const placeholderText = computed(() => {
78+
if (view.value.type === 'INPUT' || view.value.type === 'SELECT') {
79+
return view.value.prompt
80+
}
81+
return t('command.placeholder')
82+
})
83+
5784
const filteredCmdList = computed(() => {
85+
if (view.value.type === 'INPUT') {
86+
return []
87+
}
88+
89+
const list = view.value.type === 'SELECT' ? view.value.items : commands.value
90+
5891
if (!inputVal.value) {
59-
return commands.value
92+
return list
6093
}
6194
const filter = inputVal.value.trim().toLowerCase()
62-
return commands.value.filter(
63-
item =>
95+
return list.filter(
96+
(item: any) =>
6497
item.name.toLowerCase().includes(filter) ||
6598
item.description?.toLowerCase().includes(filter) ||
6699
item.id.includes(filter),
@@ -84,6 +117,7 @@ function open() {
84117
inputVal.value = ''
85118
selected.value = commands.value[0]?.id || ''
86119
show.value = true
120+
view.value = { type: 'ROOT' }
87121
nextTick(focusInput)
88122
}
89123
@@ -101,20 +135,36 @@ function toggle() {
101135
}
102136
}
103137
104-
function triggerCommand(id: string) {
105-
const selectedItem = filteredCmdList.value.find(item => item.id === id)
106-
selectedItem?.handler?.({} as CommandContext)
138+
function onTrigger(id: string) {
107139
triggeringId.value = id
108-
setTimeout(() => {
140+
141+
if (view.value.type === 'ROOT') {
142+
const selectedItem = filteredCmdList.value.find((item: any) => item.id === id)
143+
selectedItem?.handler?.(cmdCtx)
144+
setTimeout(() => {
145+
triggeringId.value = ''
146+
if (view.value.type === 'ROOT') {
147+
close()
148+
}
149+
}, 100)
150+
} else if (view.value.type === 'INPUT') {
151+
view.value.resolve(inputVal.value)
152+
close()
153+
} else if (view.value.type === 'SELECT') {
154+
const selectedItem = filteredCmdList.value.find((item: any) => item.id === id)
155+
view.value.resolve(selectedItem)
109156
close()
110-
triggeringId.value = ''
111-
}, 100)
157+
}
112158
}
113159
114160
const handleKeydown = useThrottleFn((e: KeyboardEvent) => {
115-
if (!filteredCmdList.value.length) return
161+
if (view.value.type === 'INPUT' && e.key === 'Enter') {
162+
e.preventDefault()
163+
onTrigger('') // Trigger for input doesn't need ID
164+
return
165+
}
116166
117-
const currentIndex = filteredCmdList.value.findIndex(item => item.id === selected.value)
167+
const currentIndex = filteredCmdList.value.findIndex((item: any) => item.id === selected.value)
118168
119169
if (e.key === 'ArrowDown') {
120170
e.preventDefault()
@@ -127,7 +177,7 @@ const handleKeydown = useThrottleFn((e: KeyboardEvent) => {
127177
selected.value = filteredCmdList.value[prevIndex]?.id || ''
128178
} else if (e.key === 'Enter') {
129179
e.preventDefault()
130-
triggerCommand(selected.value)
180+
onTrigger(selected.value)
131181
} else if (e.key === 'Escape') {
132182
e.preventDefault()
133183
close()

app/components/InstallCommandTerminal.vue

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22
import type { JsrPackageInfo } from '#shared/types/jsr'
33
import type { PackageManagerId } from '~/utils/install-command'
44
5+
const { t } = useI18n()
6+
57
const props = defineProps<{
68
packageName: string
79
requestedVersion?: string | null
@@ -93,6 +95,37 @@ const copyRunCommand = (command?: string) => copyRun(getFullRunCommand(command))
9395
9496
const { copied: createCopied, copy: copyCreate } = useClipboard({ copiedDuring: 2000 })
9597
const copyCreateCommand = () => copyCreate(getFullCreateCommand())
98+
99+
registerScopedCommand({
100+
id: 'package:install',
101+
name: t('command.copy_install'),
102+
description: t('command.copy_install_desc'),
103+
handler: async () => {
104+
copyInstallCommand()
105+
},
106+
})
107+
108+
if (props.executableInfo?.hasExecutable) {
109+
registerScopedCommand({
110+
id: 'packages:copy-run',
111+
name: t('command.copy_run'),
112+
description: t('command.copy_run_desc'),
113+
handler: async () => {
114+
copyRunCommand()
115+
},
116+
})
117+
}
118+
119+
if (props.createPackageInfo) {
120+
registerScopedCommand({
121+
id: 'packages:copy-create',
122+
name: t('command.copy_create'),
123+
description: t('command.copy_create_desc'),
124+
handler: async () => {
125+
copyCreateCommand()
126+
},
127+
})
128+
}
96129
</script>
97130

98131
<template>

app/plugins/commands.ts

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,22 @@
11
export default defineNuxtPlugin(() => {
22
const { register } = useCommandRegistry()
3+
const router = useRouter()
4+
35
register({
4-
id: 'npmx:hello',
5-
name: 'Hello',
6-
description: 'Say hello to npmx',
6+
id: 'packages:search',
7+
name: 'Search Packages',
8+
description: 'Search for npm packages',
79
handler: async () => {
8-
console.log('Hello npmx!')
10+
const searchInput = document.querySelector<HTMLInputElement>(
11+
'input[type="search"], input[name="q"]',
12+
)
13+
14+
if (searchInput) {
15+
searchInput.focus()
16+
return
17+
}
18+
19+
router.push('/search')
920
},
1021
})
1122
})

i18n/locales/en.json

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -748,5 +748,14 @@
748748
"empty": "No organizations found",
749749
"view_all": "View all"
750750
}
751+
},
752+
"command": {
753+
"placeholder": "Enter command...",
754+
"copy_install": "Copy install command",
755+
"copy_install_desc": "Copy install command to clipboard",
756+
"copy_run": "Copy run command",
757+
"copy_run_desc": "Copy run command to clipboard",
758+
"copy_create": "Copy create command",
759+
"copy_create_desc": "Copy create command to clipboard"
751760
}
752761
}

i18n/locales/zh-CN.json

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -747,5 +747,14 @@
747747
"empty": "未找到组织",
748748
"view_all": "查看全部"
749749
}
750+
},
751+
"command": {
752+
"placeholder": "输入命令...",
753+
"copy_install": "复制安装命令",
754+
"copy_install_desc": "复制安装命令到剪贴板",
755+
"copy_run": "复制运行命令",
756+
"copy_run_desc": "复制运行命令到剪贴板",
757+
"copy_create": "复制创建命令",
758+
"copy_create_desc": "复制创建命令到剪贴板"
750759
}
751760
}

0 commit comments

Comments
 (0)