Notification Center
blockcommunicationFull-page notification center: unread badge, mark-all-read, All/Unread/Mentions tabs with functional filtering, day-grouped rows (Today / Earlier) pairing category icon tiles with titles, bodies, relative timestamps, unread dots and hover actions (mark read, dismiss). Per-tab empty states included; swap `notifications` for your source.
Installation
$pnpm dlx uipkge-ng@latest add notification-center$npx uipkge-ng@latest add notification-center$yarn dlx uipkge-ng@latest add notification-center$bunx uipkge-ng@latest add notification-centerManifest:
https://uipkge.dev/r/angular/notification-center.jsonInstalls to:src/app/components/blocks/notification-center/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.
NotificationCenterIteminterface NotificationCenterItem {
id: string
category: NotificationCenterCategory
title: string
body: string
date: Date
read: boolean
}npm dependencies
Files installed (2)
src/app/components/blocks/notification-center/notification-center.component.ts10 kB
import { Component, Input, computed, signal, ChangeDetectionStrategy } from '@angular/core' import { BellOff, Check, CheckCheck, LucideAngularModule, MessageSquare, ShieldAlert, UserPlus, X, type LucideIconData, } from 'lucide-angular' import { cn } from '@/lib/utils' import { UiBadgeComponent } from '@/ui/badge/badge.component' import { UiButtonComponent } from '@/ui/button/button.component' import { UiRelativeTimeComponent } from '@/ui/relative-time/relative-time.component' import { UiSectionCardComponent } from '@/ui/section-card/section-card.component' import { UiTabsComponent, UiTabsListComponent, UiTabsTriggerComponent } from '@/ui/tabs/tabs.component' export type NotificationCenterCategory = 'mention' | 'comment' | 'system' | 'security' export interface NotificationCenterItem { id: string category: NotificationCenterCategory title: string body: string date: Date read: boolean } export type NotificationCenterFilter = 'all' | 'unread' | 'mentions' const categoryConfig: Record<NotificationCenterCategory, { icon: LucideIconData; tileClass: string }> = { mention: { icon: UserPlus, tileClass: 'bg-primary/10 text-primary' }, comment: { icon: MessageSquare, tileClass: 'bg-chart-3/10 text-chart-3' }, system: { icon: BellOff, tileClass: 'bg-muted text-muted-foreground' }, security: { icon: ShieldAlert, tileClass: 'bg-warning/10 text-warning' }, } const now = new Date() const minutesAgo = (m: number) => new Date(now.getTime() - m * 60_000) const hoursAgo = (h: number) => new Date(now.getTime() - h * 3_600_000) const daysAgo = (d: number) => new Date(now.getTime() - d * 86_400_000) const stubNotifications: NotificationCenterItem[] = [ { id: 'n1', category: 'mention', title: 'Priya mentioned you', body: '@amara can you review the onboarding flow?', date: minutesAgo(6), read: false, }, { id: 'n2', category: 'security', title: 'New sign-in from Berlin', body: 'Chrome on macOS · 84.190.201.3', date: minutesAgo(52), read: false, }, { id: 'n3', category: 'comment', title: 'Jonas commented', body: 'Shipped the fix — closing the ticket.', date: hoursAgo(2), read: false, }, { id: 'n4', category: 'system', title: 'Weekly digest ready', body: 'Your workspace summary for last week.', date: hoursAgo(20), read: true, }, { id: 'n5', category: 'mention', title: 'Marcus mentioned you', body: 'Design review moved to 15:00, @amara.', date: daysAgo(1), read: false, }, { id: 'n6', category: 'comment', title: 'Amara commented', body: 'Copy updated per legal review.', date: daysAgo(1), read: true, }, { id: 'n7', category: 'security', title: 'API key rotated', body: 'uipkge_live_…4f2a was rotated automatically.', date: daysAgo(2), read: true, }, ] /** * NotificationCenter -- the React block, 1:1: a full-page notification SectionCard with an * unread badge, mark-all-read, All / Unread / Mentions tabs that filter the list, day-grouped * rows (Today / Earlier) pairing category icon tiles with titles, bodies, relative timestamps, * unread dots and hover actions (mark read, dismiss), plus per-tab empty states. Pass * `[notifications]` to swap the sample data for your source. * * Icons inside `ui-button` sit in `<lucide-icon>`, so the React `size="sm"` `has-[>svg]:px-2.5` * padding is set explicitly on the mark-all-read button. */ @Component({ changeDetection: ChangeDetectionStrategy.Eager, selector: 'ui-notification-center, [ui-notification-center]', standalone: true, // React renders the SectionCard itself: the host stays out of layout and `class` goes to the card. host: { '[attr.class]': '"contents"', 'data-slot': 'notification-center' }, imports: [ LucideAngularModule, UiBadgeComponent, UiButtonComponent, UiRelativeTimeComponent, UiSectionCardComponent, UiTabsComponent, UiTabsListComponent, UiTabsTriggerComponent, ], template: ` <div ui-section-card [title]="'Notifications'" [description]="'Everything that needs your attention.'" [class]="className" > <div slot="header-action" class="flex flex-wrap items-center gap-2"> @if (unreadCount() > 0) { <span ui-badge variant="default">{{ unreadCount() }} new</span> } <button ui-button variant="ghost" size="sm" class="px-2.5" [disabled]="unreadCount() === 0" (click)="markAllRead()" > <lucide-icon [img]="CheckCheck" aria-hidden="true" /> Mark all read </button> </div> <div ui-tabs [value]="filter()" (valueChange)="setFilter($event)" class="mb-2"> <div ui-tabs-list> <button ui-tabs-trigger value="all">All</button> <button ui-tabs-trigger value="unread">Unread</button> <button ui-tabs-trigger value="mentions">Mentions</button> </div> </div> @if (groups().length === 0) { <div class="text-muted-foreground flex flex-col items-center gap-2 py-12 text-sm"> <!-- [class] feeds only the svg: a static class lands on the host AND the svg (opacity twice). --> <lucide-icon [img]="BellOff" class="size-6" [class]="'opacity-50'" aria-hidden="true" /> @if (filter() === 'mentions') { No mentions — you're all caught up. } @else if (filter() === 'unread') { Nothing unread. Nice work. } @else { Your inbox is empty. } </div> } @for (group of groups(); track group.label) { <div class="mb-2"> <p class="text-muted-foreground py-1.5 text-xs font-medium tracking-widest uppercase">{{ group.label }}</p> <ul class="divide-y"> @for (notification of group.items; track notification.id) { <li class="group hover:bg-accent/40 -mx-2 flex cursor-pointer items-start gap-3 rounded-md px-2 py-3 transition-colors" (click)="markRead(notification.id)" > <span [class]=" cn('mt-0.5 flex size-8 shrink-0 items-center justify-center rounded-md', tileClass(notification)) " aria-hidden="true" > <lucide-icon [img]="icon(notification)" class="size-4" /> </span> <div class="min-w-0 flex-1"> <div class="flex flex-wrap items-center gap-2"> <p [class]="cn('truncate text-sm', notification.read ? 'font-normal' : 'font-medium')"> {{ notification.title }} </p> @if (!notification.read) { <span class="bg-primary size-1.5 shrink-0 rounded-full" aria-label="Unread"></span> } </div> <p class="text-muted-foreground truncate text-xs">{{ notification.body }}</p> </div> <div class="flex shrink-0 items-center gap-1"> <time ui-relative-time [date]="notification.date" class="text-muted-foreground text-xs"></time> <span class="flex opacity-0 transition-opacity group-hover:opacity-100"> @if (!notification.read) { <button ui-button variant="ghost" size="icon-sm" aria-label="Mark as read" (click)="$event.stopPropagation(); markRead(notification.id)" > <lucide-icon [img]="Check" aria-hidden="true" /> </button> } <button ui-button variant="ghost" size="icon-sm" aria-label="Dismiss" (click)="$event.stopPropagation()" > <lucide-icon [img]="X" aria-hidden="true" /> </button> </span> </div> </li> } </ul> </div> } </div> `, }) export class UiNotificationCenterComponent { protected readonly BellOff = BellOff protected readonly Check = Check protected readonly CheckCheck = CheckCheck protected readonly X = X protected readonly cn = cn /** Your notifications; falls back to the sample list (React `notifications ?? stubNotifications`). */ @Input() set notifications(value: NotificationCenterItem[] | undefined) { this.items.set(value ?? stubNotifications) } @Input('class') className?: string readonly items = signal<NotificationCenterItem[]>(stubNotifications) readonly filter = signal<NotificationCenterFilter>('all') readonly unreadCount = computed(() => this.items().filter((n) => !n.read).length) readonly filtered = computed(() => this.items().filter((n) => { if (this.filter() === 'unread') return !n.read if (this.filter() === 'mentions') return n.category === 'mention' return true }), ) readonly groups = computed(() => { const startOfToday = new Date() startOfToday.setHours(0, 0, 0, 0) return [ { label: 'Today', items: this.filtered().filter((n) => n.date >= startOfToday) }, { label: 'Earlier', items: this.filtered().filter((n) => n.date < startOfToday) }, ].filter((group) => group.items.length > 0) }) protected icon(n: NotificationCenterItem): LucideIconData { return categoryConfig[n.category].icon } protected tileClass(n: NotificationCenterItem): string { return categoryConfig[n.category].tileClass } setFilter(value: string): void { this.filter.set(value as NotificationCenterFilter) } markRead(id: string): void { this.items.update((list) => list.map((n) => (n.id === id ? { ...n, read: true } : n))) } markAllRead(): void { this.items.update((list) => list.map((n) => ({ ...n, read: true }))) } }src/app/components/blocks/notification-center/index.ts0.2 kB
export { UiNotificationCenterComponent, type NotificationCenterCategory, type NotificationCenterFilter, type NotificationCenterItem, } from './notification-center.component'
Raw manifest:https://uipkge.dev/r/angular/notification-center.json