UIPackage

Command Palette

block dashboard
Edit on GitHub

Header-grade search/command palette. Bundles the slim trigger button (with platform-aware ⌘K/Ctrl-K kbd hint) AND the modal CommandDialog into one block; consumers drop it once and the global keyboard shortcut wires itself. Takes a `groups` array of `{ heading, items: [{ label, hint, icon, onSelect }] }` and emits `select`. `show-trigger=false` hides the inline button so the consumer can fire it from elsewhere via the exposed `show()` / `toggle()` methods.

Also available for React ->

Installation

$ npx shadcn-vue@latest add https://uipkge.dev/r/vue/command-palette.json

Or with the named registry: npx shadcn-vue@latest add @uipkge/command-palette

Examples

Schema

Type aliases exported from this item's source. Use these to shape the data you pass in.

CommandPaletteItem
interface CommandPaletteItem {
  label: string
  value?: string
  hint?: string
  icon?: Component
  onSelect?: () => void
}
CommandPaletteGroup
interface CommandPaletteGroup {
  heading: string
  items: CommandPaletteItem[]
}

npm dependencies

Includes

Used by

Files (1)

  • app/components/blocks/CommandPalette.vue 4 kB
    <script setup lang="ts">
    import { computed, onBeforeUnmount, onMounted, ref, type Component } from 'vue'
    import { LayoutDashboard, FileText, Inbox, Settings, Users, KanbanSquare, Search } from 'lucide-vue-next'
    import {
      CommandDialog,
      CommandEmpty,
      CommandGroup,
      CommandInput,
      CommandItem,
      CommandList,
      CommandSeparator,
      CommandShortcut,
    } from '@/components/ui/command'
    
    export interface CommandPaletteItem {
      label: string
      value?: string
      hint?: string
      icon?: Component
      onSelect?: () => void
    }
    
    export interface CommandPaletteGroup {
      heading: string
      items: CommandPaletteItem[]
    }
    
    const props = withDefaults(
      defineProps<{
        groups?: CommandPaletteGroup[]
        placeholder?: string
        triggerLabel?: string
        showTrigger?: boolean
      }>(),
      {
        placeholder: 'Search pages, commands…',
        triggerLabel: 'Search pages, commands…',
        showTrigger: true,
        groups: () => [
          {
            heading: 'Navigate',
            items: [
              { label: 'Dashboard', hint: '/dashboard', icon: LayoutDashboard },
              { label: 'Inbox', hint: '/inbox', icon: Inbox },
              { label: 'Kanban', hint: '/kanban', icon: KanbanSquare },
              { label: 'Team', hint: '/team', icon: Users },
            ],
          },
          {
            heading: 'Settings',
            items: [
              { label: 'Profile', hint: '/profile', icon: FileText },
              { label: 'Settings', hint: '/settings', icon: Settings },
            ],
          },
        ],
      },
    )
    
    const emit = defineEmits<{
      (e: 'select', item: CommandPaletteItem): void
    }>()
    
    const open = ref(false)
    
    function show() {
      open.value = true
    }
    
    function hide() {
      open.value = false
    }
    
    function toggle() {
      open.value = !open.value
    }
    
    function pick(item: CommandPaletteItem) {
      hide()
      item.onSelect?.()
      emit('select', item)
    }
    
    function onKeydown(e: KeyboardEvent) {
      if (e.key === 'k' && (e.metaKey || e.ctrlKey)) {
        e.preventDefault()
        toggle()
      }
    }
    
    const triggerShortcut = computed(() => {
      if (typeof navigator === 'undefined') return '⌘K'
      return /Mac|iPhone|iPad/i.test(navigator.platform) ? '⌘K' : 'Ctrl K'
    })
    
    onMounted(() => window.addEventListener('keydown', onKeydown))
    onBeforeUnmount(() => window.removeEventListener('keydown', onKeydown))
    
    defineExpose({ show, hide, toggle })
    </script>
    
    <template>
      <button
        v-if="showTrigger"
        type="button"
        class="bg-secondary/50 hover:bg-secondary text-muted-foreground focus-visible:ring-ring relative hidden h-8 w-full items-center gap-2 rounded-lg border border-transparent px-2.5 text-sm shadow-none transition-colors focus-visible:ring-1 focus-visible:outline-none sm:flex md:w-[220px] lg:w-[300px]"
        aria-label="Open command palette"
        @click="show"
      >
        <Search class="size-3.5 shrink-0" />
        <span class="flex-1 truncate text-left">{{ triggerLabel }}</span>
        <kbd
          class="bg-muted/80 text-muted-foreground pointer-events-none flex h-5 items-center justify-center rounded-md border px-1.5 font-mono text-[10px] font-medium"
        >
          <span>{{ triggerShortcut }}</span>
        </kbd>
      </button>
    
      <CommandDialog v-model:open="open" title="Command palette" description="Search pages and run commands">
        <CommandInput :placeholder="placeholder" />
        <CommandList class="max-h-[480px]">
          <CommandEmpty>No matches.</CommandEmpty>
          <template v-for="(group, gi) in groups" :key="group.heading">
            <CommandGroup :heading="group.heading">
              <CommandItem
                v-for="item in group.items"
                :key="`${group.heading}-${item.label}`"
                :value="`${group.heading} ${item.label} ${item.hint ?? ''}`"
                @select="pick(item)"
              >
                <component :is="item.icon" v-if="item.icon" class="size-4" />
                <span>{{ item.label }}</span>
                <CommandShortcut v-if="item.hint" class="text-muted-foreground/70">{{ item.hint }}</CommandShortcut>
              </CommandItem>
            </CommandGroup>
            <CommandSeparator v-if="gi < groups.length - 1" />
          </template>
        </CommandList>
      </CommandDialog>
    </template>

Raw manifest: https://uipkge.dev/r/vue/command-palette.json