Skip to content
This repository was archived by the owner on May 1, 2025. It is now read-only.

Commit 6df5f8a

Browse files
committed
feat: basic search feature
1 parent 2518b07 commit 6df5f8a

File tree

4 files changed

+68
-19
lines changed

4 files changed

+68
-19
lines changed

app.vue

Lines changed: 0 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -4,19 +4,6 @@ import 'splitpanes/dist/splitpanes.css'
44
import './styles/base.css'
55
import './styles/prose.css'
66
import './styles/overrides.css'
7-
8-
addCommands(
9-
{
10-
title: 'Home',
11-
to: '/',
12-
icon: 'i-ph-house-duotone',
13-
},
14-
{
15-
title: 'Vue Basic',
16-
to: '/vue/intro',
17-
icon: 'i-ph-file-duotone',
18-
},
19-
)
207
</script>
218

229
<template>

components/CommandPalette.vue

Lines changed: 39 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ const commands = useCommandsStore()
66
const router = useRouter()
77
88
const selected = ref(0)
9+
const input = ref<HTMLInputElement>()
910
1011
function move(delta: number) {
1112
selected.value += delta
@@ -23,6 +24,35 @@ function runCommand(command: Command) {
2324
commands.isShown = false
2425
}
2526
27+
function scrollIntoView(elOrComponent?: any) {
28+
const el = elOrComponent?.$el || elOrComponent
29+
el?.scrollIntoView?.({
30+
block: 'nearest',
31+
inline: 'nearest',
32+
})
33+
}
34+
35+
// Reset selected when search changes
36+
watch(
37+
() => commands.search,
38+
() => {
39+
selected.value = 0
40+
},
41+
)
42+
43+
watch(
44+
() => commands.isShown,
45+
() => {
46+
if (commands.isShown) {
47+
commands.search = ''
48+
// Auto-focus on input open
49+
nextTick(() => {
50+
input.value?.focus()
51+
})
52+
}
53+
},
54+
)
55+
2656
useEventListener('keydown', (e) => {
2757
if ((e.metaKey || e.ctrlKey) && e.key === 'k') {
2858
commands.isShown = !commands.isShown
@@ -59,23 +89,28 @@ useEventListener('keydown', (e) => {
5989
fixed inset-0 z-index-command-palette flex="~ items-center justify-center"
6090
>
6191
<div absolute inset-0 z--1 bg-black:75 />
62-
<div border="~ base rounded" h-100 w-200 bg-base>
92+
<div
93+
border="~ base rounded" h-100 w-200 of-hidden bg-base
94+
grid="~ rows-[max-content_1fr]"
95+
>
6396
<div flex="~ items-center">
6497
<div class="i-ph-magnifying-glass-duotone" m4 text-xl />
6598
<input
99+
ref="input"
66100
v-model="commands.search"
67101
h-full w-full rounded border-none p4 pl0 outline-none bg-base
68102
placeholder="Search..."
69103
>
70104
</div>
71105

72-
<div border="t base" flex="~ col">
106+
<div border="t base" flex="~ col" of-y-auto py2>
73107
<component
74108
:is="c.to ? NuxtLink : 'button'"
75109
v-for="c, idx in commands.commandsResult"
76110
:key="c.id || c.title"
77-
:to="c.to"
78-
flex="~ gap-2 items-center" mx1 rounded p2 px3
111+
:ref="(el: Element) => selected === idx && scrollIntoView(el)"
112+
:to="c.to" flex="~ gap-2 items-center" mx1 rounded p2
113+
px3
79114
:class="selected === idx ? 'bg-active' : ''"
80115
@click="runCommand(c)"
81116
>

nuxt.config.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,9 @@ export default defineNuxtConfig({
121121
'remark-external-links',
122122
],
123123
},
124+
experimental: {
125+
// @ts-expect-error awaits https://github.com/nuxt/content/pull/2506
126+
search: {},
127+
},
124128
},
125-
126129
})

stores/commands.ts

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import type { ParsedContent } from '@nuxt/content/dist/runtime/types'
12
import Fuse from 'fuse.js'
23

34
export interface Command {
@@ -14,16 +15,39 @@ export const useCommandsStore = defineStore('commands', () => {
1415
const search = ref('')
1516
const isShown = ref(false)
1617
const commandsAll = reactive<Set<Command>>(new Set())
18+
const guidesResult = ref<Command[]>([])
1719

1820
const fuse = computed(() => new Fuse(Array.from(commandsAll), {
1921
keys: ['title', 'description'],
2022
threshold: 0.3,
2123
}))
2224

25+
const debouncedSearch = refDebounced(search, 100)
26+
27+
watch(debouncedSearch, async (v) => {
28+
if (v) {
29+
// TODO: send a PR to nuxt/content to default generic type
30+
// TODO: move it out if it's reactive
31+
const result = await searchContent(v, {}) as ComputedRef<ParsedContent[]>
32+
guidesResult.value = result.value.map((i): Command => ({
33+
id: i.id,
34+
title: i.title || 'Untitled',
35+
to: i.id,
36+
icon: 'i-ph-file-duotone',
37+
}))
38+
}
39+
else {
40+
guidesResult.value = []
41+
}
42+
})
43+
2344
const commandsResult = computed(() => {
2445
if (!search.value)
2546
return Array.from(commandsAll)
26-
return fuse.value.search(search.value).map(i => i.item)
47+
return [
48+
...fuse.value.search(search.value).map(i => i.item),
49+
...guidesResult.value,
50+
]
2751
})
2852

2953
return {

0 commit comments

Comments
 (0)