Şablonlar
Bildirim merkezi
Üst çubuktaki zil düğmesi; okunmamış sayısı, Tümü ve Okunmamış ayrımı, okundu işaretleme ve boş durumla açılır panel.
Bildirim merkezi, kullanıcıya gelen olayları sayfadan ayrılmadan gösterir. Zil düğmesi okunmamış sayısını taşır; panel bir Popover içindedir ve dışarı tıklayınca ya da Escape ile kapanır. Anlık sonuçlar için Toast, kalıcı geçmiş için bu panel kullanılır (Bildirimler).
Önizleme
Bölüm başlığı “Önizleme”Zile basın. Bir satıra basınca o bildirim okunur ve sayı düşer; “Okunmamış” seçimiyle yalnız yenileri görün; “Tümünü okundu işaretle” ile hepsini kapatın. Sayfa başlığındaki marka ve mod seçimi önizlemeyi de değiştirir.
pnpm add @bigbang/ui| Veri | Tür | Not |
|---|---|---|
| Bildirimler | { id, icon, title, meta, unread }[] |
Yeniden eskiye sıralı gelir; satıra basmak unread’i kapatır. |
state |
'some' | 'empty' |
empty boş durumla başlar. |
defaultOpen |
boolean |
Panel açık başlar. |
AppShell şablonu bu bileşeni üst çubukta arama düğmesiyle hesap menüsü arasına koyar.
import { Badge, Button, EmptyState, Icon, IconButton, Inline, Link, ListItem, Popover, SegmentedControl, Stack, VisuallyHidden } from '@bigbang/ui';import type { IconName } from '@bigbang/ui';import { useState } from 'react';import type { Lang } from '../../patterns/lang';
const copy = { tr: { title: 'Bildirimler', trigger: (unread: number) => (unread > 0 ? `Bildirimler, ${unread} okunmamış` : 'Bildirimler'), show: 'Göster', all: 'Tümü', unreadOnly: 'Okunmamış', unread: 'Okunmamış', markAll: 'Tümünü okundu işaretle', markedAll: 'Tüm bildirimler okundu olarak işaretlendi.', seeAll: 'Tüm bildirimler', emptyTitle: 'Yeni bildirim yok', emptyText: 'Size atanan görevler ve ekipten gelen yanıtlar burada görünür.', unreadEmptyTitle: 'Okunmamış bildirim yok', unreadEmptyText: 'Hepsini okudunuz. Eski bildirimler "Tümü" altında.', items: [ { icon: 'user', title: 'Kullanıcı B sizi Proje 1 ekibine ekledi', meta: '5 dk önce' }, { icon: 'pencil', title: 'Kayıt 12 üzerinde yeni yorum var', meta: '1 saat önce' }, { icon: 'clock', title: 'Görev 3 için son gün yarın', meta: '3 saat önce' }, { icon: 'circle-check', title: 'Dışa aktarma tamamlandı', meta: 'Dün' }, { icon: 'settings', title: 'Çalışma alanı ayarları güncellendi', meta: '2 gün önce' }, ], }, en: { title: 'Notifications', trigger: (unread: number) => (unread > 0 ? `Notifications, ${unread} unread` : 'Notifications'), show: 'Show', all: 'All', unreadOnly: 'Unread', unread: 'Unread', markAll: 'Mark all as read', markedAll: 'All notifications marked as read.', seeAll: 'All notifications', emptyTitle: 'No new notifications', emptyText: 'Tasks assigned to you and replies from your team show up here.', unreadEmptyTitle: 'No unread notifications', unreadEmptyText: 'You have read them all. Older ones are under "All".', items: [ { icon: 'user', title: 'User B added you to the Project 1 team', meta: '5 min ago' }, { icon: 'pencil', title: 'New comment on Record 12', meta: '1 hour ago' }, { icon: 'clock', title: 'Task 3 is due tomorrow', meta: '3 hours ago' }, { icon: 'circle-check', title: 'Export finished', meta: 'Yesterday' }, { icon: 'settings', title: 'Workspace settings updated', meta: '2 days ago' }, ], },};
type Item = { id: number; icon: IconName; title: string; meta: string; unread: boolean };
export type NotificationsCentreProps = { lang?: Lang; /** `empty` starts with no notifications. */ state?: 'some' | 'empty'; /** Opens the panel on first render. */ defaultOpen?: boolean;};
/** * The notification bell of the app header: an unread count on the button and a Popover with an * All / Unread switch, a list where pressing a row marks it read, "Mark all as read" and an empty state. */export function NotificationsCentre({ lang = 'tr', state = 'some', defaultOpen = false }: NotificationsCentreProps) { const t = copy[lang]; const [items, setItems] = useState<Item[]>(() => state === 'empty' ? [] : t.items.map((item, index) => ({ ...item, id: index, icon: item.icon as IconName, unread: index < 3 })), ); const [filter, setFilter] = useState('all'); const [announcement, setAnnouncement] = useState(''); const unread = items.filter((item) => item.unread).length; const shown = filter === 'unread' ? items.filter((item) => item.unread) : items;
const markRead = (id: number) => setItems((list) => list.map((item) => (item.id === id ? { ...item, unread: false } : item))); const markAll = () => { setItems((list) => list.map((item) => ({ ...item, unread: false }))); setAnnouncement(t.markedAll); };
return ( <span className="relative inline-flex"> <Popover title={t.title} side="bottom" align="end" size="lg" defaultOpen={defaultOpen} onOpenChange={(open) => { if (!open) setAnnouncement(''); }} trigger={<IconButton icon="bell" label={t.trigger(unread)} variant="tertiary" size="sm" tooltip={false} />} > <Stack gap="md"> {items.length > 0 ? ( <SegmentedControl label={t.show} size="sm" fullWidth value={filter} onValueChange={setFilter} items={[ { value: 'all', label: t.all }, { value: 'unread', label: unread > 0 ? `${t.unreadOnly} (${unread})` : t.unreadOnly }, ]} /> ) : null} {shown.length > 0 ? ( <ListItem.List divided animate aria-label={t.title} className="-mx-sm max-h-80 overflow-y-auto"> {shown.map((item) => ( <ListItem key={item.id} size="sm" render={<button type="button" />} onClick={() => markRead(item.id)} leading={<Icon name={item.icon} tone={item.unread ? 'brand' : 'subtle'} />} title={ <> {/* The space sits outside the hidden span so the accessible name keeps it. */} {item.unread ? ( <> <VisuallyHidden>{`${t.unread}:`}</VisuallyHidden>{' '} </> ) : null} <span className={item.unread ? 'font-semibold' : undefined}>{item.title}</span> </> } subtitle={item.meta} trailing={item.unread ? <span aria-hidden="true" className="block size-2 rounded-full bg-status-brand-emphasis" /> : null} /> ))} </ListItem.List> ) : ( <EmptyState size="sm" headingLevel={3} icon={items.length === 0 ? 'bell' : 'circle-check'} title={items.length === 0 ? t.emptyTitle : t.unreadEmptyTitle} description={items.length === 0 ? t.emptyText : t.unreadEmptyText} /> )} <Inline justify="between" align="center" gap="sm" wrap> {items.length > 0 ? ( <Button variant="tertiary" size="sm" startIcon={<Icon name="check" />} onClick={markAll} disabled={unread === 0} focusableWhenDisabled> {t.markAll} </Button> ) : ( <span /> )} <Link href="#" size="sm"> {t.seeAll} </Link> </Inline> <VisuallyHidden role="status">{announcement}</VisuallyHidden> </Stack> </Popover> {unread > 0 ? ( // The count is part of the button's name; the badge only shows it. <Badge aria-hidden="true" tone="brand" variant="bold" size="sm" className="pointer-events-none absolute -end-xs -top-xs min-w-5 justify-center"> {unread} </Badge> ) : null} </span> );}- Zil IconButton ve üzerinde okunmamış sayısını gösteren kalın Badge.
- Popover (
size="lg",align="end"), başlık “Bildirimler”. - SegmentedControl “Göster”: Tümü / Okunmamış (sayıyla).
- Düğme satırlarından ListItem listesi: ikon, başlık, zaman; okunmamışlar kalın, sonda bir nokta.
- Liste boşsa
h3başlıklı küçük EmptyState. - Alt satır: “Tümünü okundu işaretle” ve “Tüm bildirimler” Link’i.
Hareket
Bölüm başlığı “Hareket”Panel motion.popover ile zilin altından solup ölçeklenerek açılır. Liste satırları motion.listItem ile kısa aralıklarla girer. Azaltılmış harekette ölçek ve kayma kalkar, yalnızca kısa solma kalır.
Erişilebilirlik
Bölüm başlığı “Erişilebilirlik”- Zilin adı sayıyı içerir: “Bildirimler, 3 okunmamış”. Rozet
aria-hidden’dır; sayı iki kez okunmaz ve yalnız renkle verilmez. - Panel başlığıyla adlandırılmış bir
dialog’dur; açılınca odak ilk denetime gider, Escape kapatıp odağı zile döndürür. - Okunmamış satırların adı görünmez “Okunmamış:” ön ekiyle başlar; kalın yazı ve nokta tek başına anlam taşımaz.
- “Tümünü okundu işaretle” sonucu panel içindeki kibar durum bölgesinde okunur. Okunmamış kalmayınca düğme devre dışı ama odaklanabilir kalır.
- Boş durum başlığı
h3’tür; sayfanınh1’i ve panelin başlığı üstündedir.
Kullanılan bileşenler
Bölüm başlığı “Kullanılan bileşenler”Popover, IconButton, Badge, SegmentedControl, ListItem, EmptyState, Button, Link, VisuallyHidden. Kalıplar: Bildirimler, Boş durumlar.