مرکز اعلان (NotificationCenter)

مرکز اعلان مبتنی بر Popover با نشان شمارنده، severity رنگ‌بندی، فیلترهای تب، و عملیات خوانده‌شده/بستن

معرفی

NotificationCenter یک دکمه‌ی زنگ با نشان شمارنده‌ی اعلان‌های خوانده‌نشده است. دکمه‌ی زنگ یک Button فقط‌آیکون (ghost) روی نردبان کنترل‌هاست: بدون Provider مربعی 30 پیکسلی، و داخل ردیفی با ControlSizeProvider (مثل actions قاب ProductFrame) هم‌قد همان ردیف — هم‌قد ThemeToggle و UserMenu کنارش (تا 3٫x دکمه‌ای دست‌ساز و 36 پیکسلی بود). کلیک روی آن یک Popover باز می‌کند که لیست اعلان‌ها را با ترتیب زمانی و دسته‌بندی severity نشان می‌دهد. کاربر می‌تواند هر اعلان را خوانده‌شده علامت بزند، ببندد، یا با کلیک روی متن اعلان به مقصد هدایت شود.

چه زمانی استفاده کنیم:

  • در actions قاب برنامه (ProductFrame)، پیش از منوی کاربر، برای آگاه‌سازی از هشدارها، بولتن‌های جدید، تکمیل job، و خطاهای سیستمی
  • برای صف هشدارهای افکارسنجی (مثل «گسترش ناگهانی پست بحرانی» یا «کاهش دقت کراولر»)
  • همراه با Toast (Sonner): توست برای اعلام لحظه‌ای، NotificationCenter برای تاریخچه

چه زمانی استفاده نکنیم:

  • برای نمایش یک پیام مهم دائمی در بالای صفحه — از Banner استفاده کنید
  • برای خطای فرم — error همان FormRow (خطای فیلد) یا error قالب (FormPage، SettingsSection: خطای ذخیره)؛ برای توضیح context در صفحه — Alert
  • برای تأیید destructive — از AlertDialog / ConfirmDialog استفاده کنید
← روی زنگ کلیک کنید

زمین بازی

با تغییر تنظیمات زیر، پیش‌نمایش زنده را مشاهده کنید.

زمین بازی
تنظیمات
داده
3
حالت
کد این نمونه به‌صورت خودکار قابل تولید نیست — برای کد آماده‌ی copy/paste به بخش «استفاده» در بالای صفحه مراجعه کنید.

استفاده

'use client'
import * as React from 'react'
import { NotificationCenter, type NotificationItem } from '@partodata/ui'

function Header() {
  const [items, setItems] = React.useState<NotificationItem[]>([
    {
      id: 'n1',
      title: 'هشدار گسترش پست بحرانی',
      body: 'پست خوشه «رونمایی محصول جدید» در 20 دقیقه اخیر 3400 بازنشر گرفت.',
      timestamp: new Date(Date.now() - 3 * 60 * 1000),
      severity: 'critical',
      read: false,
      action: { label: 'باز کردن خوشه', onClick: () => router.push('/clusters/42') },
    },
  ])

  return (
    <NotificationCenter
      items={items}
      onMarkRead={(id) => setItems((p) => p.map((i) => (i.id === id ? { ...i, read: true } : i)))}
      onMarkAllRead={() => setItems((p) => p.map((i) => ({ ...i, read: true })))}
      onDismiss={(id) => setItems((p) => p.filter((i) => i.id !== id))}
    />
  )
}

severity و رنگ‌بندی

پنج نوع severity پشتیبانی می‌شود. هر severity یک رنگ خط‌حاشیه‌ی شروع + رنگ نقطه/آیکون اختصاصی دارد:

severityتوکن منبعمورد استفاده
info--brand-defaultپیش‌فرض — اطلاع‌رسانی عادی (گزارش آماده، کاربر جدید)
success--sentiment-positiveتکمیل موفقیت‌آمیز یک job (تحلیل کامنت تکمیل شد)
warning--warning-defaultهشدارهای غیر-بحرانی (کاهش دقت، نزدیک شدن به quota)
destructive--destructive-defaultخطای سیستم، از دست رفتن اتصال
critical--status-criticalبحران دامنه‌ای افکارسنجی (گسترش ناگهانی پست، شکست تهدیدات)

فیلترهای تب

