Menu

Change language

GitHub repo

Announcement — Corner Card

blockmarketing

Corner announcement card that slides in after a delay carrying a thumbnail, headline, supporting line, and two actions, staying out of the page flow entirely.

Installation

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

Variants

Loading interactive previews…

Files installed (2)

  • src/app/components/blocks/announcement-corner-toast/announcement-corner-toast.component.ts4.8 kB
    // Edit this file to change the copy, the `delay` or the dismissal key.
    // Dismissal is session-scoped (sessionStorage), so the card comes back in a new tab.
    import {
      Component,
      DestroyRef,
      Input,
      afterNextRender,
      computed,
      inject,
      signal,
      ChangeDetectionStrategy,
    } from '@angular/core'
    import { ArrowRight, Bell, LucideAngularModule, X } from 'lucide-angular'
    import { cn } from '@/lib/utils'
    import { UiBadgeComponent } from '@/ui/badge/badge.component'
    import { UiButtonComponent } from '@/ui/button/button.component'
    import { UiCardComponent, UiCardContentComponent } from '@/ui/card/card.component'
    import { UiSeparatorComponent } from '@/ui/separator/separator.component'
    
    /**
     * AnnouncementCornerToast -- the React block, 1:1: a fixed bottom-right card that slides
     * in `delay` ms after the first render with a badge, headline, supporting line and two
     * actions. "Not now" and the close button dismiss it for the rest of the session.
     */
    @Component({
      changeDetection: ChangeDetectionStrategy.Eager,
      selector: 'ui-announcement-corner-toast, [ui-announcement-corner-toast]',
      standalone: true,
      // React renders the fixed <div> itself: keep this wrapper out of the layout.
      host: { class: 'contents' },
      imports: [
        LucideAngularModule,
        UiBadgeComponent,
        UiButtonComponent,
        UiCardComponent,
        UiCardContentComponent,
        UiSeparatorComponent,
      ],
      template: `
        <div
          data-slot="announcement-corner-toast"
          [class]="rootClass()"
          role="complementary"
          aria-label="Product announcement"
          [attr.aria-hidden]="!visible()"
        >
          <ui-card class="shadow-lg">
            <ui-card-content class="p-4">
              <div class="flex items-start gap-3">
                <span
                  class="border-border bg-muted flex size-10 shrink-0 items-center justify-center rounded-lg border"
                  aria-hidden="true"
                >
                  <lucide-icon [img]="Bell" class="text-primary size-4" />
                </span>
                <div class="min-w-0 flex-1">
                  <span ui-badge variant="secondary" class="mb-1.5">New</span>
                  <p class="text-sm font-semibold">Scoping now follows SCIM groups</p>
                  <p class="text-muted-foreground mt-1 text-xs leading-relaxed">
                    Move someone between teams in your IdP and their dashboards follow within the sync window.
                  </p>
                </div>
                <button
                  ui-button
                  variant="ghost"
                  size="icon"
                  class="-mt-1 -mr-1 size-7 shrink-0"
                  aria-label="Dismiss announcement"
                  (click)="dismiss()"
                >
                  <lucide-icon [img]="X" class="size-3.5" aria-hidden="true" />
                </button>
              </div>
    
              <ui-separator class="my-3" />
    
              <div class="flex items-center gap-2">
                <!-- px-2.5: React's size="sm" gets it from has-[>svg], but here the svg sits inside <lucide-icon>. -->
                <button ui-button size="sm" class="h-7 px-2.5">
                  Read the changelog
                  <!-- [class] (not class=) feeds only the svg, so ml-1 is not applied twice (host + svg). -->
                  <lucide-icon [img]="ArrowRight" [class]="'ml-1 size-3.5'" aria-hidden="true" />
                </button>
                <button ui-button variant="ghost" size="sm" class="h-7" (click)="dismiss()">Not now</button>
              </div>
            </ui-card-content>
          </ui-card>
        </div>
      `,
    })
    export class UiAnnouncementCornerToastComponent {
      /** Milliseconds before the card slides in. */
      @Input() delay = 1200
      /** sessionStorage key holding the dismissal. */
      @Input() storageKey = 'uipkge:corner-announcement-dismissed'
    
      // Hidden on the server and on first paint; the delay timer is what reveals it.
      readonly visible = signal(false)
    
      protected readonly rootClass = computed(() =>
        cn(
          'fixed right-4 bottom-4 z-50 w-[22rem] max-w-[calc(100vw-2rem)] transition duration-300',
          this.visible() ? 'translate-y-0 opacity-100' : 'pointer-events-none invisible translate-y-4 opacity-0',
        ),
      )
    
      protected readonly Bell = Bell
      protected readonly X = X
      protected readonly ArrowRight = ArrowRight
    
      private timer: ReturnType<typeof setTimeout> | undefined
    
      constructor() {
        // Browser only, after the first render (React's useEffect).
        afterNextRender(() => {
          if (this.wasDismissed()) return
          this.timer = setTimeout(() => this.visible.set(true), this.delay)
        })
        inject(DestroyRef).onDestroy(() => clearTimeout(this.timer))
      }
    
      dismiss(): void {
        this.visible.set(false)
        try {
          window.sessionStorage.setItem(this.storageKey, '1')
        } catch {
          // Blocked storage: the dismissal still holds for this page view.
        }
      }
    
      private wasDismissed(): boolean {
        try {
          return window.sessionStorage.getItem(this.storageKey) === '1'
        } catch {
          return false
        }
      }
    }
    
  • src/app/components/blocks/announcement-corner-toast/index.ts0.1 kB

Raw manifest:https://uipkge.dev/r/angular/announcement-corner-toast.json