# Command Palette

Use `ui-command-palette` for global command search, quick navigation, and action launching in dense workspaces. Good palettes reduce hunting through menus and sidebars by making intent searchable.

## Import
```ts
import { CommandPaletteComponent, type CommandPaletteItem } from 'ui';
```

## Basic action launcher
```ts
import { Component, signal } from '@angular/core';
import { ButtonComponent, CommandPaletteComponent, type CommandPaletteItem } from 'ui';

const WORKSPACE_ITEMS: Omit<CommandPaletteItem, 'action'>[] = [
  {
    id: 'go-dashboard',
    label: 'Go to dashboard',
    description: 'Open the main workspace dashboard',
    icon: 'apps',
    group: 'Navigation',
    keywords: ['home', 'overview', 'dashboard'],
  },
  {
    id: 'go-projects',
    label: 'Open projects',
    description: 'Browse active and archived projects',
    icon: 'folder',
    group: 'Navigation',
    keywords: ['projects', 'workspaces', 'files'],
  },
  {
    id: 'new-project',
    label: 'Create project',
    description: 'Start a new project from a template',
    icon: 'document_add',
    group: 'Actions',
    keywords: ['new', 'create', 'project'],
  },
  {
    id: 'invite-user',
    label: 'Invite teammate',
    description: 'Send an invite to a new collaborator',
    icon: 'person_add',
    group: 'Actions',
    keywords: ['invite', 'user', 'teammate'],
  },
  {
    id: 'open-shortcuts',
    label: 'Show keyboard shortcuts',
    description: 'Open the keyboard shortcuts reference',
    icon: 'keyboard',
    group: 'Help',
    keywords: ['shortcuts', 'help', 'keyboard'],
  },
];

@Component({
  selector: 'app-command-palette-basic-demo',
  standalone: true,
  imports: [ButtonComponent, CommandPaletteComponent],
  template: `
    <div style="display:flex;flex-direction:column;gap:1rem;width:100%;max-width:36rem">
      <div
        style="display:flex;flex-wrap:wrap;gap:0.75rem;align-items:center;padding:0.75rem 0.875rem;border:1px dashed var(--color-neutral-stroke-rest);border-radius:1rem;background:var(--color-neutral-background2-rest)"
      >
        <ui-button variant="primary" (click)="visible.set(true)">Open command palette</ui-button>
        <ui-button variant="secondary" appearance="outline" (click)="reset()">Reset</ui-button>
        <span style="font-size:0.8125rem;color:var(--color-neutral-foreground2-rest)">
          Last action: <strong>{{ lastAction() || 'none' }}</strong>
        </span>
      </div>

      <ui-command-palette
        [(visible)]="visible"
        [items]="items"
        placeholder="Type a command or search..."
        emptyText="No commands found"
        [maxResults]="8"
        (commandExecuted)="onExecuted($event)"
        (closed)="onClosed()"
      />
    </div>
  `,
})
export class CommandPaletteBasicDemoComponent {
  protected readonly visible = signal(false);
  protected readonly lastAction = signal('');

  protected readonly items = WORKSPACE_ITEMS.map(item => ({
    ...item,
    action: () => this.lastAction.set(item.label),
  }));

  protected onExecuted(item: CommandPaletteItem): void {
    this.lastAction.set(item.label);
  }

  protected onClosed(): void {
    if (!this.lastAction()) {
      this.lastAction.set('palette closed');
    }
  }

  protected reset(): void {
    this.visible.set(false);
    this.lastAction.set('');
  }
}
```

