Skip to content

Commit 3cf828e

Browse files
arashsheydaantfu
andauthored
feat: mutliple level command-palette, commands for docs (#247)
Co-authored-by: Anthony Fu <[email protected]>
1 parent e5cef5e commit 3cf828e

File tree

9 files changed

+979
-39
lines changed

9 files changed

+979
-39
lines changed

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,3 +50,5 @@ Temporary Items
5050

5151
# Workspaces
5252
packages/devtools/README.md
53+
54+
clones

package.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
{
2+
"type": "module",
23
"version": "0.5.5",
34
"private": false,
45
"packageManager": "[email protected]",
@@ -34,10 +35,12 @@
3435
"eslint": "8.42.0",
3536
"esno": "^0.16.3",
3637
"execa": "^7.1.1",
38+
"gray-matter": "^4.0.3",
3739
"lint-staged": "^13.2.2",
3840
"nuxt": "^3.5.2",
3941
"pathe": "^1.1.1",
4042
"simple-git-hooks": "^2.8.1",
43+
"tiged": "^2.12.5",
4144
"typescript": "5.0.4",
4245
"unocss": "^0.53.0",
4346
"vite-hot-client": "^0.2.1",

packages/devtools/client/components/CommandPalette.vue

Lines changed: 78 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,67 +1,108 @@
11
<script setup lang="ts">
22
import Fuse from 'fuse.js'
3+
import type { CommandItem } from '~/composables/state-commands'
34
45
const show = ref(false)
56
const 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
912
const 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[]>([])
2425
const selectedIndex = ref(0)
2526
2627
watch(search, () => {
2728
selectedIndex.value = 0
29+
scrollToITem()
2830
})
2931
3032
function 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+
3957
useEventListener('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>

packages/devtools/client/composables/state-commands.ts

Lines changed: 39 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,9 @@ import type { MaybeRefOrGetter } from 'vue'
44
export interface CommandItem {
55
id: string
66
title: string
7+
description?: string
78
icon?: string
8-
action: () => void
9+
action: () => void | CommandItem[] | Promise<CommandItem[]>
910
}
1011

1112
const registeredCommands = reactive(new Map<string, MaybeRefOrGetter<CommandItem[]>>())
@@ -19,7 +20,17 @@ export function useCommands() {
1920
id: 'fixed:settings',
2021
title: 'Settings',
2122
icon: 'carbon-settings-adjust',
22-
action: () => router.push('/settings'),
23+
action: () => {
24+
router.push('/settings')
25+
},
26+
},
27+
{
28+
id: 'fixed:docs',
29+
title: 'Nuxt Documentations',
30+
icon: 'logos-nuxt-icon',
31+
action: () => {
32+
return getNuxtDocsCommands()
33+
},
2334
},
2435
]
2536

@@ -58,3 +69,29 @@ export function registerCommands(getter: MaybeRefOrGetter<CommandItem[]>) {
5869
registeredCommands.delete(id)
5970
})
6071
}
72+
73+
let _nuxtDocsCommands: CommandItem[] | undefined
74+
75+
const docsIcons = [
76+
[':components:', 'i-carbon-assembly-cluster'],
77+
[':modules:', 'i-carbon-cube'],
78+
[':commands:', 'i-carbon-terminal'],
79+
[':directory-structure:', 'i-carbon-folder'],
80+
[':composables:', 'i-carbon-function'],
81+
[':getting-started:', 'i-carbon-idea'],
82+
[':api:', 'carbon-api-1'],
83+
]
84+
85+
export async function getNuxtDocsCommands() {
86+
if (!_nuxtDocsCommands) {
87+
const list = await import('../data/nuxt-docs.json').then(i => i.default)
88+
_nuxtDocsCommands = list.map(i => ({
89+
...i,
90+
icon: docsIcons.find(([k]) => i.id.includes(k))?.[1] || 'i-carbon-document-multiple-01',
91+
action: () => {
92+
window.open(i.url, '_blank')
93+
},
94+
}))
95+
}
96+
return _nuxtDocsCommands
97+
}

0 commit comments

Comments
 (0)