<script setup lang="ts"> import type { CommandScope, QueryIndexedCommand } from '@/composables/command' const emit = defineEmits<{ (event: 'close'): void }>() const registry = useCommandRegistry() const inputEl = $ref<HTMLInputElement>() const resultEl = $ref<HTMLDivElement>() const scopes = $ref<CommandScope[]>([]) let input = $(commandPanelInput) onMounted(() => { inputEl?.focus() }) const commandMode = $computed(() => input.startsWith('>')) const result = $computed(() => commandMode ? registry.query(scopes.map(s => s.id).join('.'), input.slice(1)) : { length: 0, items: [], grouped: {} }) let active = $ref(0) watch($$(result), (n, o) => { if (n.length !== o.length || !n.items.every((i, idx) => i === o.items[idx])) active = 0 }) const findItemEl = (index: number) => resultEl?.querySelector(`[data-index="${index}"]`) as HTMLDivElement | null const onCommandActivate = (item: QueryIndexedCommand) => { if (item.onActivate) { item.onActivate() emit('close') } else if (item.onComplete) { scopes.push(item.onComplete()) input = '>' } } const onCommandComplete = (item: QueryIndexedCommand) => { if (item.onComplete) { scopes.push(item.onComplete()) input = '>' } else if (item.onActivate) { item.onActivate() } } const intoView = (index: number) => { const el = findItemEl(index) if (el) el.scrollIntoView({ block: 'nearest' }) } function setActive(index: number) { const len = result.length active = (index + len) % len intoView(active) } const onKeyDown = (e: KeyboardEvent) => { switch (e.key) { case 'p': case 'ArrowUp': { if (e.key === 'p' && !e.ctrlKey) break e.preventDefault() setActive(active - 1) break } case 'n': case 'ArrowDown': { if (e.key === 'n' && !e.ctrlKey) break e.preventDefault() setActive(active + 1) break } case 'Home': { e.preventDefault() active = 0 intoView(active) break } case 'End': { e.preventDefault() setActive(result.length - 1) break } case 'Enter': { e.preventDefault() const cmd = result.items[active] if (cmd) onCommandActivate(cmd) break } case 'Tab': { e.preventDefault() const cmd = result.items[active] if (cmd) onCommandComplete(cmd) break } case 'Backspace': { if (input === '>' && scopes.length) { e.preventDefault() scopes.pop() } break } } } </script> <template> <div class="flex flex-col w-50vw max-w-180 h-50vh max-h-120"> <!-- Input --> <label class="flex mx-3 my-1 items-center"> <div mx-1 i-ri:search-line /> <div v-for="scope in scopes" :key="scope.id" class="flex items-center mx-1 gap-2"> <div class="text-sm">{{ scope.display }}</div> <span class="text-secondary">/</span> </div> <input ref="inputEl" v-model="input" class="focus:outline-none flex-1 p-2 rounded bg-base" placeholder="Search" @keydown="onKeyDown" > <CommandKey name="Escape" /> </label> <div class="w-full border-b-1 border-base" /> <!-- Results --> <div ref="resultEl" class="flex-1 mx-1 overflow-y-auto"> <template v-for="[scope, group] in result.grouped" :key="scope"> <div class="mt-2 px-2 py-1 text-sm text-secondary"> {{ scope }} </div> <template v-for="cmd in group" :key="cmd.index"> <div class="flex px-3 py-2 my-1 items-center rounded-lg hover:bg-active transition-all duration-65 ease-in-out cursor-pointer scroll-m-10" :class="{ 'bg-active': active === cmd.index }" :data-index="cmd.index" @click="onCommandActivate(cmd)" > <div v-if="cmd.icon" mr-2 :class="cmd.icon" /> <div class="flex-1 flex items-baseline gap-2"> <div :class="{ 'font-medium': active === cmd.index }"> {{ cmd.name }} </div> <div v-if="cmd.description" class="text-xs text-secondary"> {{ cmd.description }} </div> </div> <div v-if="cmd.onComplete" class="flex items-center gap-1 transition-all duration-65 ease-in-out" :class="active === cmd.index ? 'opacity-100' : 'opacity-0'" > <div class="text-xs text-secondary"> {{ $t('command.complete') }} </div> <CommandKey name="Tab" /> </div> <div v-if="cmd.onActivate" class="flex items-center gap-1 transition-all duration-65 ease-in-out" :class="active === cmd.index ? 'opacity-100' : 'opacity-0'" > <div class="text-xs text-secondary"> {{ $t('command.activate') }} </div> <CommandKey name="Enter" /> </div> </div> </template> </template> </div> <div class="w-full border-b-1 border-base" /> <!-- Footer --> <div class="flex items-center px-3 py-1 text-xs"> <div i-ri:lightbulb-flash-line /> Tip: Use <!-- <CommandKey name="Ctrl+K" /> to search, --> <CommandKey name="Ctrl+/" /> to activate command mode. </div> </div> </template>