## Grouped versus flat commands
```ts
import { Component, signal } from '@angular/core';
import { ButtonComponent, CommandPaletteComponent, type CommandPaletteItem } from 'ui';

const GROUPED_ITEMS: CommandPaletteItem[] = [
  {
    id: 'go-dashboard',
    label: 'Go to dashboard',
    description: 'Open the main workspace dashboard',
    icon: 'apps',
    group: 'Navigation',
    keywords: ['home', 'overview', 'dashboard'],
    action: () => {},
  },
  {
    id: 'go-projects',
    label: 'Open projects',
    description: 'Browse active and archived projects',
    icon: 'folder',
    group: 'Navigation',
    keywords: ['projects', 'workspaces', 'files'],
    action: () => {},
  },
  {
    id: 'new-project',
    label: 'Create project',
    description: 'Start a new project from a template',
    icon: 'document_add',
    group: 'Actions',
    keywords: ['new', 'create', 'project'],
    action: () => {},
  },
  {
    id: 'invite-user',
    label: 'Invite teammate',
    description: 'Send an invite to a new collaborator',
    icon: 'person_add',
    group: 'Actions',
    keywords: ['invite', 'user', 'teammate'],
    action: () => {},
  },
  {
    id: 'open-shortcuts',
    label: 'Show keyboard shortcuts',
    description: 'Open the keyboard shortcuts reference',
    icon: 'keyboard',
    group: 'Help',
    keywords: ['shortcuts', 'help', 'keyboard'],
    action: () => {},
  },
];

@Component({
  selector: 'app-command-palette-grouping-demo',
  standalone: true,
  imports: [ButtonComponent, CommandPaletteComponent],
  template: `
    <div
      style="display:flex;flex-wrap:wrap;gap:1rem;align-items:flex-start;width:100%;max-width:48rem"
    >
      <div
        style="flex:1 1 18rem;padding:1rem;border:1px solid var(--color-neutral-stroke-rest);border-radius:1rem;background:var(--color-neutral-background-rest)"
      >
        <p
          style="margin:0 0 0.75rem;font-size:0.875rem;font-weight:600;color:var(--color-neutral-foreground2-rest)"
        >
          Grouped commands
        </p>
        <ui-button variant="primary" appearance="outline" (click)="groupedVisible.set(true)">
          Open grouped palette
        </ui-button>
        <ui-command-palette
          [(visible)]="groupedVisible"
          [items]="groupedItems"
          placeholder="Search grouped commands..."
          emptyText="No grouped commands found"
          [maxResults]="8"
        />
      </div>

      <div
        style="flex:1 1 18rem;padding:1rem;border:1px solid var(--color-neutral-stroke-rest);border-radius:1rem;background:var(--color-neutral-background-rest)"
      >
        <p
          style="margin:0 0 0.75rem;font-size:0.875rem;font-weight:600;color:var(--color-neutral-foreground2-rest)"
        >
          Flat commands
        </p>
        <ui-button variant="secondary" appearance="outline" (click)="flatVisible.set(true)">
          Open flat palette
        </ui-button>
        <ui-command-palette
          [(visible)]="flatVisible"
          [items]="flatItems"
          placeholder="Search flat commands..."
          emptyText="No flat commands found"
          [maxResults]="8"
        />
      </div>
    </div>
  `,
})
export class CommandPaletteGroupingDemoComponent {
  protected readonly groupedVisible = signal(false);
  protected readonly flatVisible = signal(false);

  protected readonly groupedItems = GROUPED_ITEMS;
  protected readonly flatItems = GROUPED_ITEMS.map(({ group, ...item }) => item);
}
```

## Search-heavy, disabled, and configured palettes
```ts
import { Component, signal } from '@angular/core';
import { ButtonComponent, CommandPaletteComponent, type CommandPaletteItem } from 'ui';

const SEARCH_ITEMS: CommandPaletteItem[] = [
  {
    id: 'format-document',
    label: 'Format document',
    description: 'Auto-format the current document',
    icon: 'text_align_justify',
    keywords: ['format', 'beautify', 'code', 'document'],
    action: () => {},
  },
  {
    id: 'build-project',
    label: 'Build project',
    description: 'Compile and validate the current project',
    icon: 'wrench',
    keywords: ['build', 'compile', 'validate', 'project'],
    action: () => {},
  },
  {
    id: 'run-tests',
    label: 'Run tests',
    description: 'Execute all available test suites',
    icon: 'beaker',
    keywords: ['test', 'qa', 'verify', 'checks'],
    action: () => {},
  },
  {
    id: 'deploy-preview',
    label: 'Deploy preview',
    description: 'Publish a preview build to the review environment',
    icon: 'arrow_upload',
    keywords: ['deploy', 'preview', 'publish', 'release'],
    action: () => {},
  },
  {
    id: 'save-all',
    label: 'Save all',
    description: 'Persist all open changes',
    icon: 'save',
    keywords: ['save', 'store', 'persist'],
    action: () => {},
  },
];

const DISABLED_ITEMS: CommandPaletteItem[] = [
  {
    id: 'sync-now',
    label: 'Sync now',
    description: 'Synchronize the current workspace',
    icon: 'arrow_sync',
    group: 'Actions',
    keywords: ['sync', 'refresh'],
    action: () => {},
  },
  {
    id: 'premium-export',
    label: 'Export analytics',
    description: 'Requires a premium plan',
    icon: 'chart_multiple',
    group: 'Actions',
    keywords: ['export', 'analytics', 'premium'],
    disabled: true,
    action: () => {},
  },
  {
    id: 'locked-admin',
    label: 'Admin console',
    description: 'Unavailable without admin access',
    icon: 'shield',
    group: 'Navigation',
    keywords: ['admin', 'console', 'permissions'],
    disabled: true,
    action: () => {},
  },
  {
    id: 'open-billing',
    label: 'Open billing',
    description: 'Review billing and invoices',
    icon: 'wallet',
    group: 'Navigation',
    keywords: ['billing', 'invoices', 'payments'],
    action: () => {},
  },
];

@Component({
  selector: 'app-command-palette-options-demo',
  standalone: true,
  imports: [ButtonComponent, CommandPaletteComponent],
  template: `
    <div style="display:flex;flex-direction:column;gap:1rem;width:100%;max-width:40rem">
      <div
        style="display:flex;flex-wrap:wrap;gap:0.75rem;align-items:center;padding:0.75rem 0.875rem;border:1px dashed var(--color-neutral-stroke-rest);border-radius:1rem;background:var(--color-neutral-background2-rest)"
      >
        <ui-button variant="primary" (click)="searchVisible.set(true)"
          >Open search-heavy palette</ui-button
        >
        <ui-button variant="secondary" appearance="outline" (click)="disabledVisible.set(true)">
          Open disabled-items palette
        </ui-button>
        <ui-button variant="secondary" appearance="outline" (click)="reset()">Reset</ui-button>
      </div>

      <ui-command-palette
        [(visible)]="searchVisible"
        [items]="searchItems"
        placeholder="Search commands, tests, builds..."
        emptyText="Try another keyword"
        emptyDescription="Search matches label, description, and keywords."
        [maxResults]="5"
      />

      <ui-command-palette
        [(visible)]="disabledVisible"
        [items]="disabledItems"
        placeholder="What would you like to do?"
        emptyText="No matching actions found"
        [maxResults]="8"
      />
    </div>
  `,
})
export class CommandPaletteOptionsDemoComponent {
  protected readonly searchVisible = signal(false);
  protected readonly disabledVisible = signal(false);

  protected readonly searchItems = SEARCH_ITEMS;
  protected readonly disabledItems = DISABLED_ITEMS;

  protected reset(): void {
    this.searchVisible.set(false);
    this.disabledVisible.set(false);
  }
}
```

