Announcement — Corner Card
blockmarketingCorner 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
$pnpm dlx uipkge-ng@latest add announcement-corner-toast$npx uipkge-ng@latest add announcement-corner-toast$yarn dlx uipkge-ng@latest add announcement-corner-toast$bunx uipkge-ng@latest add announcement-corner-toastManifest:
https://uipkge.dev/r/angular/announcement-corner-toast.jsonInstalls to:src/app/components/blocks/announcement-corner-toast/Variants
Loading interactive previews…
Loading interactive previews…
npm dependencies
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
export { UiAnnouncementCornerToastComponent } from './announcement-corner-toast.component'
Raw manifest:https://uipkge.dev/r/angular/announcement-corner-toast.json