Skip to content

Commit 727cc82

Browse files
committed
WIP(command-bar): trigger handlers
1 parent 79475b7 commit 727cc82

2 files changed

Lines changed: 135 additions & 36 deletions

File tree

app/app.vue

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ const localeMap = locales.value.reduce(
1919
{} as Record<string, Directions>,
2020
)
2121
22+
const commandBarRef = useTemplateRef('commandBarRef')
23+
2224
useHead({
2325
htmlAttrs: {
2426
lang: () => locale.value,
@@ -37,6 +39,11 @@ if (import.meta.server) {
3739
function handleGlobalKeydown(e: KeyboardEvent) {
3840
const target = e.target as HTMLElement
3941
42+
if (e.key === 'k' && (e.metaKey || e.ctrlKey)) {
43+
e.preventDefault()
44+
commandBarRef.value?.toggle()
45+
}
46+
4047
const isEditableTarget =
4148
target.tagName === 'INPUT' || target.tagName === 'TEXTAREA' || target.isContentEditable
4249
@@ -69,7 +76,7 @@ if (import.meta.client) {
6976
<template>
7077
<div class="min-h-screen flex flex-col bg-bg text-fg">
7178
<a href="#main-content" class="skip-link font-mono">{{ $t('common.skip_link') }}</a>
72-
<CommandBar />
79+
<CommandBar ref="commandBarRef" />
7380

7481
<AppHeader :show-logo="!isHomepage" />
7582

app/components/CommandBar.vue

Lines changed: 127 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,60 +1,76 @@
11
<template>
2-
<div class="fixed inset-0 z-[1000] flex items-center justify-center bg-black/50 backdrop-blur-sm">
2+
<Transition name="fade">
33
<div
4-
class="cmdbar-container flex items-center justify-center border border-border shadow-lg rounded-xl bg-bg p2 flex-col gap-2"
4+
class="fixed inset-0 z-[1000] flex items-center justify-center bg-black/50 backdrop-blur-sm"
5+
v-show="show"
56
>
6-
<label for="command-input" class="sr-only">command-input</label>
7-
8-
<input
9-
type="text"
10-
label="Enter command..."
11-
v-model="inputVal"
12-
id="command-input"
13-
class="w-96 h-12 px-4 text-fg outline-none bg-bg-subtle border border-border rounded-md"
14-
placeholder="Enter command..."
15-
/>
16-
17-
<div class="w-96 h-48 overflow-auto">
18-
<div
19-
v-for="item in filteredCmdList"
20-
:key="item.id"
21-
class="flex-col items-center justify-between px-4 py-2 not-first:mt-2 hover:bg-bg-subtle select-none cursor-pointer rounded-md"
22-
:class="{ 'bg-bg-subtle': item.id === selected }"
23-
>
24-
<div class="text-fg">{{ item.name }}</div>
25-
<div class="text-fg-subtle">{{ item.description }}</div>
7+
<div
8+
class="cmdbar-container flex items-center justify-center border border-border shadow-lg rounded-xl bg-bg p2 flex-col gap-2"
9+
>
10+
<label for="command-input" class="sr-only">command-input</label>
11+
12+
<input
13+
type="text"
14+
label="Enter command..."
15+
v-model="inputVal"
16+
id="command-input"
17+
ref="inputRef"
18+
class="w-xl h-12 px-4 text-fg outline-none bg-bg-subtle border border-border rounded-md"
19+
placeholder="Enter command..."
20+
@keydown="handleKeydown"
21+
/>
22+
23+
<div class="w-xl h-lg overflow-auto">
24+
<div
25+
v-for="item in filteredCmdList"
26+
:key="item.id"
27+
class="flex-col items-center justify-between px-4 py-2 not-first:mt-2 hover:bg-bg-elevated select-none cursor-pointer rounded-md transition"
28+
:class="{ 'bg-bg-subtle': item.id === selected }"
29+
@click="triggerCommand(item.id)"
30+
>
31+
<div class="text-fg">{{ item.name }}</div>
32+
<div class="text-fg-subtle text-sm">{{ item.description }}</div>
33+
</div>
2634
</div>
2735
</div>
2836
</div>
29-
</div>
37+
</Transition>
3038
</template>
3139

3240
<script setup lang="ts">
3341
const cmdList = [
3442
{
35-
id: 'npmx',
36-
name: 'npmx',
37-
description: 'Run npmx commands',
43+
id: 'package:search',
44+
name: 'Search packages',
45+
description: 'Search for packages',
46+
handler: () => {},
3847
},
3948
{
40-
id: 'npmx-init',
41-
name: 'npmx init',
42-
description: 'Initialize a new npmx project',
49+
id: 'org:search',
50+
name: 'Search organizations',
51+
description: 'Search for organizations',
52+
handler: () => {},
4353
},
4454
{
45-
id: 'npmx-install',
46-
name: 'npmx install',
47-
description: 'Install npmx dependencies',
55+
id: 'package:like',
56+
name: 'Like this',
57+
description: 'Like this package',
58+
handler: () => {},
4859
},
4960
{
50-
id: 'npmx-run',
51-
name: 'npmx run',
52-
description: 'Run npmx scripts',
61+
id: 'package:install',
62+
name: 'Copy install command',
63+
description: 'Copy install command to clipboard',
64+
handler: () => {},
5365
},
5466
]
5567
5668
const selected = ref(cmdList[0]?.id || '')
5769
const inputVal = ref('')
70+
const show = ref(false)
71+
const inputRef = useTemplateRef('inputRef')
72+
73+
const { focused: inputFocused } = useFocus(inputRef)
5874
5975
const filteredCmdList = computed(() => {
6076
if (!inputVal.value) {
@@ -77,4 +93,80 @@ watch(
7793
}
7894
},
7995
)
96+
97+
function focusInput() {
98+
inputFocused.value = true
99+
}
100+
101+
function open() {
102+
inputVal.value = ''
103+
selected.value = cmdList[0]?.id || ''
104+
show.value = true
105+
nextTick(focusInput)
106+
}
107+
108+
function close() {
109+
inputVal.value = ''
110+
selected.value = cmdList[0]?.id || ''
111+
show.value = false
112+
}
113+
114+
function toggle() {
115+
if (show.value) {
116+
close()
117+
} else {
118+
open()
119+
}
120+
}
121+
122+
function triggerCommand(id: string) {
123+
const selectedItem = filteredCmdList.value.find(item => item.id === id)
124+
selectedItem?.handler?.()
125+
close()
126+
}
127+
128+
const handleKeydown = useThrottleFn((e: KeyboardEvent) => {
129+
if (!filteredCmdList.value.length) return
130+
131+
const currentIndex = filteredCmdList.value.findIndex(item => item.id === selected.value)
132+
133+
if (e.key === 'ArrowDown') {
134+
e.preventDefault()
135+
const nextIndex = (currentIndex + 1) % filteredCmdList.value.length
136+
selected.value = filteredCmdList.value[nextIndex]?.id || ''
137+
} else if (e.key === 'ArrowUp') {
138+
e.preventDefault()
139+
const prevIndex =
140+
(currentIndex - 1 + filteredCmdList.value.length) % filteredCmdList.value.length
141+
selected.value = filteredCmdList.value[prevIndex]?.id || ''
142+
} else if (e.key === 'Enter') {
143+
e.preventDefault()
144+
triggerCommand(selected.value)
145+
} else if (e.key === 'Escape') {
146+
e.preventDefault()
147+
close()
148+
}
149+
}, 50)
150+
151+
defineExpose({
152+
open,
153+
close,
154+
toggle,
155+
})
80156
</script>
157+
158+
<style scoped>
159+
.fade-enter-active {
160+
transition: all 0.05s ease-out;
161+
}
162+
163+
.fade-leave-active {
164+
transition: all 0.1s ease-in;
165+
}
166+
167+
.fade-enter-from,
168+
.fade-leave-to {
169+
opacity: 0;
170+
transform: translateY(-10px);
171+
}
172+
</style>

0 commit comments

Comments
 (0)