Announcement Banner
blockmarketingInteractive release, security, and maintenance broadcast center with rotating live ticker, CLI copy button, and floating pill / top-bar modes.
Installation
$pnpm dlx uipkge-ng@latest add announcement-banner$npx uipkge-ng@latest add announcement-banner$yarn dlx uipkge-ng@latest add announcement-banner$bunx uipkge-ng@latest add announcement-bannerManifest:
https://uipkge.dev/r/angular/announcement-banner.jsonInstalls to:src/app/components/blocks/announcement-banner/Variants
Loading interactive previews…
Loading interactive previews…
Schema
Type aliases exported from this item's source. Use these to shape the data you pass in.
AnnouncementIteminterface AnnouncementItem {
id: string
category: AnnouncementCategory
badgeText: string
title: string
actionLabel: string
actionUrl?: string
cliSnippet?: string
}npm dependencies
Includes
Files installed (2)
src/app/components/blocks/announcement-banner/announcement-banner.component.ts11.7 kB
// Deprecated alias of the announcement broadcast center (use ui-announcement). rotating live ticker with CLI copy and prev/next // controls, plus floating-pill and top-bar modes. Port of the Vue/React block 1:1 — // same three announcements, same 6s autoplay that pauses on hover, same category // badge tones. `variant` picks the placement; `autoplay` starts the rotation. import { ChangeDetectionStrategy, Component, DestroyRef, Input, OnDestroy, OnInit, computed, inject, signal, } from '@angular/core' import { ArrowRight, ChevronLeft, ChevronRight, LucideAngularModule, Megaphone, Terminal, X } from 'lucide-angular' import { cn } from '@/lib/utils' import { UiBadgeComponent } from '@/ui/badge/badge.component' export type AnnouncementVariant = 'floating-pill' | 'top-bar' | 'interactive-ticker' export type AnnouncementCategory = 'release' | 'security' | 'maintenance' | 'feature' export interface AnnouncementItem { id: string category: AnnouncementCategory badgeText: string title: string actionLabel: string actionUrl?: string cliSnippet?: string } const announcements: AnnouncementItem[] = [ { id: 'ann-1', category: 'release', badgeText: 'v2.4.0 Release', title: 'Tailwind CSS v4 OKLCH tokens & 281+ production workbenches are live.', actionLabel: 'Read Changelog', actionUrl: 'https://uipkge.dev/changelog', cliSnippet: 'npx shadcn-vue@latest add https://uipkge.dev/r/vue/init.json', }, { id: 'ann-2', category: 'feature', badgeText: 'Dual-Framework', title: '100% Vue 3.5 & React 19 component parity achieved with zero runtime CSS.', actionLabel: 'Explore Parity Matrix', actionUrl: 'https://uipkge.dev/components', cliSnippet: 'npx shadcn@latest add https://uipkge.dev/r/react/init.json', }, { id: 'ann-3', category: 'security', badgeText: 'Security Notice', title: 'Upstream Reka UI & Radix dependency security patches verified and updated.', actionLabel: 'Security Advisory', actionUrl: 'https://uipkge.dev/security', }, ] const categoryBadgeClass: Record<AnnouncementCategory, string> = { release: 'bg-primary/10 text-primary border-primary/20', security: 'border-warning/20 bg-warning/10 text-warning', maintenance: '', feature: 'border-success/20 bg-success/10 text-success', } @Component({ changeDetection: ChangeDetectionStrategy.Eager, selector: 'ui-announcement-banner, [ui-announcement-banner]', standalone: true, // The block renders its own root <div>: keep this wrapper out of the layout. host: { '[attr.class]': '"contents"' }, imports: [LucideAngularModule, UiBadgeComponent], template: ` @if (!dismissed()) { <div data-slot="announcement-banner" [attr.data-variant]="variant" role="region" aria-label="Announcement" [class]="rootClass" (mouseenter)="isPaused.set(true)" (mouseleave)="isPaused.set(false)" > @if (variant === 'interactive-ticker') { <div class="bg-muted/70 border-border text-foreground flex items-center justify-between gap-3 overflow-hidden border-b px-4 py-2 text-xs shadow-xs backdrop-blur-md" > <div class="mx-auto flex w-full max-w-6xl items-center justify-between gap-4"> <!-- Left: Status Beacon & Item Indicator --> <div class="flex shrink-0 items-center gap-3"> <div class="text-muted-foreground flex items-center gap-1.5 font-mono text-xs"> <span class="bg-primary size-2 animate-pulse rounded-full"></span> <span class="text-foreground font-semibold">Live Dispatch</span> </div> <span ui-badge variant="secondary" [class]="badgeClass()">{{ current().badgeText }}</span> </div> <!-- Center: Dynamic Headline & Action Link --> <div class="flex flex-1 items-center justify-center gap-2 truncate"> <p class="text-foreground truncate font-medium">{{ current().title }}</p> @if (current().actionUrl; as actionUrl) { <a [href]="actionUrl" class="text-primary hidden shrink-0 items-center gap-1 font-semibold hover:underline sm:inline-flex" > {{ current().actionLabel }} <lucide-icon [img]="ArrowRight" class="size-3" /> </a> } </div> <!-- Right: Ticker Navigation & CLI / Dismiss --> <div class="flex shrink-0 items-center gap-2"> @if (current().cliSnippet; as snippet) { <button type="button" class="bg-background border-border text-muted-foreground hover:text-foreground hidden items-center gap-1.5 rounded border px-2 py-0.5 font-mono text-xs transition-colors md:inline-flex" (click)="copyCliSnippet(snippet)" > <lucide-icon [img]="Terminal" class="text-primary size-3" /> <span>{{ copiedCli() ? 'Copied CLI!' : 'Copy CLI' }}</span> </button> } <div class="border-border bg-background flex items-center overflow-hidden rounded-md border"> <button type="button" class="hover:bg-muted text-muted-foreground hover:text-foreground p-1 transition-colors" aria-label="Previous announcement" (click)="prevAnnouncement()" > <lucide-icon [img]="ChevronLeft" class="size-3.5" /> </button> <span class="text-muted-foreground px-1.5 font-mono text-xs"> {{ currentIndex() + 1 }}/{{ announcementCount }} </span> <button type="button" class="hover:bg-muted text-muted-foreground hover:text-foreground p-1 transition-colors" aria-label="Next announcement" (click)="nextAnnouncement()" > <lucide-icon [img]="ChevronRight" class="size-3.5" /> </button> </div> <button type="button" aria-label="Dismiss banner" class="text-muted-foreground hover:text-foreground hover:bg-muted rounded-md p-1 transition-colors" (click)="dismiss()" > <lucide-icon [img]="X" class="size-3.5" /> </button> </div> </div> </div> } @else if (variant === 'floating-pill') { <div class="mx-auto w-fit max-w-2xl px-4 py-2"> <div class="bg-card/90 border-border text-foreground hover:border-primary/40 flex items-center gap-3 rounded-full border py-1.5 pr-2 pl-3 text-xs shadow-md backdrop-blur-md transition-colors" > <span ui-badge variant="secondary" [class]="pillBadgeClass">{{ current().badgeText }}</span> <p class="text-foreground max-w-sm truncate font-medium">{{ current().title }}</p> @if (current().actionUrl; as actionUrl) { <a [href]="actionUrl" class="text-primary inline-flex shrink-0 items-center gap-1 font-semibold hover:underline" > {{ current().actionLabel }} <lucide-icon [img]="ArrowRight" class="size-3" /> </a> } <button type="button" aria-label="Dismiss announcement" class="text-muted-foreground hover:text-foreground hover:bg-muted rounded-full p-1 transition-colors" (click)="dismiss()" > <lucide-icon [img]="X" class="size-3.5" /> </button> </div> </div> } @else { <div class="bg-primary text-primary-foreground flex items-center justify-between gap-3 px-4 py-2 text-xs shadow-xs" > <div class="mx-auto flex w-full max-w-6xl items-center justify-between gap-4"> <div class="flex items-center gap-2 truncate"> <lucide-icon [img]="Megaphone" class="size-4 shrink-0" /> <span class="font-mono font-bold">[{{ current().badgeText }}]</span> <span class="truncate">{{ current().title }}</span> </div> <div class="flex shrink-0 items-center gap-3"> @if (current().actionUrl; as actionUrl) { <a [href]="actionUrl" class="inline-flex items-center gap-1 font-semibold underline-offset-4 hover:underline" > {{ current().actionLabel }} <lucide-icon [img]="ArrowRight" class="size-3" /> </a> } <button type="button" aria-label="Dismiss banner" class="text-primary-foreground/80 hover:text-primary-foreground hover:bg-primary-foreground/15 rounded p-1 transition-colors" (click)="dismiss()" > <lucide-icon [img]="X" class="size-3.5" /> </button> </div> </div> </div> } </div> } `, }) export class UiAnnouncementBannerComponent implements OnInit, OnDestroy { @Input() variant: AnnouncementVariant = 'interactive-ticker' @Input() autoplay = true @Input('class') className?: string readonly currentIndex = signal(0) readonly dismissed = signal(false) readonly isPaused = signal(false) readonly copiedCli = signal(false) protected readonly announcementCount = announcements.length protected readonly pillBadgeClass = 'bg-primary/10 text-primary border-primary/20 px-2 py-0.5 font-mono text-xs' protected readonly ArrowRight = ArrowRight protected readonly ChevronLeft = ChevronLeft protected readonly ChevronRight = ChevronRight protected readonly Megaphone = Megaphone protected readonly Terminal = Terminal protected readonly X = X private timer: ReturnType<typeof setInterval> | undefined private copyTimer: ReturnType<typeof setTimeout> | undefined constructor() { inject(DestroyRef).onDestroy(() => { clearInterval(this.timer) clearTimeout(this.copyTimer) }) } ngOnInit(): void { this.startAutoplay() } ngOnDestroy(): void { this.stopAutoplay() } readonly current = computed(() => announcements[this.currentIndex()]!) readonly badgeClass = computed(() => cn('px-2 py-0.5 font-mono text-xs', categoryBadgeClass[this.current().category])) get rootClass(): string { return cn('relative z-50 transition-colors duration-200', this.className) } nextAnnouncement(): void { this.currentIndex.update((i) => (i + 1) % announcements.length) } prevAnnouncement(): void { this.currentIndex.update((i) => (i - 1 + announcements.length) % announcements.length) } copyCliSnippet(snippet?: string): void { if (!snippet) return try { void navigator.clipboard.writeText(snippet).catch(() => {}) } catch { // Blocked clipboard: the label flip below still confirms the intent. } this.copiedCli.set(true) clearTimeout(this.copyTimer) this.copyTimer = setTimeout(() => this.copiedCli.set(false), 2000) } dismiss(): void { this.dismissed.set(true) } private startAutoplay(): void { if (!this.autoplay) return this.stopAutoplay() this.timer = setInterval(() => { if (!this.isPaused()) this.nextAnnouncement() }, 6000) } private stopAutoplay(): void { clearInterval(this.timer) this.timer = undefined } }src/app/components/blocks/announcement-banner/index.ts0.2 kB
export { UiAnnouncementBannerComponent, type AnnouncementCategory, type AnnouncementItem, type AnnouncementVariant, } from './announcement-banner.component'
Raw manifest:https://uipkge.dev/r/angular/announcement-banner.json