NotificationCenter
Menu-ready notification list with mark-as-read, delete, and empty state. Renders each notification as a clickable card with timestamp.
01 Usage
Basic
Notifications
<script lang="ts">
import { NotificationCenter } from '@urbicon-ui/auth';
const demoNotifications = [
{
id: '1',
userId: 'demo',
title: 'Welcome!',
body: 'Your account has been created.',
type: 'system',
typeKey: 'welcome',
url: null,
icon: null,
createdAt: new Date(Date.now() - 5 * 60000),
readAt: null
},
{
id: '2',
userId: 'demo',
title: 'New feature available',
body: 'Passkey login is now supported.',
type: 'system',
typeKey: 'feature',
url: null,
icon: null,
createdAt: new Date(Date.now() - 3600000),
readAt: new Date()
}
];
</script>
<NotificationCenter
notifications={demoNotifications}
onMarkAsRead={(id) => console.log('Mark as read:', id)}
onMarkAllAsRead={() => console.log('Mark all as read')}
onDelete={(id) => console.log('Delete:', id)}
/>
02 Accessibility
Each notification is a button, not a clickable row
The body of every item is a real <button> rather than a click handler on the <li>. That
is what makes it reachable with Tab and activatable with Enter or Space without any extra ARIA — a div with an onclick would have needed a role, a tabindex and a key
handler to reach the same place.
Unread state does not reach assistive tech
The unread dot is correctly aria-hidden="true" — it
is decoration. But nothing replaces it: the read/unread distinction exists only as that
dot, a background tint, and a data-unread attribute
for CSS. A screen-reader user cannot currently tell a read notification from an unread
one. Use the item snippet if your application needs that distinction
spoken.
The delete button is icon-only and generically named
It renders a × glyph, so an aria-label is mandatory and present — but it is the bare
localized "Delete", without the notification's title. Compare PasskeyManager and InvitationManager,
which append the row's subject; in a list of ten notifications this one gives ten identically
named buttons.
The list is a list; the timestamp is not yet a timestamp
Items sit in a real <ul>, so the count is
announced before the contents. The relative age uses a <time> element but supplies no datetime attribute, and its text ("2 hours ago") is not
a valid datetime string — so the element carries no machine-readable date and gives assistive
tech nothing the plain text would not. Treat it as styling, not semantics.
03 API Reference
Prop | Type | Default | Description | |
|---|---|---|---|---|
notifications required | import('../../../server/adapters/types.js').NotificationRecord[] | — | Notification records to display. | |
class | string | — | Extra classes on the root element. | |
item | Snippet<[import('../../../server/adapters/types.js').NotificationRecord]> | — | Custom notification item renderer. | |
onDelete | (id: string) => void | — | Called when a notification is deleted. | |
onMarkAllAsRead | () => void | — | Called when all notifications are marked as read. | |
onMarkAsRead | (id: string) => void | — | Called when a single notification is marked as read. | |
onNotificationClick | (
notification: import('../../../server/adapters/types.js').NotificationRecord
) => void | — | Called when a notification is clicked (e.g. to navigate to a URL).
SECURITY: notification.url is DB-/server-sourced and untrusted — before
passing it to goto() / window.location, validate it is same-origin or
relative (reject javascript: and absolute cross-origin URLs). Never
navigate to a raw notification.url. | |
slotClasses | Partial<Record<'root' | 'header' | 'list' | 'item' | 'empty', string>> | — | Per-slot class overrides. See component source for available slot keys. | |
t | PartialAuthLocale | — | Locale overrides, deep-merged over the active built-in bundle (resolved from the i18n context). Pass any subset — a single string or a whole tree. | |
unstyled | boolean | — | Strip all default styling. |
04 Types
Local type definitions used by this component.
Name | Kind | Category | Used by | Description | |
|---|---|---|---|---|---|
NotificationCenterProps | interface | props | 0 | — | |
PartialAuthLocale | type | helper | 1 | Consumer-facing locale input: any subset of AuthLocale. Components
accept this as their t prop and deep-merge it over the active built-in
bundle, so overriding a single string never silently blanks the rest. | |
DeepPartial | type | helper | 0 | Recursive partial: every branch and leaf becomes optional. | |
AuthLocale | interface | helper | 0 | The complete auth locale bundle. Every key is required: the bundles this
package ships (en, de) satisfy the full shape, and consumer overrides
enter as PartialAuthLocale — deep-merged over the active built-in
bundle by mergeAuthLocale — so component markup reads keys directly,
without per-key ?? '…' fallback literals (review R19).
Placeholder convention: dynamic values use **single-brace** tokens
({n}, {name}, {email}) that the consuming component substitutes itself
via String.replace('{token}', value). There is deliberately **no**
{{…}} runtime interpolator in this package — the key-based translator twin
(authT/at) was removed in R21 — so {{…}} here would render verbatim. |
05 Installation
Import
import { NotificationCenter } from '@urbicon-ui/auth';