Skip to content

Commit

Permalink
feat: mutliple level command-palette, commands for docs (#247)
Browse files Browse the repository at this point in the history
Co-authored-by: Anthony Fu <anthonyfu117@hotmail.com>
  • Loading branch information
arashsheyda and antfu authored Jun 5, 2023
1 parent e5cef5e commit 3cf828e
Show file tree
Hide file tree
Showing 9 changed files with 979 additions and 39 deletions.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -50,3 +50,5 @@ Temporary Items

# Workspaces
packages/devtools/README.md

clones
3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
{
"type": "module",
"version": "0.5.5",
"private": false,
"packageManager": "pnpm@8.6.1",
Expand Down Expand Up @@ -34,10 +35,12 @@
"eslint": "8.42.0",
"esno": "^0.16.3",
"execa": "^7.1.1",
"gray-matter": "^4.0.3",
"lint-staged": "^13.2.2",
"nuxt": "^3.5.2",
"pathe": "^1.1.1",
"simple-git-hooks": "^2.8.1",
"tiged": "^2.12.5",
"typescript": "5.0.4",
"unocss": "^0.53.0",
"vite-hot-client": "^0.2.1",
Expand Down
113 changes: 78 additions & 35 deletions packages/devtools/client/components/CommandPalette.vue
Original file line number Diff line number Diff line change
@@ -1,67 +1,108 @@
<script setup lang="ts">
import Fuse from 'fuse.js'
import type { CommandItem } from '~/composables/state-commands'
const show = ref(false)
const search = ref('')
const items = useCommands()
const rootItems = useCommands()
const overrideItems = ref<CommandItem[] | undefined>()
const items = computed(() => overrideItems.value || rootItems.value)
const fuse = computed(() => new Fuse(items.value, {
keys: [
'id',
'title',
],
threshold: 0.3,
distance: 50,
}))
const filtered = computed(() => {
const result = search.value
? fuse.value.search(search.value).map(i => i.item)
: (items.value || [])
return result
})
const filtered = computed(() => search.value
? fuse.value.search(search.value).map(i => i.item)
: (items.value || []),
)
const elements = ref<any[]>([])
const selectedIndex = ref(0)
watch(search, () => {
selectedIndex.value = 0
scrollToITem()
})
function moveSelected(delta: number) {
selectedIndex.value = ((selectedIndex.value + delta) + filtered.value.length) % filtered.value.length
scrollToITem()
}
const item = elements.value[selectedIndex.value]
item.scrollIntoView({
function scrollToITem() {
const item = document.getElementById(filtered.value[selectedIndex.value]?.id)
item?.scrollIntoView({
block: 'center',
})
}
async function enterItem(item: CommandItem) {
const result = await item.action()
if (!result) {
overrideItems.value = undefined
search.value = ''
show.value = false
}
else {
overrideItems.value = result
search.value = ''
}
}
useEventListener('keydown', (e) => {
if ((e.ctrlKey || e.metaKey) && e.key === 'k') {
e.preventDefault()
overrideItems.value = undefined
search.value = ''
show.value = !show.value
return
}
if (show.value) {
if (e.key === 'ArrowDown' || e.key === 'ArrowUp') {
if (!show.value)
return
switch (e.key) {
case 'ArrowDown':
case 'ArrowUp':
e.preventDefault()
moveSelected(e.key === 'ArrowDown' ? 1 : -1)
}
break
if (e.key === 'Enter') {
case 'Enter': {
const item = filtered.value[selectedIndex.value]
if (item) {
e.preventDefault()
item.action()
show.value = false
enterItem(item)
}
break
}
if (e.key === 'Escape')
show.value = false
case 'Escape': {
e.preventDefault()
if (overrideItems.value) {
overrideItems.value = undefined
search.value = ''
}
else {
show.value = false
}
break
}
}
})
function onKeyDown(e: KeyboardEvent) {
if (e.key === 'Backspace' && !search.value && overrideItems.value) {
e.preventDefault()
overrideItems.value = undefined
search.value = ''
}
}
</script>

<template>
Expand All @@ -71,27 +112,29 @@ useEventListener('keydown', (e) => {
<NTextInput
v-model="search"
placeholder="Type to search..."
class="rounded-none py3 px2! ring-0!" n="lg green borderless"
class="rounded-none py3 px2! ring-0!"
n="green borderless"
@keydown="onKeyDown"
/>
</header>
<div flex-auto of-auto p2 flex="~ col">
<button
v-for="item, idx of filtered"
:id="item.id"
ref="elements"
:key="item.id"
@click="item.action(), show = false"
@click="enterItem(item)"
@mouseover="selectedIndex = idx"
>
<div
flex="~ items-center justify-between" rounded px3 py2
:class="selectedIndex === idx ? 'op100 bg-primary/10 text-primary saturate-100 bg-active' : 'op50'"
flex="~ gap-2 items-center justify-between" rounded px3 py2
:class="selectedIndex === idx ? 'op100 bg-primary/10 text-primary saturate-100 bg-active' : 'op80'"
>
<span flex items-center gap2>
<TabIcon text-xl :icon="item.icon" :title="item.title" />
{{ item.title }}
<TabIcon :icon="item.icon" :title="item.title" flex-none text-xl />
<span flex flex-auto items-center gap2 of-hidden>
<span ws-nowrap>{{ item.title }}</span>
<span of-hidden truncate ws-nowrap text-sm op50>{{ item.description }}</span>
</span>
<NIcon v-if="selectedIndex === idx" icon="tabler-arrow-back" />
<NIcon v-if="selectedIndex === idx" icon="i-carbon-text-new-line scale-x--100" flex-none />
</div>
</button>
<div v-if="!filtered.length" h-full flex items-center justify-center gap-2 text-xl>
Expand All @@ -105,12 +148,6 @@ useEventListener('keydown', (e) => {
</div>
</div>
<footer border="t base" flex="~ none justify-between items-center gap-4" pointer-events-none px4 py2>
<div text-xs flex="~ items-center gap2">
<NButton n="xs" px1>
<NIcon icon="tabler-arrow-back" />
</NButton>
<span op75>to select</span>
</div>
<div text-xs flex="~ items-center gap2">
<NButton n="xs" px1>
<NIcon icon="carbon-arrow-down" />
Expand All @@ -124,7 +161,13 @@ useEventListener('keydown', (e) => {
<NButton n="xs" px1>
Esc
</NButton>
<span op75>to close</span>
<span op75>to {{ overrideItems ? 'go back' : 'close' }}</span>
</div>
<div text-xs flex="~ items-center gap2">
<NButton n="xs" px1>
<NIcon icon="i-carbon-text-new-line scale-x--100" />
</NButton>
<span op75>to select</span>
</div>
</footer>
</div>
Expand Down
41 changes: 39 additions & 2 deletions packages/devtools/client/composables/state-commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,9 @@ import type { MaybeRefOrGetter } from 'vue'
export interface CommandItem {
id: string
title: string
description?: string
icon?: string
action: () => void
action: () => void | CommandItem[] | Promise<CommandItem[]>
}

const registeredCommands = reactive(new Map<string, MaybeRefOrGetter<CommandItem[]>>())
Expand All @@ -19,7 +20,17 @@ export function useCommands() {
id: 'fixed:settings',
title: 'Settings',
icon: 'carbon-settings-adjust',
action: () => router.push('/settings'),
action: () => {
router.push('/settings')
},
},
{
id: 'fixed:docs',
title: 'Nuxt Documentations',
icon: 'logos-nuxt-icon',
action: () => {
return getNuxtDocsCommands()
},
},
]

Expand Down Expand Up @@ -58,3 +69,29 @@ export function registerCommands(getter: MaybeRefOrGetter<CommandItem[]>) {
registeredCommands.delete(id)
})
}

let _nuxtDocsCommands: CommandItem[] | undefined

const docsIcons = [
[':components:', 'i-carbon-assembly-cluster'],
[':modules:', 'i-carbon-cube'],
[':commands:', 'i-carbon-terminal'],
[':directory-structure:', 'i-carbon-folder'],
[':composables:', 'i-carbon-function'],
[':getting-started:', 'i-carbon-idea'],
[':api:', 'carbon-api-1'],
]

export async function getNuxtDocsCommands() {
if (!_nuxtDocsCommands) {
const list = await import('../data/nuxt-docs.json').then(i => i.default)
_nuxtDocsCommands = list.map(i => ({
...i,
icon: docsIcons.find(([k]) => i.id.includes(k))?.[1] || 'i-carbon-document-multiple-01',
action: () => {
window.open(i.url, '_blank')
},
}))
}
return _nuxtDocsCommands
}
Loading

0 comments on commit 3cf828e

Please sign in to comment.