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 emits `select`. `show-trigger=false` hides the inline button so the consumer can fire it from elsewhere via the exposed `show()` / `toggle()` methods.

Installation

$npx uipkge-ng@latest add command-palette
Manifest:https://uipkge.dev/r/angular/command-palette.jsonInstalls to:src/app/components/blocks/command-palette/

Variants

Loading interactive previews…

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?: LucideIconData
  onSelect?: () => void
}
CommandPaletteGroup
interface CommandPaletteGroup {
  heading: string
  items: CommandPaletteItem[]
}

npm dependencies

Includes

Files installed (2)

  • src/app/components/blocks/command-palette/command-palette.component.ts5.4 kB
    import {
      Component,
      EventEmitter,
      Input,
      Output,
      booleanAttribute,
      signal,
      ChangeDetectionStrategy,
    } from '@angular/core'
    import {
      FileText,
      Inbox,
      KanbanSquare,
      LayoutDashboard,
      LucideAngularModule,
      Search,
      Settings,
      Users,
      type LucideIconData,
    } from 'lucide-angular'
    import {
      UiCommandDialogComponent,
      UiCommandEmptyComponent,
      UiCommandGroupComponent,
      UiCommandInputComponent,
      UiCommandItemComponent,
      UiCommandListComponent,
      UiCommandSeparatorComponent,
      UiCommandShortcutComponent,
    } from '@/ui/command/command.component'
    
    export interface CommandPaletteItem {
      label: string
      value?: string
      hint?: string
      icon?: LucideIconData
      onSelect?: () => void
    }
    
    export interface CommandPaletteGroup {
      heading: string
      items: CommandPaletteItem[]
    }
    
    const defaultGroups: CommandPaletteGroup[] = [
      {
        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 },
        ],
      },
    ]
    
    /**
     * CommandPalette -- the React block, 1:1: a slim header search trigger (with a
     * platform-aware Cmd-K / Ctrl-K hint) plus the modal CommandDialog. Drop it once and
     * the global keyboard shortcut wires itself. `[showTrigger]="false"` hides the inline
     * button; open it from elsewhere through a template ref (`palette.show()` / `toggle()`).
     */
    @Component({
      changeDetection: ChangeDetectionStrategy.Eager,
      selector: 'ui-command-palette, [ui-command-palette]',
      standalone: true,
      // React renders a fragment (trigger + dialog): keep this wrapper out of the layout.
      host: { class: 'contents', '(window:keydown)': 'onKeydown($event)' },
      imports: [
        LucideAngularModule,
        UiCommandDialogComponent,
        UiCommandEmptyComponent,
        UiCommandGroupComponent,
        UiCommandInputComponent,
        UiCommandItemComponent,
        UiCommandListComponent,
        UiCommandSeparatorComponent,
        UiCommandShortcutComponent,
      ],
      template: `
        @if (showTrigger) {
          <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)="show()"
          >
            <lucide-icon [img]="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-xs font-medium"
            >
              <span>{{ triggerShortcut }}</span>
            </kbd>
          </button>
        }
    
        <ui-command-dialog
          [open]="open()"
          (openChange)="setOpen($event)"
          title="Command palette"
          description="Search pages and run commands"
        >
          <ui-command-input [placeholder]="placeholder" />
          <ui-command-list class="max-h-[480px]">
            <ui-command-empty>No matches.</ui-command-empty>
            @for (group of groups; track group.heading; let last = $last) {
              <ui-command-group [heading]="group.heading">
                @for (item of group.items; track item.label) {
                  <ui-command-item
                    data-slot="command-palette"
                    [value]="group.heading + ' ' + item.label + ' ' + (item.hint ?? '')"
                    (select)="pick(item)"
                  >
                    @if (item.icon) {
                      <lucide-icon [img]="item.icon" class="size-4" />
                    }
                    <span>{{ item.label }}</span>
                    @if (item.hint) {
                      <ui-command-shortcut class="text-muted-foreground/70">{{ item.hint }}</ui-command-shortcut>
                    }
                  </ui-command-item>
                }
              </ui-command-group>
              @if (!last) {
                <ui-command-separator />
              }
            }
          </ui-command-list>
        </ui-command-dialog>
      `,
    })
    export class UiCommandPaletteComponent {
      protected readonly Search = Search
    
      @Input() groups: CommandPaletteGroup[] = defaultGroups
      @Input() placeholder = 'Search pages, commands…'
      @Input() triggerLabel = 'Search pages, commands…'
      @Input({ transform: booleanAttribute }) showTrigger = true
      /** Fires after an item's own `onSelect`, with the picked item (React `onSelect`). */
      @Output() select = new EventEmitter<CommandPaletteItem>()
    
      readonly open = signal(false)
      readonly triggerShortcut =
        typeof navigator === 'undefined' ? '⌘K' : /Mac|iPhone|iPad/i.test(navigator.platform) ? '⌘K' : 'Ctrl K'
    
      setOpen(value: boolean): void {
        if (value === this.open()) return
        this.open.set(value)
      }
    
      show(): void {
        this.setOpen(true)
      }
    
      hide(): void {
        this.setOpen(false)
      }
    
      toggle(): void {
        this.setOpen(!this.open())
      }
    
      pick(item: CommandPaletteItem): void {
        this.hide()
        item.onSelect?.()
        this.select.emit(item)
      }
    
      onKeydown(e: KeyboardEvent): void {
        if (e.key === 'k' && (e.metaKey || e.ctrlKey)) {
          e.preventDefault()
          this.toggle()
        }
      }
    }
    
  • src/app/components/blocks/command-palette/index.ts0.1 kB

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