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">
3341const 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
5668const selected = ref (cmdList [0 ]?.id || ' ' )
5769const inputVal = ref (' ' )
70+ const show = ref (false )
71+ const inputRef = useTemplateRef (' inputRef' )
72+
73+ const { focused : inputFocused } = useFocus (inputRef )
5874
5975const 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