پنل می‌تواند چند تب فیلتر داشته باشد که کاربر با کلیک بینشان جابه‌جا می‌شود:

← پالت با تب‌های فیلتر
<NotificationCenter
  items={items}
  filters={[
    { key: 'all', label: 'همه', predicate: () => true },
    { key: 'unread', label: 'خوانده‌نشده', predicate: (i) => !i.read },
    { key: 'critical', label: 'بحرانی', predicate: (i) => i.severity === 'critical' },
  ]}
  defaultFilter="unread"
/>

overflow نشان شمارنده

اگر تعداد خوانده‌نشده‌ها از 99 عبور کند، نشان به +99 (یا 99+ در انگلیسی) تبدیل می‌شود تا width ثابت بماند:

← 137 اعلان — نشان «+99»

ادغام با Toast

الگوی رایج: اعلان‌های لحظه‌ای با toast() نمایش داده می‌شوند، و هم‌زمان به state اپ اضافه می‌شوند تا در NotificationCenter بمانند.

import { toast } from 'sonner'

function emit(item: NotificationItem) {
  setNotifications((p) => [item, ...p])
  toast(item.title, { description: item.body })
}

Props

Prop

Type

NotificationItem

Prop

Type

راهنمای استفاده

بکنید

  • state آیتم‌ها را در store مرکزی اپ نگه دارید (zustand/redux/context). NotificationCenter presentational است و داده را نمی‌سازد - برای اعلان‌های real-time، toast() را برای لحظه‌ای + افزودن به state برای تاریخچه هم‌زمان اجرا کنید - در اپ‌های چندبخشی، از filters برای تفکیک منبع اعلان (سیستم / افکارسنجی / عملیات) استفاده کنید - برای اعلان‌های پایدار که کاربر نباید حذفشان کند (مثل گزارش امضاء‌شده)، onDismiss را نپاس کنید

نکنید

  • بیش از 50 اعلان را یک‌جا render نکنید — اعلان‌های قدیمی را به صفحه‌ی «تاریخچه» ببرید یا با hasMore/onLoadMore صفحه‌بندی کنید - severity را به دلخواه استفاده نکنید — هر severity معنای مشخص دارد - NotificationCenter را درون مودال یا Drawer قرار ندهید — جایگاه قراردادی آن در Header است - برای نمایش وضعیت online/offline از severity استفاده نکنید — این یک پیام کوتاه، نه تاریخچه‌ای است

دسترسی‌پذیری

  • دکمه‌ی زنگ aria-label دارد که در حالت وجود اعلان خوانده‌نشده شامل تعداد آن‌هاست
  • نشان شمارنده aria-hidden="true" است چون اطلاعات آن در aria-label دکمه قرار دارد
  • تب‌های فیلتر الگوی کامل tab را پیاده می‌کنند: role="tablist" + role="tab" + aria-selected + aria-controls روی هر تب و role="tabpanel" روی فهرست اعلان‌ها
  • کل نوار تب‌ها یک توقفگاه Tab است (roving tabindex)؛ جابه‌جایی بین تب‌ها با کلیدهای جهت‌دار و Home/End انجام می‌شود. نگاشت کلیدها بر اساس جهت سند است — در صفحه‌ی RTL، ← تب بعدی و → تب قبلی است
  • ردیفی که onSelect دارد یک دکمه‌ی کشیده روی تمام سطح ردیف می‌سازد که نامش از عنوان اعلان گرفته می‌شود؛ دکمه‌ی action و دکمه‌های «خوانده شد» / «بستن» روی آن قرار می‌گیرند، پس هیچ دکمه‌ای داخل دکمه‌ی دیگر قرار نمی‌گیرد
  • زمان هر اعلان با <time datetime="..."> markup شده تا assistive tech آن را درست بخواند
  • تمرکز صفحه‌کلید با Tab روی دکمه‌ی زنگ → Enter برای باز کردن → Tab درون پنل
  • Escape پنل را می‌بندد (از Popover primitive)

کامپوننت‌های مرتبط

  • Banner — برای پیام مهم دائمی در بالای صفحه
  • Callout — برای پیام درون‌صفحه‌ای (context)؛ خطای فرم نه: آن error ردیف یا قالب است
  • Sonner — برای اعلان لحظه‌ای گذرا
  • AlertDialog — برای تأیید destructive