Menu

Change language

GitHub repo

Header

blockmarketing

Site header collection: sticky top bar, dismissible announcement stack, centered-logo split, floating pill, full-width mega menu, inline search with command palette, and a transparent-over-hero scroll variant — each with a mobile sheet fallback.

Installation

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

Variants

Loading interactive previews…

Props

NameType / ValuesDefaultRequired
variantHeaderVariantdefaultoptional

Schema

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

MegaItem
interface MegaItem {
  icon: IconNode
  iconName: string
  title: string
  body: string
}
MegaMenu
interface MegaMenu {
  label: string
  groups: { heading: string; items: MegaItem[] }[]
  highlight: { title: string; body: string; cta: string }
}
HTMLElementTagNameMap
interface HTMLElementTagNameMap {
    'uip-header': UipHeader
  }
}

Files installed (1)

  • src/uip/blocks/header/header.ts33.9 kB
    import { LitElement, css, html, isServer, nothing } from 'lit'
    import {
      ArrowRight,
      BarChart3,
      BookOpen,
      Boxes,
      CircleDot,
      Compass,
      FileText,
      History,
      Layers,
      LifeBuoy,
      Lock,
      Menu,
      Search,
      ShoppingBag,
      User,
      Users,
      Workflow,
      X,
      type IconNode,
    } from 'lucide'
    import { cn } from '../../lib/utils'
    import { tailwind } from '../../lib/styles'
    import { ThemeController } from '../../lib/theme'
    import { icon } from '../../lib/icon'
    // Side-effect imports: register the composed elements for standalone use
    // (consumers who install only this block still get working primitives).
    import '../../components/badge/badge'
    import '../../components/button/button'
    import '../../components/command/command'
    import '../../components/kbd/kbd'
    import '../../components/navigation-menu/navigation-menu'
    import '../../components/separator/separator'
    import '../../components/sheet/sheet'
    
    export type HeaderVariant =
      | 'default'
      | 'announcement'
      | 'centered'
      | 'pill'
      | 'mega'
      | 'search'
      | 'transparent'
    
    const STORAGE_KEY = 'uipkge:announcement-dismissed'
    
    const defaultLinks = [
      { label: 'Features', href: '#features' },
      { label: 'Pricing', href: '#pricing' },
      { label: 'Customers', href: '#customers' },
      { label: 'Docs', href: '#docs' },
      { label: 'Blog', href: '#blog' },
    ]
    
    const stackLinks = [
      { label: 'Platform', href: '#platform' },
      { label: 'Pricing', href: '#pricing' },
      { label: 'Customers', href: '#customers' },
      { label: 'Docs', href: '#docs' },
    ]
    
    const centeredLeft = [
      { label: 'Collections', href: '#collections' },
      { label: 'New in', href: '#new' },
      { label: 'Studio', href: '#studio' },
    ]
    const centeredRight = [
      { label: 'Journal', href: '#journal' },
      { label: 'Stockists', href: '#stockists' },
      { label: 'Contact', href: '#contact' },
    ]
    const centeredLinkClass =
      'text-muted-foreground hover:text-foreground 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'
    
    const pillLinks = [
      { label: 'Product', href: '#product' },
      { label: 'Pricing', href: '#pricing' },
      { label: 'Customers', href: '#customers' },
      { label: 'Docs', href: '#docs' },
    ]
    
    interface MegaItem {
      icon: IconNode
      iconName: string
      title: string
      body: string
    }
    
    interface MegaMenu {
      label: string
      groups: { heading: string; items: MegaItem[] }[]
      highlight: { title: string; body: string; cta: string }
    }
    
    // `iconName` beside each icon node: lucide nodes don't carry their kebab-case
    // name, which icon() needs for the lucide-<name> class.
    const megaMenus: MegaMenu[] = [
      {
        label: 'Product',
        groups: [
          {
            heading: 'Core',
            items: [
              { icon: Boxes, iconName: 'boxes', title: 'Metric layer', body: 'Versioned definitions your whole org shares.' },
              {
                icon: BarChart3,
                iconName: 'chart-column',
                title: 'Dashboards',
                body: 'Composed from certified metrics, not raw SQL.',
              },
              { icon: Workflow, iconName: 'workflow', title: 'Pipelines', body: 'Scheduled materialisation with cost ceilings.' },
            ],
          },
          {
            heading: 'Governance',
            items: [
              { icon: Lock, iconName: 'lock', title: 'Row-level access', body: 'Scope evaluated per query, per identity.' },
              {
                icon: BookOpen,
                iconName: 'book-open',
                title: 'Change history',
                body: 'Every definition edit reviewed and revertible.',
              },
              {
                icon: LifeBuoy,
                iconName: 'life-buoy',
                title: 'Audit exports',
                body: 'Generated from the same history, not by hand.',
              },
            ],
          },
        ],
        highlight: {
          title: 'Ship your first dashboard today',
          body: 'Connect a warehouse, publish one certified metric, and invite the team. No migration required.',
          cta: 'Start the walkthrough',
        },
      },
      {
        label: 'Resources',
        groups: [
          {
            heading: 'Learn',
            items: [
              {
                icon: BookOpen,
                iconName: 'book-open',
                title: 'Documentation',
                body: 'Setup, modelling, and the access model.',
              },
              { icon: Layers, iconName: 'layers', title: 'Guides', body: 'Reconciliation and first-close checklists.' },
              {
                icon: BarChart3,
                iconName: 'chart-column',
                title: 'Benchmarks',
                body: 'Close speed across 240 finance teams.',
              },
            ],
          },
          {
            heading: 'Support',
            items: [
              {
                icon: LifeBuoy,
                iconName: 'life-buoy',
                title: 'Help centre',
                body: 'Answers to the questions support gets most.',
              },
              { icon: Workflow, iconName: 'workflow', title: 'Status', body: 'Live uptime and incident history.' },
              { icon: Lock, iconName: 'lock', title: 'Trust centre', body: 'SOC 2, subprocessors, and data residency.' },
            ],
          },
        ],
        highlight: {
          title: 'Read the rollout plan',
          body: 'The five-week implementation we run for every customer, written down so you can run it yourself.',
          cta: 'Open the plan',
        },
      },
    ]
    
    const megaPlainLinks = [
      { label: 'Pricing', href: '#pricing' },
      { label: 'Customers', href: '#customers' },
    ]
    
    const searchLinks = [
      { label: 'Product', href: '#product' },
      { label: 'Pricing', href: '#pricing' },
      { label: 'Docs', href: '#docs' },
    ]
    
    const searchGroups: { heading: string; items: { icon: IconNode; iconName: string; label: string }[] }[] = [
      {
        heading: 'Documentation',
        items: [
          { icon: BookOpen, iconName: 'book-open', label: 'Connect a warehouse' },
          { icon: FileText, iconName: 'file-text', label: 'Define your first metric' },
          { icon: BarChart3, iconName: 'chart-column', label: 'Publish a dashboard' },
        ],
      },
      {
        heading: 'Company',
        items: [
          { icon: Users, iconName: 'users', label: 'Customer stories' },
          { icon: History, iconName: 'history', label: 'Changelog' },
        ],
      },
    ]
    
    const transparentLinks = [
      { label: 'Product', href: '#product' },
      { label: 'Solutions', href: '#solutions' },
      { label: 'Pricing', href: '#pricing' },
      { label: 'Docs', href: '#docs' },
    ]
    
    /**
     * <uip-header> — the registry Header block collection as a web component.
     *
     * One block, one install unit: `variant` selects the section
     * ('default' | 'announcement' | 'centered' | 'pill' | 'mega' | 'search' |
     * 'transparent'). Class strings are React's verbatim per variant. React state
     * (useState) becomes @state fields; mobile Sheets become <uip-sheet> (open +
     * open-change), the mega dropdowns become <uip-navigation-menu>, and the
     * search palette becomes <uip-command-dialog>. Timers, scroll / keydown
     * listeners and the pill ResizeObserver are all torn down in
     * disconnectedCallback.
     */
    export class UipHeader 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 = {
        variant: { reflect: true },
        sheetOpen: { state: true },
        dismissed: { state: true },
        pillActive: { state: true },
        indicator: { state: true },
        searchOpen: { state: true },
        scrolled: { state: true },
      }
    
      variant: HeaderVariant = 'default'
      private sheetOpen = false
      private dismissed = false
      private pillActive = 0
      private indicator = { left: 0, width: 0 }
      private searchOpen = false
      private scrolled = false
      private observer?: ResizeObserver
    
      constructor() {
        super()
        new ThemeController(this)
      }
    
      connectedCallback() {
        super.connectedCallback()
        this.setAttribute('data-uipkge', '')
        this.setAttribute('data-slot', 'header')
        if (isServer) return
        try {
          if (window.sessionStorage.getItem(STORAGE_KEY) === '1') this.dismissed = true
        } catch {
          // Blocked storage: the strip stays visible, like React's first paint.
        }
        window.addEventListener('scroll', this.onScroll, { passive: true })
        this.onScroll()
        window.addEventListener('keydown', this.onKeydown)
      }
    
      disconnectedCallback() {
        super.disconnectedCallback()
        window.removeEventListener('scroll', this.onScroll)
        window.removeEventListener('keydown', this.onKeydown)
        this.observer?.disconnect()
        this.observer = undefined
      }
    
      firstUpdated() {
        this.measurePill()
        if (typeof ResizeObserver === 'undefined') return
        this.observer = new ResizeObserver(() => this.measurePill())
        const nav = this.renderRoot.querySelector('[data-pill-nav]')
        if (nav) this.observer.observe(nav)
      }
    
      private onScroll = () => {
        this.scrolled = window.scrollY > 24
      }
    
      private onKeydown = (e: KeyboardEvent) => {
        if (e.key.toLowerCase() !== 'k' || !(e.metaKey || e.ctrlKey)) return
        e.preventDefault()
        this.searchOpen = !this.searchOpen
      }
    
      private dismiss() {
        this.dismissed = true
        try {
          window.sessionStorage.setItem(STORAGE_KEY, '1')
        } catch {
          // Blocked storage: the dismissal still holds for this page view.
        }
      }
    
      // Measured from the live element, so the indicator fits any label length or
      // font — no hard-coded widths to drift out of sync with the copy.
      private measurePill(index = this.pillActive) {
        const links = this.renderRoot.querySelectorAll<HTMLElement>('[data-pill-link]')
        const el = links[index]
        if (el) this.indicator = { left: el.offsetLeft, width: el.offsetWidth }
      }
    
      private mobileSheet(heading: string, body: unknown, side: 'right' | 'left' | 'top' = 'right') {
        return html`
          <uip-sheet
            side=${side}
            heading=${heading}
            .open=${this.sheetOpen}
            @open-change=${(e: CustomEvent<{ open: boolean }>) => {
              this.sheetOpen = e.detail.open
            }}
          >
            ${body}
          </uip-sheet>
        `
      }
    
      render() {
        switch (this.variant) {
          case 'announcement':
            return this.renderAnnouncement()
          case 'centered':
            return this.renderCentered()
          case 'pill':
            return this.renderPill()
          case 'mega':
            return this.renderMega()
          case 'search':
            return this.renderSearch()
          case 'transparent':
            return this.renderTransparent()
          default:
            return this.renderDefault()
        }
      }
    
      private renderDefault() {
        return html`
          <header
            data-slot="header-01"
            class="bg-background/80 supports-[backdrop-filter]:bg-background/60 sticky top-0 z-40 border-b backdrop-blur"
          >
            <div class="mx-auto flex h-16 max-w-6xl items-center justify-between px-6">
              <a href="#" class="flex items-center gap-2">
                <div class="bg-primary text-primary-foreground flex size-8 items-center justify-center rounded-md">
                  ${icon(Boxes, 'boxes', 'size-4')}
                </div>
                <span class="text-base font-semibold">Acme</span>
              </a>
    
              <nav class="hidden items-center gap-6 md:flex" aria-label="Primary">
                ${defaultLinks.map(
                  (link) => html`<a
                    href=${link.href}
                    class="text-muted-foreground hover:text-foreground text-sm transition-colors"
                  >
                    ${link.label}
                  </a>`,
                )}
              </nav>
    
              <div class="hidden items-center gap-2 md:flex">
                <uip-button variant="ghost" size="sm">Sign in</uip-button>
                <uip-button size="sm">Start free trial</uip-button>
              </div>
    
              <div class="md:hidden">
                ${this.mobileSheet(
                  'Acme',
                  html`
                    <uip-button slot="trigger" variant="ghost" size="icon" aria-label="Open menu">
                      ${icon(Menu, 'menu', 'size-5')}
                    </uip-button>
                    <nav class="flex flex-1 flex-col gap-1 p-4" aria-label="Mobile">
                      ${defaultLinks.map(
                        (link) => html`<a
                          href=${link.href}
                          class="hover:bg-muted rounded-md px-3 py-2 text-sm transition-colors"
                          @click=${() => {
                            this.sheetOpen = false
                          }}
                        >
                          ${link.label}
                        </a>`,
                      )}
                    </nav>
                    <div class="flex flex-col gap-2 border-t p-4">
                      <uip-button variant="outline" class="w-full">Sign in</uip-button>
                      <uip-button class="w-full">Start free trial</uip-button>
                    </div>
                  `,
                )}
              </div>
            </div>
          </header>
        `
      }
    
      private renderAnnouncement() {
        return html`
          <div data-slot="header-announcement-stack" class="sticky top-0 z-40">
            ${!this.dismissed
              ? html`
                  <div class="border-border bg-muted/60 border-b backdrop-blur">
                    <div class="mx-auto flex h-10 max-w-6xl items-center gap-3 px-6 text-sm">
                      <uip-badge variant="secondary" class="shrink-0">New</uip-badge>
                      <p class="min-w-0 truncate">
                        Row-level scoping now evaluates against SCIM groups.
                        <a href="#changelog" class="hover:text-foreground underline underline-offset-4">
                          Read the changelog
                        </a>
                      </p>
                      ${icon(ArrowRight, 'arrow-right', 'text-muted-foreground hidden size-3.5 shrink-0 sm:block')}
                      <uip-button
                        variant="ghost"
                        size="icon"
                        class="ml-auto size-7 shrink-0"
                        aria-label="Dismiss announcement"
                        @click=${this.dismiss}
                      >
                        ${icon(X, 'x', 'size-3.5')}
                      </uip-button>
                    </div>
                  </div>
                `
              : ''}
            <header class="border-border bg-background/90 border-b backdrop-blur">
              <div class="mx-auto flex h-16 max-w-6xl items-center gap-6 px-6">
                <a href="#top" class="flex shrink-0 items-center gap-2 font-semibold tracking-tight">
                  ${icon(Layers, 'layers', 'text-primary size-5')} Northwind
                </a>
    
                <nav class="hidden items-center gap-1 md:flex" aria-label="Primary">
                  ${stackLinks.map(
                    (link) => html`<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>
    
                <div class="ml-auto hidden items-center gap-2 md:flex">
                  <uip-button variant="ghost" size="sm">Sign in</uip-button>
                  <uip-button size="sm">Book a demo</uip-button>
                </div>
    
                <div class="ml-auto md:hidden">
                  ${this.mobileSheet(
                    'Menu',
                    html`
                      <uip-button slot="trigger" variant="ghost" size="icon" aria-label="Open navigation menu">
                        ${icon(Menu, 'menu', 'size-5')}
                      </uip-button>
                      <nav class="mt-6 grid gap-1" aria-label="Mobile">
                        ${stackLinks.map(
                          (link) => html`<a
                            href=${link.href}
                            class="hover:bg-muted rounded-md px-3 py-2 text-sm font-medium transition-colors"
                            @click=${() => {
                              this.sheetOpen = false
                            }}
                          >
                            ${link.label}
                          </a>`,
                        )}
                      </nav>
                      <uip-separator class="[&::part(base)]:my-5"></uip-separator>
                      <div class="grid gap-2">
                        <uip-button variant="outline">Sign in</uip-button>
                        <uip-button>Book a demo</uip-button>
                      </div>
                    `,
                  )}
                </div>
              </div>
            </header>
          </div>
        `
      }
    
      private renderCentered() {
        return html`
          <header data-slot="header-centered-logo" class="border-border bg-background border-b">
            <div class="mx-auto flex h-16 max-w-6xl items-center px-6">
              <nav class="hidden flex-1 basis-0 items-center gap-1 lg:flex" aria-label="Primary left">
                ${centeredLeft.map(
                  (link) => html`<a href=${link.href} class=${centeredLinkClass}>${link.label}</a>`,
                )}
              </nav>
    
              <div class="lg:hidden">
                ${this.mobileSheet(
                  'Menu',
                  html`
                    <uip-button slot="trigger" variant="ghost" size="icon" aria-label="Open navigation menu">
                      ${icon(Menu, 'menu', 'size-5')}
                    </uip-button>
                    <nav class="mt-6 grid gap-1" aria-label="Mobile">
                      ${[...centeredLeft, ...centeredRight].map(
                        (link) => html`<a
                          href=${link.href}
                          class="hover:bg-muted rounded-md px-3 py-2 text-sm font-medium transition-colors"
                          @click=${() => {
                            this.sheetOpen = false
                          }}
                        >
                          ${link.label}
                        </a>`,
                      )}
                    </nav>
                    <uip-separator class="[&::part(base)]:my-5"></uip-separator>
                    <uip-button class="w-full">Account</uip-button>
                  `,
                  'left',
                )}
              </div>
    
              <a
                href="#top"
                class="mx-auto flex items-center gap-2 text-base font-semibold tracking-[0.18em] uppercase lg:mx-6"
              >
                ${icon(Compass, 'compass', 'text-primary size-4')} Halden
              </a>
    
              <nav class="hidden flex-1 basis-0 items-center justify-end gap-1 lg:flex" aria-label="Primary right">
                ${centeredRight.map(
                  (link) => html`<a href=${link.href} class=${centeredLinkClass}>${link.label}</a>`,
                )}
              </nav>
    
              <div class="flex items-center gap-1 lg:ml-4">
                <uip-button variant="ghost" size="icon" aria-label="Account">
                  ${icon(User, 'user', 'size-4')}
                </uip-button>
                <uip-button variant="ghost" size="icon" aria-label="Bag">
                  ${icon(ShoppingBag, 'shopping-bag', 'size-4')}
                </uip-button>
              </div>
            </div>
          </header>
        `
      }
    
      private renderPill() {
        return html`
          <div data-slot="header-floating-pill" class="pointer-events-none sticky top-0 z-40 px-4 pt-4">
            <header
              class="border-border bg-card/85 pointer-events-auto mx-auto flex h-14 max-w-4xl items-center gap-3 rounded-full border px-3 shadow-sm backdrop-blur"
            >
              <a href="#top" class="flex shrink-0 items-center gap-2 pl-2 font-semibold tracking-tight">
                ${icon(CircleDot, 'circle-dot', 'text-primary size-4')} Verity
              </a>
    
              <nav data-pill-nav class="relative hidden items-center md:flex" aria-label="Primary">
                <span
                  class="bg-muted absolute inset-y-1 rounded-full transition-colors duration-200 ease-out"
                  style=${`left: ${this.indicator.left}px; width: ${this.indicator.width}px`}
                  aria-hidden="true"
                ></span>
                ${pillLinks.map(
                  (link, index) => html`<a
                    href=${link.href}
                    data-pill-link
                    class=${`focus-visible:ring-ring relative rounded-full px-3.5 py-1.5 text-sm font-medium transition-colors focus-visible:ring-2 focus-visible:outline-none ${
                      index === this.pillActive ? 'text-foreground' : 'text-muted-foreground hover:text-foreground'
                    }`}
                    aria-current=${index === this.pillActive ? 'page' : nothing}
                    @mouseenter=${() => this.measurePill(index)}
                    @mouseleave=${() => this.measurePill()}
                    @focus=${() => this.measurePill(index)}
                    @blur=${() => this.measurePill()}
                    @click=${() => {
                      this.pillActive = index
                    }}
                  >
                    ${link.label}
                  </a>`,
                )}
              </nav>
    
              <uip-button size="sm" class="ml-auto [&::part(base)]:rounded-full">Start free</uip-button>
    
              <div class="md:hidden">
                ${this.mobileSheet(
                  'Menu',
                  html`
                    <uip-button
                      slot="trigger"
                      variant="ghost"
                      size="icon"
                      class="[&::part(base)]:rounded-full"
                      aria-label="Open navigation menu"
                    >
                      ${icon(Menu, 'menu', 'size-5')}
                    </uip-button>
                    <nav class="mt-6 grid gap-1" aria-label="Mobile">
                      ${pillLinks.map(
                        (link) => html`<a
                          href=${link.href}
                          class="hover:bg-muted rounded-md px-3 py-2 text-sm font-medium transition-colors"
                          @click=${() => {
                            this.sheetOpen = false
                          }}
                        >
                          ${link.label}
                        </a>`,
                      )}
                    </nav>
                  `,
                  'top',
                )}
              </div>
            </header>
          </div>
        `
      }
    
      private renderMega() {
        return html`
          <header data-slot="header-mega-menu" class="border-border bg-background/90 border-b backdrop-blur">
            <div class="mx-auto flex h-16 max-w-6xl items-center gap-6 px-6">
              <a href="#top" class="flex shrink-0 items-center gap-2 font-semibold tracking-tight">
                ${icon(Layers, 'layers', 'text-primary size-5')} Northwind
              </a>
    
              <div class="hidden lg:flex">
                <uip-navigation-menu>
                  ${megaMenus.map(
                    (menu) => html`
                      <uip-navigation-menu-item>
                        <span slot="trigger">${menu.label}</span>
                        <div slot="content" class="grid w-[46rem] grid-cols-[1fr_1fr_16rem] gap-6 p-6">
                          ${menu.groups.map(
                            (group) => html`
                              <div>
                                <p class="text-muted-foreground font-mono text-xs tracking-[0.14em] uppercase">
                                  ${group.heading}
                                </p>
                                <ul class="mt-3 space-y-1">
                                  ${group.items.map(
                                    (item) => html`
                                      <li>
                                        <a
                                          href="#"
                                          class="hover:bg-muted focus-visible:ring-ring flex gap-3 rounded-md p-2 transition-colors focus-visible:ring-2 focus-visible:outline-none"
                                        >
                                          ${icon(item.icon, item.iconName, 'text-muted-foreground mt-0.5 size-4 shrink-0')}
                                          <span class="min-w-0">
                                            <span class="block text-sm font-medium">${item.title}</span>
                                            <span class="text-muted-foreground block text-xs leading-snug"
                                              >${item.body}</span
                                            >
                                          </span>
                                        </a>
                                      </li>
                                    `,
                                  )}
                                </ul>
                              </div>
                            `,
                          )}
                          <div class="bg-muted/50 flex flex-col rounded-lg p-4">
                            <p class="text-sm font-semibold">${menu.highlight.title}</p>
                            <p class="text-muted-foreground mt-2 text-xs leading-relaxed">${menu.highlight.body}</p>
                            <uip-button variant="link" class="mt-auto h-auto justify-start p-0 pt-4 text-xs">
                              ${menu.highlight.cta} ${icon(ArrowRight, 'arrow-right', 'ml-1 size-3')}
                            </uip-button>
                          </div>
                        </div>
                      </uip-navigation-menu-item>
                    `,
                  )}
                  ${megaPlainLinks.map(
                    (link) => html`
                      <uip-navigation-menu-item>
                        <uip-navigation-menu-link href=${link.href} trigger-style>${link.label}</uip-navigation-menu-link>
                      </uip-navigation-menu-item>
                    `,
                  )}
                </uip-navigation-menu>
              </div>
    
              <div class="ml-auto flex items-center gap-2">
                <span class="hidden sm:contents">
                  <uip-button variant="ghost" size="sm">Sign in</uip-button>
                </span>
                <span class="hidden sm:contents">
                  <uip-separator orientation="vertical" class="[&::part(base)]:h-5!"></uip-separator>
                </span>
                <uip-button size="sm">Book a demo</uip-button>
              </div>
    
              <div class="ml-auto lg:hidden">
                ${this.mobileSheet(
                  'Menu',
                  html`
                    <uip-button slot="trigger" variant="ghost" size="icon" aria-label="Open navigation menu">
                      ${icon(Menu, 'menu', 'size-5')}
                    </uip-button>
                    <nav class="mt-6 grid gap-5" aria-label="Mobile">
                      ${megaMenus.map(
                        (menu) => html`
                          <div>
                            <p class="text-muted-foreground/70 text-xs font-medium tracking-wide uppercase">
                              ${menu.label}
                            </p>
                            <ul class="mt-2 grid gap-1">
                              ${menu.groups.map((group) =>
                                group.items.map(
                                  (item) => html`
                                    <li>
                                      <a
                                        href="#"
                                        class="hover:bg-muted block rounded-md px-3 py-2 text-sm font-medium transition-colors"
                                        @click=${() => {
                                          this.sheetOpen = false
                                        }}
                                      >
                                        ${item.title}
                                      </a>
                                    </li>
                                  `,
                                ),
                              )}
                            </ul>
                          </div>
                        `,
                      )}
                      <uip-separator></uip-separator>
                      ${megaPlainLinks.map(
                        (link) => html`<a
                          href=${link.href}
                          class="hover:bg-muted rounded-md px-3 py-2 text-sm font-medium transition-colors"
                          @click=${() => {
                            this.sheetOpen = false
                          }}
                        >
                          ${link.label}
                        </a>`,
                      )}
                      <uip-separator></uip-separator>
                      <div class="grid gap-2">
                        <uip-button variant="outline">Sign in</uip-button>
                        <uip-button>Book a demo</uip-button>
                      </div>
                    </nav>
                  `,
                )}
              </div>
            </div>
          </header>
        `
      }
    
      private renderSearch() {
        return html`
          <header data-slot="header-search-command" class="border-border bg-background/90 border-b backdrop-blur">
            <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">
                ${icon(Compass, 'compass', 'text-primary size-5')} Halden
              </a>
    
              <nav class="hidden items-center gap-1 md:flex" aria-label="Primary">
                ${searchLinks.map(
                  (link) => html`<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>
    
              <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=${() => {
                  this.searchOpen = true
                }}
              >
                ${icon(Search, 'search', 'size-4 shrink-0')}
                <span class="truncate">Search docs…</span>
                <span class="hidden sm:contents">
                  <uip-kbd class="ml-auto">⌘K</uip-kbd>
                </span>
              </button>
    
              <span class="hidden md:contents">
                <uip-separator orientation="vertical" class="[&::part(base)]:h-5!"></uip-separator>
              </span>
              <span class="hidden sm:contents">
                <uip-button size="sm" class="shrink-0">Start free</uip-button>
              </span>
    
              <div class="shrink-0 md:hidden">
                ${this.mobileSheet(
                  'Menu',
                  html`
                    <uip-button slot="trigger" variant="ghost" size="icon" aria-label="Open navigation menu">
                      ${icon(Menu, 'menu', 'size-5')}
                    </uip-button>
                    <nav class="mt-6 grid gap-1" aria-label="Mobile">
                      ${searchLinks.map(
                        (link) => html`<a
                          href=${link.href}
                          class="hover:bg-muted rounded-md px-3 py-2 text-sm font-medium transition-colors"
                          @click=${() => {
                            this.sheetOpen = false
                          }}
                        >
                          ${link.label}
                        </a>`,
                      )}
                    </nav>
                    <uip-separator class="[&::part(base)]:my-5"></uip-separator>
                    <div class="grid gap-2">
                      <uip-button variant="outline">Sign in</uip-button>
                      <uip-button>Start free</uip-button>
                    </div>
                  `,
                )}
              </div>
            </div>
    
            <uip-command-dialog
              placeholder="Search documentation, guides, and stories…"
              .open=${this.searchOpen}
              @open-change=${(e: CustomEvent<{ open: boolean }>) => {
                this.searchOpen = e.detail.open
              }}
            >
              <uip-command-empty>No results.</uip-command-empty>
              ${searchGroups.map(
                (group) => html`
                  <uip-command-group heading=${group.heading}>
                    ${group.items.map(
                      (item) => html`
                        <uip-command-item value=${item.label}>
                          ${icon(item.icon, item.iconName, 'text-muted-foreground mr-2 size-4')} ${item.label}
                        </uip-command-item>
                      `,
                    )}
                  </uip-command-group>
                `,
              )}
            </uip-command-dialog>
          </header>
        `
      }
    
      private renderTransparent() {
        return html`
          <header
            data-slot="header-transparent-scroll"
            class=${cn(
              'fixed inset-x-0 top-0 z-40 transition-colors duration-200',
              this.scrolled ? 'border-border bg-background/85 border-b backdrop-blur' : 'border-b border-transparent',
            )}
          >
            <div class="mx-auto flex h-16 max-w-6xl items-center gap-6 px-6">
              <a href="#top" class="flex items-center gap-2 font-semibold tracking-tight">
                ${icon(Layers, 'layers', 'text-primary size-5')} Northwind
              </a>
    
              <nav class="hidden items-center gap-1 md:flex" aria-label="Primary">
                ${transparentLinks.map(
                  (link) => html`<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>
    
              <div class="ml-auto hidden items-center gap-2 md:flex">
                <uip-button variant="ghost" size="sm">Sign in</uip-button>
                <uip-button size="sm">Start free</uip-button>
              </div>
    
              <div class="ml-auto md:hidden">
                ${this.mobileSheet(
                  'Menu',
                  html`
                    <uip-button slot="trigger" variant="ghost" size="icon" aria-label="Open navigation menu">
                      ${icon(Menu, 'menu', 'size-5')}
                    </uip-button>
                    <nav class="mt-6 grid gap-1" aria-label="Mobile">
                      ${transparentLinks.map(
                        (link) => html`<a
                          href=${link.href}
                          class="hover:bg-muted rounded-md px-3 py-2 text-sm font-medium transition-colors"
                          @click=${() => {
                            this.sheetOpen = false
                          }}
                        >
                          ${link.label}
                        </a>`,
                      )}
                    </nav>
                    <uip-separator class="[&::part(base)]:my-5"></uip-separator>
                    <div class="grid gap-2">
                      <uip-button variant="outline">Sign in</uip-button>
                      <uip-button>Start free</uip-button>
                    </div>
                  `,
                )}
              </div>
            </div>
          </header>
        `
      }
    }
    
    customElements.get('uip-header') || customElements.define('uip-header', UipHeader)
    
    declare global {
      interface HTMLElementTagNameMap {
        'uip-header': UipHeader
      }
    }
    

Raw manifest:https://uipkge.dev/r/lit/header.json