Menu

Change language

GitHub repo

Command Palette

blocklayout

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 calls `onSelect`. `showTrigger={false}` hides the inline button so the consumer can control open state from elsewhere.

Installation

$npx shadcn@latest add https://uipkge.dev/r/lit/command-palette.json
Manifest:https://uipkge.dev/r/lit/command-palette.jsonInstalls to:src/uip/blocks/command-palette/

Variants

Loading interactive previews…

Props

NameType / ValuesDefaultRequired
placeholderstringSearch pages, commands…optional
trigger-labelstringSearch pages, commands…optional
converterstring—optional

Schema

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

CommandPaletteItem
interface CommandPaletteItem {
  label: string
  hint?: string
  icon?: IconNode
  iconName?: string
  onSelect?: () => void
}
CommandPaletteGroup
interface CommandPaletteGroup {
  heading: string
  items: CommandPaletteItem[]
}
HTMLElementTagNameMap
interface HTMLElementTagNameMap {
    'uip-command-palette': UipCommandPalette
  }
}

npm dependencies

Files installed (1)

  • src/uip/blocks/command-palette/command-palette.ts6.3 kB
    import { LitElement, css, html, isServer, nothing } from 'lit'
    import { FileText, Inbox, LayoutDashboard, Search, Settings, SquareKanban, Users, type IconNode } from 'lucide'
    import { tailwind } from '../../lib/styles'
    import { ThemeController } from '../../lib/theme'
    import { icon } from '../../lib/icon'
    // Side-effect import: registers <uip-command-dialog> for standalone use
    // (consumers who install only this block still get a working dialog).
    import '../../components/command/command'
    
    export interface CommandPaletteItem {
      label: string
      hint?: string
      icon?: IconNode
      iconName?: string
      onSelect?: () => void
    }
    
    export interface CommandPaletteGroup {
      heading: string
      items: CommandPaletteItem[]
    }
    
    const defaultGroups: CommandPaletteGroup[] = [
      {
        heading: 'Navigate',
        items: [
          { label: 'Dashboard', hint: '/dashboard', icon: LayoutDashboard, iconName: 'layout-dashboard' },
          { label: 'Inbox', hint: '/inbox', icon: Inbox, iconName: 'inbox' },
          { label: 'Kanban', hint: '/kanban', icon: SquareKanban, iconName: 'square-kanban' },
          { label: 'Team', hint: '/team', icon: Users, iconName: 'users' },
        ],
      },
      {
        heading: 'Settings',
        items: [
          { label: 'Profile', hint: '/profile', icon: FileText, iconName: 'file-text' },
          { label: 'Settings', hint: '/settings', icon: Settings, iconName: 'settings' },
        ],
      },
    ]
    
    /**
     * <uip-command-palette> — the registry CommandPalette block as a web component.
     *
     * Class strings are React's CommandPalette verbatim. React state (open,
     * platform shortcut) becomes @state fields; the ⌘K / Ctrl-K global shortcut
     * is wired in connectedCallback. `onSelect` becomes a `select` CustomEvent
     * (bubbles, composed, detail { label, hint }); a per-item `onSelect` in a
     * programmatically-set `groups` array is still called. `groups` is a property
     * (no attribute — structured data).
     *
     * Deviation: React's `KanbanSquare` icon is the `SquareKanban` IconNode here
     * (the installed `lucide` package only exports `SquareKanban`; same glyph).
     */
    export class UipCommandPalette extends LitElement {
      static shadowRootOptions = { ...LitElement.shadowRootOptions, delegatesFocus: true }
      // :host display can't be a utility class (the host has no template of its own).
      static styles = [tailwind, css`:host { display: block; }`]
    
      static properties = {
        groups: { attribute: false },
        placeholder: {},
        triggerLabel: { attribute: 'trigger-label' },
        showTrigger: {
          type: Boolean,
          attribute: 'show-trigger',
          converter: { fromAttribute: (v: string | null) => v !== null && v !== 'false' },
        },
        open: { state: true },
        triggerShortcut: { state: true },
      }
    
      groups: CommandPaletteGroup[] = defaultGroups
      placeholder = 'Search pages, commands…'
      triggerLabel = 'Search pages, commands…'
      showTrigger = true
      private open = false
      private triggerShortcut = '⌘K'
    
      constructor() {
        super()
        new ThemeController(this)
      }
    
      connectedCallback() {
        super.connectedCallback()
        this.setAttribute('data-uipkge', '')
        this.setAttribute('data-slot', 'command-palette')
        if (!isServer && typeof navigator !== 'undefined') {
          this.triggerShortcut = /Mac|iPhone|iPad/i.test(navigator.platform) ? '⌘K' : 'Ctrl K'
          window.addEventListener('keydown', this.onKeydown)
        }
      }
    
      disconnectedCallback() {
        super.disconnectedCallback()
        if (!isServer && typeof window !== 'undefined') window.removeEventListener('keydown', this.onKeydown)
      }
    
      private onKeydown = (e: KeyboardEvent) => {
        if (e.key === 'k' && (e.metaKey || e.ctrlKey)) {
          e.preventDefault()
          this.open = !this.open
        }
      }
    
      private pick(item: CommandPaletteItem) {
        this.open = false
        item.onSelect?.()
        this.dispatchEvent(
          new CustomEvent('select', { detail: { label: item.label, hint: item.hint }, bubbles: true, composed: true }),
        )
      }
    
      render() {
        return html`
          ${this.showTrigger
            ? html`<button
                type="button"
                class="bg-secondary/50 hover:bg-secondary text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 relative hidden h-8 w-full items-center gap-2 rounded-lg border border-transparent px-2.5 text-sm shadow-none transition-colors outline-none focus-visible:ring-[3px] sm:flex md:w-[220px] lg:w-[300px]"
                aria-label="Open command palette"
                @click=${() => {
                  this.open = true
                }}
              >
                ${icon(Search, 'search', 'size-3.5 shrink-0')}
                <span class="flex-1 truncate text-left">${this.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-xs font-medium"
                >
                  <span>${this.triggerShortcut}</span>
                </kbd>
              </button>`
            : nothing}
    
          <uip-command-dialog
            ?open=${this.open}
            placeholder=${this.placeholder}
            heading="Command palette"
            description="Search pages and run commands"
            @open-change=${(e: CustomEvent<{ open: boolean }>) => {
              this.open = e.detail.open
            }}
          >
            <uip-command-empty>No matches.</uip-command-empty>
            ${this.groups.map(
              (group, gi) => html`
                <uip-command-group heading=${group.heading}>
                  ${group.items.map(
                    (item) => html`
                      <uip-command-item
                        data-slot="command-palette"
                        value=${`${group.heading} ${item.label} ${item.hint ?? ''}`}
                        @select=${() => this.pick(item)}
                      >
                        ${item.icon ? icon(item.icon, item.iconName ?? 'icon', 'size-4') : nothing}
                        <span>${item.label}</span>
                        ${item.hint ? html`<uip-command-shortcut>${item.hint}</uip-command-shortcut>` : nothing}
                      </uip-command-item>
                    `,
                  )}
                </uip-command-group>
                ${gi < this.groups.length - 1 ? html`<uip-command-separator></uip-command-separator>` : nothing}
              `,
            )}
          </uip-command-dialog>
        `
      }
    }
    
    customElements.get('uip-command-palette') || customElements.define('uip-command-palette', UipCommandPalette)
    
    declare global {
      interface HTMLElementTagNameMap {
        'uip-command-palette': UipCommandPalette
      }
    }
    

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