## No matching results
```ts
import { Component, signal } from '@angular/core';
import { ButtonComponent, CommandPaletteComponent, type CommandPaletteItem } from 'ui';

const NAVIGATION_ITEMS: CommandPaletteItem[] = [
  {
    id: 'nav-inbox',
    label: 'Inbox',
    description: 'Review pending notifications and mentions',
    icon: 'mail',
    group: 'Navigation',
    keywords: ['mail', 'mentions', 'notifications'],
    action: () => {},
  },
  {
    id: 'nav-roadmap',
    label: 'Roadmap',
    description: 'Open the team roadmap view',
    icon: 'rocket',
    group: 'Navigation',
    keywords: ['roadmap', 'planning'],
    action: () => {},
  },
  {
    id: 'nav-settings',
    label: 'Settings',
    description: 'Configure account and workspace settings',
    icon: 'settings',
    group: 'Navigation',
    keywords: ['settings', 'preferences'],
    action: () => {},
  },
  {
    id: 'nav-help',
    label: 'Help center',
    description: 'Browse support articles and product docs',
    icon: 'question_circle',
    group: 'Support',
    keywords: ['docs', 'support', 'help'],
    action: () => {},
  },
];

@Component({
  selector: 'app-command-palette-empty-results-demo',
  standalone: true,
  imports: [ButtonComponent, CommandPaletteComponent],
  template: `
    <div
      style="display:flex;flex-direction:column;gap:1rem;width:100%;max-width:40rem;padding:1rem;border:1px solid var(--color-neutral-stroke-rest);border-radius:1rem;background:var(--color-neutral-background-rest)"
    >
      <div style="font-size:0.875rem;line-height:1.5;color:var(--color-neutral-foreground2-rest)">
        Good empty results should explain what happened without turning into another generic empty
        state screen.
      </div>

      <div
        style="display:flex;flex-wrap:wrap;gap:0.75rem;align-items:center;padding:0.75rem 0.875rem;border:1px dashed var(--color-neutral-stroke-rest);border-radius:1rem;background:var(--color-neutral-background2-rest)"
      >
        <ui-button variant="primary" (click)="visible.set(true)">Open palette</ui-button>
        <ui-button variant="secondary" appearance="outline" (click)="visible.set(false)"
          >Close</ui-button
        >
      </div>

      <ui-command-palette
        [(visible)]="visible"
        [items]="items"
        placeholder="Search docs or support..."
        emptyText="No matching actions found"
        emptyDescription="Try another keyword such as dashboard, projects, or help."
        [maxResults]="6"
      />
    </div>
  `,
})
export class CommandPaletteEmptyResultsDemoComponent {
  protected readonly visible = signal(false);
  protected readonly items = NAVIGATION_ITEMS;
}
```

