Menu

Change language

GitHub repo

Announcement Banner

blockmarketing

Interactive release, security, and maintenance broadcast center with rotating live ticker, CLI copy button, and floating pill / top-bar modes.

Installation

$npx shadcn-svelte@latest add https://uipkge.dev/r/svelte/announcement-banner.json
Manifest:https://uipkge.dev/r/svelte/announcement-banner.jsonInstalls to:src/lib/components/blocks/announcement-banner/

Variants

Loading interactive previews…

npm dependencies

Includes

Files installed (1)

  • src/lib/components/blocks/announcement-banner/AnnouncementBanner.svelte9.3 kB
    <script lang="ts" module>
      export interface AnnouncementItem {
        id: string
        category: 'release' | 'security' | 'maintenance' | 'feature'
        badgeText: string
        title: string
        actionLabel: string
        actionUrl?: string
        cliSnippet?: string
      }
    
      export interface AnnouncementBannerProps {
        variant?: 'floating-pill' | 'top-bar' | 'interactive-ticker'
        autoplay?: boolean
        class?: string
      }
    </script>
    
    <script lang="ts">
      import { onDestroy, onMount } from 'svelte'
      import { ArrowRight, ChevronLeft, ChevronRight, Megaphone, Terminal, X } from '@lucide/svelte'
      import { Badge } from '$lib/components/ui/badge'
      import { cn } from '$lib/utils'
    
      let { variant = 'interactive-ticker', autoplay = true, class: className }: AnnouncementBannerProps = $props()
    
      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-svelte@latest add https://uipkge.dev/r/svelte/init.json',
        },
        {
          id: 'ann-2',
          category: 'feature',
          badgeText: 'Multi-Framework',
          title: 'Svelte 5 runes parity with the Vue 3.5 & React 19 registries, zero runtime CSS.',
          actionLabel: 'Explore Parity Matrix',
          actionUrl: 'https://uipkge.dev/components',
          cliSnippet: 'npx shadcn-svelte@latest add https://uipkge.dev/r/svelte/button.json',
        },
        {
          id: 'ann-3',
          category: 'security',
          badgeText: 'Security Notice',
          title: 'Upstream dependency security patches verified and updated.',
          actionLabel: 'Security Advisory',
          actionUrl: 'https://uipkge.dev/security',
        },
      ]
    
      let currentIndex = $state(0)
      let dismissed = $state(false)
      let isPaused = $state(false)
      let copiedCli = $state(false)
    
      let timer: ReturnType<typeof setInterval> | undefined
    
      function nextAnnouncement() {
        currentIndex = (currentIndex + 1) % announcements.length
      }
    
      function prevAnnouncement() {
        currentIndex = (currentIndex - 1 + announcements.length) % announcements.length
      }
    
      function startAutoplay() {
        if (!autoplay) return
        stopAutoplay()
        timer = setInterval(() => {
          if (!isPaused) nextAnnouncement()
        }, 6000)
      }
    
      function stopAutoplay() {
        if (timer) clearInterval(timer)
      }
    
      onMount(startAutoplay)
      onDestroy(stopAutoplay)
    
      function copyCliSnippet(snippet?: string) {
        if (!snippet) return
        navigator.clipboard.writeText(snippet)
        copiedCli = true
        setTimeout(() => (copiedCli = false), 2000)
      }
    
      const current = $derived(announcements[currentIndex])
    </script>
    
    {#if !dismissed}
      <div
        data-slot="announcement-banner"
        data-variant={variant}
        role="region"
        aria-label="Announcement"
        class={cn('relative z-50 transition-colors duration-200', className)}
        onmouseenter={() => (isPaused = true)}
        onmouseleave={() => (isPaused = false)}
      >
        <!-- 1. Interactive Ticker Mode (Default) -->
        {#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>
    
                <Badge
                  variant="secondary"
                  class={cn(
                    'px-2 py-0.5 font-mono text-xs',
                    current.category === 'release' && 'bg-primary/10 text-primary border-primary/20',
                    current.category === 'security' && 'border-warning/20 bg-warning/10 text-warning',
                    current.category === 'feature' && 'border-success/20 bg-success/10 text-success',
                  )}
                >
                  {current.badgeText}
                </Badge>
              </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}
                  <a
                    href={current.actionUrl}
                    class="text-primary hidden shrink-0 items-center gap-1 font-semibold hover:underline sm:inline-flex"
                  >
                    {current.actionLabel} <ArrowRight class="size-3" />
                  </a>
                {/if}
              </div>
    
              <!-- Right: Ticker Navigation & CLI / Dismiss -->
              <div class="flex shrink-0 items-center gap-2">
                {#if current.cliSnippet}
                  <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"
                    onclick={() => copyCliSnippet(current.cliSnippet)}
                  >
                    <Terminal class="text-primary size-3" />
                    <span>{copiedCli ? 'Copied CLI!' : 'Copy CLI'}</span>
                  </button>
                {/if}
    
                <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"
                    onclick={prevAnnouncement}
                  >
                    <ChevronLeft class="size-3.5" />
                  </button>
                  <span class="text-muted-foreground px-1.5 font-mono text-xs">
                    {currentIndex + 1}/{announcements.length}
                  </span>
                  <button
                    type="button"
                    class="hover:bg-muted text-muted-foreground hover:text-foreground p-1 transition-colors"
                    aria-label="Next announcement"
                    onclick={nextAnnouncement}
                  >
                    <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"
                  onclick={() => (dismissed = true)}
                >
                  <X class="size-3.5" />
                </button>
              </div>
            </div>
          </div>
    
          <!-- 2. Floating Pill Mode -->
        {: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"
            >
              <Badge variant="secondary" class="bg-primary/10 text-primary border-primary/20 px-2 py-0.5 font-mono text-xs">
                {current.badgeText}
              </Badge>
              <p class="text-foreground max-w-sm truncate font-medium">
                {current.title}
              </p>
              {#if current.actionUrl}
                <a
                  href={current.actionUrl}
                  class="text-primary inline-flex shrink-0 items-center gap-1 font-semibold hover:underline"
                >
                  {current.actionLabel} <ArrowRight class="size-3" />
                </a>
              {/if}
              <button
                type="button"
                aria-label="Dismiss announcement"
                class="text-muted-foreground hover:text-foreground hover:bg-muted rounded-full p-1 transition-colors"
                onclick={() => (dismissed = true)}
              >
                <X class="size-3.5" />
              </button>
            </div>
          </div>
    
          <!-- 3. Top Solid Bar Mode -->
        {: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">
                <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}
                  <a
                    href={current.actionUrl}
                    class="inline-flex items-center gap-1 font-semibold underline-offset-4 hover:underline"
                  >
                    {current.actionLabel} <ArrowRight class="size-3" />
                  </a>
                {/if}
                <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"
                  onclick={() => (dismissed = true)}
                >
                  <X class="size-3.5" />
                </button>
              </div>
            </div>
          </div>
        {/if}
      </div>
    {/if}
    

Raw manifest:https://uipkge.dev/r/svelte/announcement-banner.json