Komponen / Lapisan & umpan balik
Notification
Panel notifikasi ala Mail: titik biru untuk yang belum dibaca, filter Semua / Belum dibaca / Sebutan, dan "Tandai semua dibaca". Dibuka dari lonceng AppHeader.
Panel notifikasi
Lebar 380, radius 16, Material/Thick. Item belum dibaca berteks fg/default dengan titik biru; yang sudah dibaca fg/muted. Klik baris membuka dokumen dan menandainya dibaca. Hitungan di filter "Belum dibaca" ikut berkurang.
Notifikasi
import { useState } from 'react';
import { Button, Notification } from '@divistant/circca-react';
import { Receipt } from '@phosphor-icons/react';
// Panel notifikasi BizOps: filter Semua / Belum dibaca / Sebutan, titik biru belum dibaca, "Tandai semua dibaca".
// `now` tetap agar waktu relatif sama di server dan klien.
const NOW = new Date(2026, 9, 8, 10, 0);
const AWAL = [
{ id: 'n1', actor: 'Dimas Prasetyo', ref: 'PRJ-2026-0088', time: new Date(2026, 9, 8, 9, 55), unread: true, mention: true, text: 'Dimas Prasetyo menyebut Anda di PRJ-2026-0088: "Mohon cek jadwal UAT sebelum dikirim ke customer."' },
{ id: 'n2', actor: 'Anisa Nugraha', ref: 'PO-2026-0142', time: new Date(2026, 9, 8, 9, 0), unread: true, mention: false, text: 'Anisa Nugraha menyetujui PO-2026-0142.' },
{ id: 'n3', actor: 'Rina Kartika', ref: 'PO-2026-0146', time: new Date(2026, 9, 7, 16, 20), unread: true, mention: false, text: 'Rina Kartika mengembalikan PO-2026-0146: dokumen PO customer belum bertanda tangan.' },
{ id: 'n4', ref: 'INV-2026-0311', time: new Date(2026, 9, 3, 8, 0), unread: false, mention: false, text: 'Invoice INV-2026-0311 jatuh tempo besok.' },
];
export default function Example() {
const [items, setItems] = useState(AWAL);
const [filter, setFilter] = useState('semua');
const unread = items.filter(i => i.unread).length;
const shown = items.filter(i => (filter === 'belum-dibaca' ? i.unread : filter === 'sebutan' ? i.mention : true));
const baca = (id: string) => setItems(list => list.map(i => (i.id === id ? { ...i, unread: false } : i)));
return (
<Notification
now={NOW}
titleAs="h3"
unreadCount={unread}
onMarkAllRead={() => setItems(list => list.map(i => ({ ...i, unread: false })))}
filters={[
{ value: 'semua', label: 'Semua' },
{ value: 'belum-dibaca', label: 'Belum dibaca', count: unread },
{ value: 'sebutan', label: 'Sebutan' },
]}
filter={filter}
onFilterChange={setFilter}
footer={
<Button variant="ghost" size="sm">
Lihat semua notifikasi
</Button>
}
>
{shown.map(i => (
<Notification.Item
key={i.id}
actor={i.actor}
icon={i.actor ? undefined : Receipt}
reference={i.ref}
time={i.time}
unread={i.unread}
onClick={() => baca(i.id)}
>
{i.text}
</Notification.Item>
))}
</Notification>
);
}Di Popover dari AppHeader
Pemicu AppHeader.Notifications, panel `bare` di dalam Popover (padding Popover dinolkan). Jumlah belum dibaca masuk ke label tombol lonceng dan diumumkan sopan saat berubah.
import { useState } from 'react';
import { AppHeader, Button, Notification, Popover } from '@divistant/circca-react';
// Panel dipasang di Popover yang dipicu AppHeader.Notifications. `bare` melepas bingkai panel karena Popover sudah
// punya material, garis, dan bayangan; padding Popover dinolkan agar baris notifikasi selebar panel (380).
// landmark={false} karena contoh ini disarangkan di halaman dokumentasi yang sudah punya banner.
const NOW = new Date(2026, 9, 8, 10, 0);
export default function Example() {
const [unread, setUnread] = useState(['n1', 'n2']);
const isUnread = (id: string) => unread.includes(id);
const baca = (id: string) => setUnread(list => list.filter(x => x !== id));
return (
<div className="rounded-xl border-[0.5px] border-muted bg-canvas">
<AppHeader
landmark={false}
notifications={
<Popover
aria-label="Notifikasi"
align="end"
trigger={<AppHeader.Notifications unread={unread.length} />}
className="w-[min(380px,calc(100vw-1rem))] gap-0 rounded-2xl p-0 [&>div]:mx-0 [&>div]:px-0"
>
<Notification
bare
now={NOW}
className="w-full"
unreadCount={unread.length}
onMarkAllRead={() => setUnread([])}
footer={
<Popover.Close asChild>
<Button variant="ghost" size="sm">
Lihat semua notifikasi
</Button>
</Popover.Close>
}
>
<Notification.Item actor="Dimas Prasetyo" reference="PRJ-2026-0088" time={new Date(2026, 9, 8, 9, 55)} unread={isUnread('n1')} onClick={() => baca('n1')}>
Dimas Prasetyo menyebut Anda di PRJ-2026-0088.
</Notification.Item>
<Notification.Item actor="Anisa Nugraha" reference="PO-2026-0142" time={new Date(2026, 9, 8, 9, 0)} unread={isUnread('n2')} onClick={() => baca('n2')}>
Anisa Nugraha menyetujui PO-2026-0142.
</Notification.Item>
<Notification.Item actor="Hendra Wijaya" reference="QTN-2026-0031" time={new Date(2026, 9, 7, 14, 10)}>
Hendra Wijaya menyetujui diskon 7% untuk QTN-2026-0031.
</Notification.Item>
</Notification>
</Popover>
}
user={<AppHeader.User name="Anisa Nugraha" />}
/>
</div>
);
}Keadaan kosong
Tanpa item tampil EmptyState Compact bawaan. Ganti lewat `empty` untuk filter yang kosong, mis. "Semua sudah dibaca".
Notifikasi
Tidak ada notifikasi
Persetujuan, sebutan, dan tenggat yang perlu Anda ketahui akan muncul di sini.
Notifikasi
Semua sudah dibaca
Notifikasi baru tentang PO, invoice, dan proyek Anda akan muncul di sini.
import { Button, EmptyState, Notification } from '@divistant/circca-react';
import { Checks } from '@phosphor-icons/react';
// Tanpa item: EmptyState Compact bawaan, atau isi `empty` sendiri — mis. saat filter "Belum dibaca" kosong.
export default function Example() {
return (
<div className="flex flex-wrap items-start gap-4">
<Notification titleAs="h3" />
<Notification
titleAs="h3"
unreadCount={0}
filters={[
{ value: 'semua', label: 'Semua' },
{ value: 'belum-dibaca', label: 'Belum dibaca' },
{ value: 'sebutan', label: 'Sebutan' },
]}
defaultFilter="belum-dibaca"
empty={
<EmptyState
size="compact"
icon={Checks}
titleAs="p"
title="Semua sudah dibaca"
description="Notifikasi baru tentang PO, invoice, dan proyek Anda akan muncul di sini."
className="px-4"
/>
}
footer={
<Button variant="ghost" size="sm">
Lihat semua notifikasi
</Button>
}
/>
</div>
);
}