useCommandRegistry
Every command the palette can run. Modules register theirs at load time; the palette reads one reactive list.
vue
<script setup lang="ts">
import type { Command } from '@shared/composables/useCommandRegistry'
import { useCommandRegistry } from '@shared/composables/useCommandRegistry'
import { Badge } from '@shared/components/ui/badge'
import { Button } from '@shared/components/ui/button'
import { FilePlus, Settings, Users } from 'lucide-vue-next'
import { computed, onMounted, onUnmounted, ref } from 'vue'
const ran = ref('Nothing run yet.')
const DEMO: Command[] = [
{ id: 'docs-reg-invoice', title: 'New invoice', group: 'actions', icon: FilePlus, keywords: ['bill', 'draft'], handler: () => { ran.value = 'New invoice' } },
{ id: 'docs-reg-customers', title: 'Go to customers', group: 'pages', icon: Users, shortcut: 'g c', keywords: ['clients'], handler: () => { ran.value = 'Go to customers' } },
{ id: 'docs-reg-appearance', title: 'Appearance', group: 'settings', icon: Settings, handler: () => { ran.value = 'Appearance' } },
]
const registry = useCommandRegistry()
const myRecent = computed(() =>
registry.recentIds.value
.filter(id => id.startsWith('docs-reg-'))
.map(id => DEMO.find(c => c.id === id)?.title ?? id),
)
onMounted(() => registry.register(DEMO))
onUnmounted(() => DEMO.forEach(c => registry.unregister(c.id)))
</script>
<template>
<div class="flex flex-col gap-4">
<div class="flex flex-wrap gap-2">
<Button
v-for="command in DEMO"
:key="command.id"
size="sm"
variant="outline"
@click="registry.execute(command.id)"
>
<component :is="command.icon" class="size-4" />
{{ command.title }}
</Button>
</div>
<p class="rounded-lg border border-border p-3 sa-text-sm">
{{ ran }}
</p>
<div>
<p class="sa-text-sm font-semibold">
Recent, newest first
</p>
<div class="mt-1 flex flex-wrap gap-2">
<Badge v-for="title in myRecent" :key="title" variant="outline">
{{ title }}
</Badge>
<span v-if="!myRecent.length" class="sa-text-sm text-muted-foreground">
Run one and it appears here.
</span>
</div>
</div>
</div>
</template>Import
ts
import { useCommandRegistry } from '@shared/composables/useCommandRegistry'Signature
ts
function useCommandRegistry(): {
commands: Readonly<Ref<Command[]>>
recentIds: Readonly<Ref<string[]>>
register: (cmd: Command | Command[]) => void
unregister: (id: string) => void
execute: (id: string) => Promise<void>
}Returns
| Member | Type | Description |
|---|---|---|
commands | Readonly<Ref<Command[]>> | Everything registered, in registration order. |
recentIds | Readonly<Ref<string[]>> | The last eight executed, newest first. Persisted. |
register | (cmd | cmd[]) => void | Adds, or replaces one with the same id. |
unregister | (id) => void | Removes by id. |
execute | (id) => Promise<void> | Records recency, then awaits the handler. |
Command
| Key | Type | Description |
|---|---|---|
id | string | Unique. A second registration with the same id replaces the first. |
title | string | What the palette lists. |
group | 'pages' | 'actions' | 'recent' | 'settings' | Which section it appears under. |
icon | Component | A lucide icon. |
shortcut | string | A display-only hint such as 'g d'. It binds nothing. |
keywords | string[] | Extra words the fuzzy search should match. |
feature | keyof SmartAdminConfig['features'] | Hide the command while that flag is off. |
handler | () => void | Promise<void> | What running it does. |
Example
ts
import { useCommandRegistry } from '@shared/composables/useCommandRegistry'
import { FilePlus, Users } from 'lucide-vue-next'
import { router } from './router'
const { register } = useCommandRegistry()
register([
{
id: 'invoices.new',
title: 'New invoice',
group: 'actions',
icon: FilePlus,
keywords: ['create', 'bill', 'draft'],
handler: () => { void router.push('/invoices/new') },
},
{
id: 'customers.list',
title: 'Go to customers',
group: 'pages',
icon: Users,
shortcut: 'g c',
keywords: ['clients', 'accounts'],
handler: () => { void router.push('/customers') },
},
])Notes
executeis the only entry point. It records recency and then runs the handler, so no caller ever touchesrecentIds. Callinghandler()yourself skips the recency.recentIdsis capped at eight, deduped, and persisted undersa-recent-commands.- Re-registering the same id replaces it in place, keeping its position in the list. That is what makes a module safe to hot-reload.
featureis filtered by the palette, not at registration. Registration runs once at boot; the flag is a live setting, so a command switched off in Settings disappears on the next open rather than the next reload.keywordsis how "clients" finds "Go to customers". Without it the fuzzy search only sees the title.commandsis readonly. Add and remove throughregisterandunregister.
Related
useCommandPalette— opening and closing it.- Command palette — the component.
useShortcuts— real key bindings, as opposed to the display hint.