Command Palette
blocklayoutHeader-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
$pnpm dlx shadcn@latest add https://uipkge.dev/r/lit/command-palette.json$npx shadcn@latest add https://uipkge.dev/r/lit/command-palette.json$yarn dlx shadcn@latest add https://uipkge.dev/r/lit/command-palette.json$bunx shadcn@latest add https://uipkge.dev/r/lit/command-palette.jsonManifest:
https://uipkge.dev/r/lit/command-palette.jsonInstalls to:src/uip/blocks/command-palette/Variants
Loading interactive previews…
Loading interactive previews…
Props
| Name | Type / Values | Default | Required |
|---|---|---|---|
placeholder | string | Search pages, commands… | optional |
trigger-label | string | Search pages, commands… | optional |
converter | string | — | optional |
Schema
Type aliases exported from this item's source. Use these to shape the data you pass in.
CommandPaletteIteminterface CommandPaletteItem {
label: string
hint?: string
icon?: IconNode
iconName?: string
onSelect?: () => void
}CommandPaletteGroupinterface CommandPaletteGroup {
heading: string
items: CommandPaletteItem[]
}HTMLElementTagNameMapinterface HTMLElementTagNameMap {
'uip-command-palette': UipCommandPalette
}
}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