## Workspace command center
```ts
import { Component, signal } from '@angular/core';
import {
  ButtonComponent,
  KbdComponent,
  CommandPaletteComponent,
  type CommandPaletteItem,
} from 'ui';

const WORKSPACE_ITEMS: Omit<CommandPaletteItem, 'action'>[] = [
  {
    id: 'go-dashboard',
    label: 'Go to dashboard',
    description: 'Open the main workspace dashboard',
    icon: 'apps',
    group: 'Navigation',
    keywords: ['home', 'overview', 'dashboard'],
  },
  {
    id: 'go-projects',
    label: 'Open projects',
    description: 'Browse active and archived projects',
    icon: 'folder',
    group: 'Navigation',
    keywords: ['projects', 'workspaces', 'files'],
  },
  {
    id: 'new-project',
    label: 'Create project',
    description: 'Start a new project from a template',
    icon: 'document_add',
    group: 'Actions',
    keywords: ['new', 'create', 'project'],
  },
  {
    id: 'invite-user',
    label: 'Invite teammate',
    description: 'Send an invite to a new collaborator',
    icon: 'person_add',
    group: 'Actions',
    keywords: ['invite', 'user', 'teammate'],
  },
  {
    id: 'open-shortcuts',
    label: 'Show keyboard shortcuts',
    description: 'Open the keyboard shortcuts reference',
    icon: 'keyboard',
    group: 'Help',
    keywords: ['shortcuts', 'help', 'keyboard'],
  },
];

@Component({
  selector: 'app-command-palette-workspace-demo',
  standalone: true,
  imports: [ButtonComponent, KbdComponent, CommandPaletteComponent],
  template: `
    <div
      style="display:flex;flex-direction:column;gap:1rem;width:100%;max-width:58rem;padding:1rem;border:1px solid var(--color-neutral-stroke-rest);border-radius:1rem;background:var(--color-neutral-background-rest)"
    >
      <div
        style="display:flex;flex-wrap:wrap;align-items:center;justify-content:space-between;gap:0.75rem"
      >
        <div>
          <div style="font-size:0.875rem;font-weight:600">Workspace command center</div>
          <div style="font-size:0.8125rem;color:var(--color-neutral-foreground2-rest)">
            A realistic palette sits behind a global shortcut or shell action, not only a showcase
            button.
          </div>
        </div>
        <div style="display:flex;align-items:center;gap:0.5rem">
          <ui-kbd text="Ctrl" />
          <span>+</span>
          <ui-kbd text="K" />
        </div>
      </div>

      <div
        style="display:flex;flex-wrap:wrap;gap:0.75rem;align-items:center;padding:0.75rem 0.875rem;border:1px dashed var(--color-neutral-stroke-rest);border-radius:1rem;background:var(--color-neutral-background2-rest)"
      >
        <ui-button variant="primary" (click)="visible.set(true)">Open workspace palette</ui-button>
        <ui-button variant="secondary" appearance="outline" (click)="reset()">Reset</ui-button>
        <span style="font-size:0.8125rem;color:var(--color-neutral-foreground2-rest)">
          Last action: <strong>{{ lastAction() || 'none' }}</strong>
        </span>
      </div>

      <ui-command-palette
        [(visible)]="visible"
        [items]="items"
        placeholder="Search actions, projects, teammates..."
        emptyText="No workspace commands found"
        [maxResults]="8"
        (commandExecuted)="onExecuted($event)"
      />
    </div>
  `,
})
export class CommandPaletteWorkspaceDemoComponent {
  protected readonly visible = signal(false);
  protected readonly lastAction = signal('');

  protected readonly items = WORKSPACE_ITEMS.map(item => ({
    ...item,
    action: () => this.lastAction.set(item.label),
  }));

  protected onExecuted(item: CommandPaletteItem): void {
    this.lastAction.set(item.label);
  }

  protected reset(): void {
    this.visible.set(false);
    this.lastAction.set('');
  }
}
```

## Accessibility

### Dialog and result semantics
`ui-command-palette` renders a modal command surface with a focused search field and a result list. Results behave like selectable options and the component keeps keyboard focus trapped while open.

### Keyboard behavior
The command palette is primarily a keyboard-first surface.

| Key | Action |
| --- | --- |
| ArrowUp / ArrowDown | Move the active selection between enabled commands. |
| Enter | Execute the currently selected command. |
| Esc | Close the command palette. |
| Tab / Shift+Tab | Move focus within the trapped dialog surface. |

### Labels and discoverability
Commands should have concise labels, helpful descriptions when needed, and enough keywords to support search. Disabled commands should stay understandable rather than appearing as broken or missing actions.
