Menu

Change language

GitHub repo

Header — Inline Search

blockmarketing

Navbar with an inline search trigger that opens a command palette on click or ⌘K, listing grouped results with keyboard hints alongside the usual nav links and CTA.

Installation

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

Variants

Loading interactive previews…

Schema

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

HeaderSearchLink
interface HeaderSearchLink {
  label: string
  href: string
}
HeaderSearchItem
interface HeaderSearchItem {
  icon: LucideIconData
  label: string
}
HeaderSearchGroup
interface HeaderSearchGroup {
  heading: string
  items: HeaderSearchItem[]
}

Files installed (2)

  • src/app/components/blocks/header-search-command/header-search-command.component.ts7 kB
    // Navbar with an inline search trigger that opens a command palette on click or ⌘K,
    // listing grouped results with keyboard hints alongside nav links and CTA.
    // Port of the React block 1:1.
    import { ChangeDetectionStrategy, Component, Input, signal } from '@angular/core'
    import {
      BarChart3,
      BookOpen,
      Compass,
      FileText,
      History,
      LucideAngularModule,
      Menu,
      Search,
      Users,
      type LucideIconData,
    } from 'lucide-angular'
    import { cn } from '@/lib/utils'
    import { UiButtonComponent } from '@/ui/button/button.component'
    import {
      UiCommandDialogComponent,
      UiCommandEmptyComponent,
      UiCommandGroupComponent,
      UiCommandInputComponent,
      UiCommandItemComponent,
      UiCommandListComponent,
    } from '@/ui/command/command.component'
    import { UiKbdComponent } from '@/ui/kbd/kbd.component'
    import { UiSeparatorComponent } from '@/ui/separator/separator.component'
    import {
      UiSheetComponent,
      UiSheetContentComponent,
      UiSheetHeaderComponent,
      UiSheetTitleComponent,
      UiSheetTriggerComponent,
    } from '@/ui/sheet/sheet.component'
    
    interface HeaderSearchLink {
      label: string
      href: string
    }
    
    interface HeaderSearchItem {
      icon: LucideIconData
      label: string
    }
    
    interface HeaderSearchGroup {
      heading: string
      items: HeaderSearchItem[]
    }
    
    const links: HeaderSearchLink[] = [
      { label: 'Product', href: '#product' },
      { label: 'Pricing', href: '#pricing' },
      { label: 'Docs', href: '#docs' },
    ]
    
    const groups: HeaderSearchGroup[] = [
      {
        heading: 'Documentation',
        items: [
          { icon: BookOpen, label: 'Connect a warehouse' },
          { icon: FileText, label: 'Define your first metric' },
          { icon: BarChart3, label: 'Publish a dashboard' },
        ],
      },
      {
        heading: 'Company',
        items: [
          { icon: Users, label: 'Customer stories' },
          { icon: History, label: 'Changelog' },
        ],
      },
    ]
    
    @Component({
      changeDetection: ChangeDetectionStrategy.Eager,
      selector: 'ui-header-search-command, [ui-header-search-command]',
      standalone: true,
      // React renders the root <header> itself: the host stays out of layout and `class` goes to the root.
      host: { '[attr.class]': '"contents"', '(window:keydown)': 'onKeydown($event)' },
      imports: [
        LucideAngularModule,
        UiButtonComponent,
        UiCommandDialogComponent,
        UiCommandEmptyComponent,
        UiCommandGroupComponent,
        UiCommandInputComponent,
        UiCommandItemComponent,
        UiCommandListComponent,
        UiKbdComponent,
        UiSeparatorComponent,
        UiSheetComponent,
        UiSheetContentComponent,
        UiSheetHeaderComponent,
        UiSheetTitleComponent,
        UiSheetTriggerComponent,
      ],
      template: `
        <header data-slot="header-search-command" [class]="rootClass">
          <div class="mx-auto flex h-16 max-w-6xl items-center gap-4 px-6">
            <a href="#top" class="flex shrink-0 items-center gap-2 font-semibold tracking-tight">
              <lucide-icon [img]="Compass" class="text-primary size-5" />
              Halden
            </a>
    
            <nav class="hidden items-center gap-1 md:flex" aria-label="Primary">
              @for (link of navLinks; track link.href) {
                <a
                  [href]="link.href"
                  class="text-muted-foreground hover:text-foreground hover:bg-muted focus-visible:ring-ring rounded-md px-3 py-1.5 text-sm font-medium transition-colors focus-visible:ring-2 focus-visible:outline-none"
                >
                  {{ link.label }}
                </a>
              }
            </nav>
    
            <!-- Trigger is a button, not an input: the real field lives in the dialog,
                 so focus lands there and the shortcut hint stays visible until then. -->
            <button
              type="button"
              class="border-border bg-muted/40 text-muted-foreground hover:bg-muted focus-visible:ring-ring ml-auto flex h-9 w-full max-w-64 items-center gap-2 rounded-md border px-3 text-sm transition-colors focus-visible:ring-2 focus-visible:outline-none"
              (click)="open.set(true)"
            >
              <lucide-icon [img]="Search" class="size-4 shrink-0" />
              <span class="truncate">Search docs…</span>
              <span ui-kbd class="ml-auto hidden sm:inline-flex">⌘K</span>
            </button>
    
            <ui-separator orientation="vertical" class="hidden h-5 md:block" />
            <button ui-button size="sm" class="hidden shrink-0 sm:inline-flex">Start free</button>
    
            <div ui-sheet [open]="menuOpen()" (openChange)="menuOpen.set($event)">
              <button
                ui-button
                ui-sheet-trigger
                variant="ghost"
                size="icon"
                class="shrink-0 md:hidden"
                aria-label="Open navigation menu"
              >
                <lucide-icon [img]="Menu" class="size-5" />
              </button>
              <div ui-sheet-content side="right" class="w-72">
                <div ui-sheet-header>
                  <h2 ui-sheet-title>Menu</h2>
                </div>
                <nav class="mt-6 grid gap-1" aria-label="Mobile">
                  @for (link of navLinks; track link.href) {
                    <a
                      [href]="link.href"
                      class="hover:bg-muted rounded-md px-3 py-2 text-sm font-medium transition-colors"
                      (click)="menuOpen.set(false)"
                    >
                      {{ link.label }}
                    </a>
                  }
                </nav>
                <ui-separator class="my-5" />
                <div class="grid gap-2">
                  <button ui-button variant="outline">Sign in</button>
                  <button ui-button>Start free</button>
                </div>
              </div>
            </div>
          </div>
    
          <ui-command-dialog
            [open]="open()"
            (openChange)="open.set($event)"
            title="Search documentation"
            description="Search documentation, guides, and stories"
          >
            <ui-command-input placeholder="Search documentation, guides, and stories…" />
            <ui-command-list>
              <ui-command-empty>No results.</ui-command-empty>
              @for (group of searchGroups; track group.heading) {
                <ui-command-group [heading]="group.heading">
                  @for (item of group.items; track item.label) {
                    <ui-command-item [value]="item.label" (select)="open.set(false)">
                      <lucide-icon [img]="item.icon" class="text-muted-foreground mr-2 size-4" />
                      {{ item.label }}
                    </ui-command-item>
                  }
                </ui-command-group>
              }
            </ui-command-list>
          </ui-command-dialog>
        </header>
      `,
    })
    export class UiHeaderSearchCommandComponent {
      protected readonly Compass = Compass
      protected readonly Menu = Menu
      protected readonly Search = Search
    
      protected readonly navLinks = links
      protected readonly searchGroups = groups
    
      @Input('class') className?: string
    
      readonly open = signal(false)
      readonly menuOpen = signal(false)
    
      get rootClass(): string {
        return cn('border-border bg-background/90 border-b backdrop-blur', this.className)
      }
    
      onKeydown(event: KeyboardEvent): void {
        if (event.key.toLowerCase() !== 'k' || !(event.metaKey || event.ctrlKey)) return
        event.preventDefault()
        this.open.update((v) => !v)
      }
    }
    
  • src/app/components/blocks/header-search-command/index.ts0.1 kB

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