Menu

Change language

GitHub repo

Notification Center

blockcommunication

Full-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

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

Variants

Loading interactive previews…

Schema

Type aliases exported from this item's source. Use these to shape the data you pass in.

NotificationCenterItem
interface NotificationCenterItem {
  id: string
  category: NotificationCenterCategory
  title: string
  body: string
  date: Date
  read: boolean
}

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

Raw manifest:https://uipkge.dev/r/angular/notification-center.json