11<script setup lang="ts">
22import Fuse from ' fuse.js'
3+ import type { CommandItem } from ' ~/composables/state-commands'
34
45const show = ref (false )
56const search = ref (' ' )
67
7- const items = useCommands ()
8+ const rootItems = useCommands ()
9+ const overrideItems = ref <CommandItem [] | undefined >()
10+ const items = computed (() => overrideItems .value || rootItems .value )
811
912const fuse = computed (() => new Fuse (items .value , {
1013 keys: [
14+ ' id' ,
1115 ' title' ,
1216 ],
13- threshold: 0.3 ,
17+ distance: 50 ,
1418}))
1519
16- const filtered = computed (() => {
17- const result = search .value
18- ? fuse .value .search (search .value ).map (i => i .item )
19- : (items .value || [])
20- return result
21- })
20+ const filtered = computed (() => search .value
21+ ? fuse .value .search (search .value ).map (i => i .item )
22+ : (items .value || []),
23+ )
2224
23- const elements = ref <any []>([])
2425const selectedIndex = ref (0 )
2526
2627watch (search , () => {
2728 selectedIndex .value = 0
29+ scrollToITem ()
2830})
2931
3032function moveSelected(delta : number ) {
3133 selectedIndex .value = ((selectedIndex .value + delta ) + filtered .value .length ) % filtered .value .length
34+ scrollToITem ()
35+ }
3236
33- const item = elements .value [selectedIndex .value ]
34- item .scrollIntoView ({
37+ function scrollToITem() {
38+ const item = document .getElementById (filtered .value [selectedIndex .value ]?.id )
39+ item ?.scrollIntoView ({
3540 block: ' center' ,
3641 })
3742}
3843
44+ async function enterItem(item : CommandItem ) {
45+ const result = await item .action ()
46+ if (! result ) {
47+ overrideItems .value = undefined
48+ search .value = ' '
49+ show .value = false
50+ }
51+ else {
52+ overrideItems .value = result
53+ search .value = ' '
54+ }
55+ }
56+
3957useEventListener (' keydown' , (e ) => {
4058 if ((e .ctrlKey || e .metaKey ) && e .key === ' k' ) {
4159 e .preventDefault ()
60+ overrideItems .value = undefined
61+ search .value = ' '
4262 show .value = ! show .value
4363 return
4464 }
4565
46- if (show .value ) {
47- if (e .key === ' ArrowDown' || e .key === ' ArrowUp' ) {
66+ if (! show .value )
67+ return
68+
69+ switch (e .key ) {
70+ case ' ArrowDown' :
71+ case ' ArrowUp' :
4872 e .preventDefault ()
4973 moveSelected (e .key === ' ArrowDown' ? 1 : - 1 )
50- }
74+ break
5175
52- if ( e . key === ' Enter' ) {
76+ case ' Enter' : {
5377 const item = filtered .value [selectedIndex .value ]
5478 if (item ) {
5579 e .preventDefault ()
56- item .action ()
57- show .value = false
80+ enterItem (item )
5881 }
82+ break
5983 }
6084
61- if (e .key === ' Escape' )
62- show .value = false
85+ case ' Escape' : {
86+ e .preventDefault ()
87+ if (overrideItems .value ) {
88+ overrideItems .value = undefined
89+ search .value = ' '
90+ }
91+ else {
92+ show .value = false
93+ }
94+ break
95+ }
6396 }
6497})
98+
99+ function onKeyDown(e : KeyboardEvent ) {
100+ if (e .key === ' Backspace' && ! search .value && overrideItems .value ) {
101+ e .preventDefault ()
102+ overrideItems .value = undefined
103+ search .value = ' '
104+ }
105+ }
65106 </script >
66107
67108<template >
@@ -71,27 +112,29 @@ useEventListener('keydown', (e) => {
71112 <NTextInput
72113 v-model =" search"
73114 placeholder =" Type to search..."
74- class =" rounded-none py3 px2! ring-0!" n =" lg green borderless"
115+ class =" rounded-none py3 px2! ring-0!"
116+ n =" green borderless"
117+ @keydown =" onKeyDown"
75118 />
76119 </header >
77120 <div flex-auto of-auto p2 flex =" ~ col" >
78121 <button
79122 v-for =" item, idx of filtered"
80123 :id =" item.id"
81- ref =" elements"
82124 :key =" item.id"
83- @click =" item.action(), show = false "
125+ @click =" enterItem(item) "
84126 @mouseover =" selectedIndex = idx"
85127 >
86128 <div
87- flex =" ~ items-center justify-between" rounded px3 py2
88- :class =" selectedIndex === idx ? 'op100 bg-primary/10 text-primary saturate-100 bg-active' : 'op50 '"
129+ flex =" ~ gap-2 items-center justify-between" rounded px3 py2
130+ :class =" selectedIndex === idx ? 'op100 bg-primary/10 text-primary saturate-100 bg-active' : 'op80 '"
89131 >
90- <span flex items-center gap2 >
91- <TabIcon text-xl :icon =" item.icon" :title =" item.title" />
92- {{ item.title }}
132+ <TabIcon :icon =" item.icon" :title =" item.title" flex-none text-xl />
133+ <span flex flex-auto items-center gap2 of-hidden >
134+ <span ws-nowrap >{{ item.title }}</span >
135+ <span of-hidden truncate ws-nowrap text-sm op50 >{{ item.description }}</span >
93136 </span >
94- <NIcon v-if =" selectedIndex === idx" icon =" tabler-arrow-back " />
137+ <NIcon v-if =" selectedIndex === idx" icon =" i-carbon-text-new-line scale-x--100 " flex-none />
95138 </div >
96139 </button >
97140 <div v-if =" !filtered.length" h-full flex items-center justify-center gap-2 text-xl >
@@ -105,12 +148,6 @@ useEventListener('keydown', (e) => {
105148 </div >
106149 </div >
107150 <footer border =" t base" flex =" ~ none justify-between items-center gap-4" pointer-events-none px4 py2 >
108- <div text-xs flex =" ~ items-center gap2" >
109- <NButton n =" xs" px1 >
110- <NIcon icon =" tabler-arrow-back" />
111- </NButton >
112- <span op75 >to select</span >
113- </div >
114151 <div text-xs flex =" ~ items-center gap2" >
115152 <NButton n =" xs" px1 >
116153 <NIcon icon =" carbon-arrow-down" />
@@ -124,7 +161,13 @@ useEventListener('keydown', (e) => {
124161 <NButton n =" xs" px1 >
125162 Esc
126163 </NButton >
127- <span op75 >to close</span >
164+ <span op75 >to {{ overrideItems ? 'go back' : 'close' }}</span >
165+ </div >
166+ <div text-xs flex =" ~ items-center gap2" >
167+ <NButton n =" xs" px1 >
168+ <NIcon icon =" i-carbon-text-new-line scale-x--100" />
169+ </NButton >
170+ <span op75 >to select</span >
128171 </div >
129172 </footer >
130173 </div >
0 commit comments