Menu

Change language

GitHub repo

Header — Announcement Stack

blockmarketing

Two-tier site header: a dismissible announcement strip above a sticky navbar, where dismissing the strip collapses it and leaves the navbar flush against the viewport top.

Installation

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

Variants

Loading interactive previews…

Schema

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

AnnouncementStackLink
interface AnnouncementStackLink {
  label: string
  href: string
}

Files installed (2)

  • src/app/components/blocks/header-announcement-stack/header-announcement-stack.component.ts6.1 kB
    // Two-tier site header: a dismissible announcement strip above a sticky navbar,
    // where dismissing the strip collapses it and leaves the navbar flush against the
    // viewport top. Port of the Vue/React block 1:1 — same links, same copy, same
    // session-scoped dismissal (rendered visible first so there is no flash of a strip
    // that then vanishes).
    import { ChangeDetectionStrategy, Component, Input, OnInit, signal } from '@angular/core'
    import { ArrowRight, Layers, LucideAngularModule, Menu, X } from 'lucide-angular'
    import { cn } from '@/lib/utils'
    import { UiBadgeComponent } from '@/ui/badge/badge.component'
    import { UiButtonComponent } from '@/ui/button/button.component'
    import { UiSeparatorComponent } from '@/ui/separator/separator.component'
    import {
      UiSheetCloseComponent,
      UiSheetComponent,
      UiSheetContentComponent,
      UiSheetHeaderComponent,
      UiSheetTitleComponent,
    } from '@/ui/sheet/sheet.component'
    
    export interface AnnouncementStackLink {
      label: string
      href: string
    }
    
    const STORAGE_KEY = 'uipkge:announcement-dismissed'
    
    const links: AnnouncementStackLink[] = [
      { label: 'Platform', href: '#platform' },
      { label: 'Pricing', href: '#pricing' },
      { label: 'Customers', href: '#customers' },
      { label: 'Docs', href: '#docs' },
    ]
    
    @Component({
      changeDetection: ChangeDetectionStrategy.Eager,
      selector: 'ui-header-announcement-stack, [ui-header-announcement-stack]',
      standalone: true,
      // The block renders its own root <div>: keep this wrapper out of the layout.
      host: { '[attr.class]': '"contents"' },
      imports: [
        LucideAngularModule,
        UiBadgeComponent,
        UiButtonComponent,
        UiSeparatorComponent,
        UiSheetCloseComponent,
        UiSheetComponent,
        UiSheetContentComponent,
        UiSheetHeaderComponent,
        UiSheetTitleComponent,
      ],
      template: `
        <div data-slot="header-announcement-stack" [class]="rootClass">
          @if (!dismissed()) {
            <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">
                <span ui-badge variant="secondary" class="shrink-0">New</span>
                <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>
                <lucide-icon [img]="ArrowRight" class="text-muted-foreground hidden size-3.5 shrink-0 sm:block" />
                <button
                  ui-button
                  variant="ghost"
                  size="icon"
                  class="ml-auto size-7 shrink-0"
                  aria-label="Dismiss announcement"
                  (click)="dismiss()"
                >
                  <lucide-icon [img]="X" class="size-3.5" />
                </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">
                <lucide-icon [img]="Layers" class="text-primary size-5" />
                Northwind
              </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>
    
              <div class="ml-auto hidden items-center gap-2 md:flex">
                <button ui-button variant="ghost" size="sm">Sign in</button>
                <button ui-button size="sm">Book a demo</button>
              </div>
    
              <div ui-sheet [open]="mobileOpen()" (openChange)="mobileOpen.set($event)">
                <button
                  ui-button
                  variant="ghost"
                  size="icon"
                  class="ml-auto md:hidden"
                  aria-label="Open navigation menu"
                  (click)="mobileOpen.set(true)"
                >
                  <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)="mobileOpen.set(false)"
                      >
                        {{ link.label }}
                      </a>
                    }
                  </nav>
                  <div ui-separator class="my-5"></div>
                  <div class="grid gap-2">
                    <button ui-button variant="outline">Sign in</button>
                    <button ui-button>Book a demo</button>
                  </div>
                </div>
              </div>
            </div>
          </header>
        </div>
      `,
    })
    export class UiHeaderAnnouncementStackComponent implements OnInit {
      protected readonly ArrowRight = ArrowRight
      protected readonly Layers = Layers
      protected readonly Menu = Menu
      protected readonly X = X
    
      protected readonly navLinks = links
    
      @Input('class') className?: string
    
      readonly dismissed = signal(false)
      readonly mobileOpen = signal(false)
    
      ngOnInit(): void {
        try {
          if (typeof window !== 'undefined' && typeof window.sessionStorage !== 'undefined') {
            this.dismissed.set(window.sessionStorage.getItem(STORAGE_KEY) === '1')
          }
        } catch {
          this.dismissed.set(false)
        }
      }
    
      dismiss(): void {
        this.dismissed.set(true)
        try {
          window.sessionStorage.setItem(STORAGE_KEY, '1')
        } catch {
          // Blocked storage: the dismissal still holds for this page view.
        }
      }
    
      get rootClass(): string {
        return cn('sticky top-0 z-40', this.className)
      }
    }
    
  • src/app/components/blocks/header-announcement-stack/index.ts0.1 kB

Raw manifest:https://uipkge.dev/r/angular/header-announcement-stack.json