Notifications UI

NotificationBell & NotificationCenter components

The portal channel of the Notification service has a ready front-end: a NotificationBell with an unread badge, and a NotificationCenter that lists, paginates and marks notifications as seen. The NotificationCenter is wired to all four generated notification actions (list, unseen-count, mark-seen, mark-all-seen) and renders the rows the backend wrote to the notifications table; the NotificationBell uses only the unseen-count action to drive its badge.

They follow the library contract — actions as props. Only the NotificationCenter is backed by the h-state store (the module-level useNotificationCenterStore singleton); the NotificationBell keeps its own local unread count fed by its own unseenCountAction polling and does not subscribe to that store. Theming is currently limited: you can extend the exported theme object with extendNotificationCenterTheme, but the shipped NotificationBell/NotificationCenter render with the built-in notificationCenterTheme and accept only `className` for per-instance overrides (there is no `theme` prop yet, and `variant` is reserved — see below). Drop the bell in a top bar and the center in a drawer or page, but note the two do NOT auto-sync: marking items seen in the center updates the store-backed center badge, not the bell — the bell's badge only drops on its next pollInterval fetch, so keep pollInterval short when the bell sits next to the center.

NotificationBell#

A compact unread indicator. It polls the unseen count on an interval and renders a badge; clicking it is yours to handle (usually opening the center).

unseenCountActionNotificationUnseenCountActionOptional

The GET /notifications/unseen-count action. The bell calls it on mount and every pollInterval to keep the badge live. The badge is hidden while the count is 0 and displays the raw count otherwise, capping the display at '99+' for any count above 99.

pollIntervalnumber (ms)Optional

How often to refresh the count. Tune for liveness vs. request volume. Defaults to 30000 ms; set 0 (or any non-positive value) to disable polling entirely — the bell then only fetches the count once on mount.

onClick() => voidOptional

Fired when the bell is clicked — open the NotificationCenter, navigate, or whatever your shell needs.

classNamestringOptional

Optional. Merged onto the bell's root <button> element via cn(theme.bell.button, className), so you can add layout or spacing utilities without replacing the theme.

NotificationCenter#

The full list. It loads a page of notifications, supports infinite paging, marks items seen individually or all at once, and routes clicks to your handler.

top bar + drawer
1const a = useApiActions();2 3<NotificationBell4  unseenCountAction={a.NOTIFICATION_UNSEEN_COUNT}5  pollInterval={30000}6  onClick={() => setOpen(true)}7/>;8 9<NotificationCenter10  variant="dropdown"11  listAction={a.NOTIFICATION_LIST}12  unseenCountAction={a.NOTIFICATION_UNSEEN_COUNT}13  markSeenAction={a.NOTIFICATION_MARK_SEEN}14  markAllSeenAction={a.NOTIFICATION_MARK_ALL_SEEN}15  onNotificationClick={(n) =>16    n.entity_name && router.push(`/${n.entity_name}/${n.entity_id}`)17  }18/>;
variant'default' | 'compact' | 'dropdown' (reserved)Optional

Reserved / not yet implemented. The type is accepted but the render ignores it — the center always draws the full panel layout, so there is currently no compact or dropdown code path.

title / subtitlestringOptional

Header text. title defaults to 'Notifications' and renders in the panel header next to the unseen-count badge; subtitle (optional) renders as a line beneath it.

listAction / unseenCountActionactionsOptional

listAction (GET /notifications) pages by { limit, offset } — the shipped center sends only those two; `type` is an accepted list-action filter but the center never uses it (NotificationCenterConfig exposes no type-filter prop). unseenCountAction keeps the header badge in sync. pageSize sets the page length and defaults to 20. The center loads the list and unseen count once on mount and refreshes only via paging or mark-seen actions — it does not poll (pollInterval is accepted on the config but ignored here; only NotificationBell polls).

markSeenAction / markAllSeenActionactionsOptional

Mark one notification (by notification_id) or every unseen one as read — the store updates optimistically and the badge drops. The two differ on the error path: a mark-all failure rolls the optimistic update back by re-fetching the notification list and unseen count (handleMarkAllSeen's onErrorHandle calls fetchNotifications + fetchUnseenCount), whereas a single mark-seen failure only clears the in-flight marking state (setMarkingId(null)) and does NOT re-fetch, so a failed single mark leaves the row optimistically shown as seen until the next reload.

onNotificationClick(NotificationItem) => voidOptional

Each item carries type ('verification' | 'system' | 'custom') plus entity_name / entity_id / source, so your handler can deep-link to the record the notification is about — e.g. open the pending verification it announced. Clicking a notification row is the sole trigger: an UNSEEN row is optimistically marked seen (badge drops) and markSeenAction is called, and onNotificationClick fires only after that mark resolves (NOT if it errors); an already-seen row fires onNotificationClick immediately with no mark-seen call.

classNamestringOptional

Optional. Merged onto the center's root container <div> via cn(theme.container.base, className), so you can size or position the panel without overriding the theme.

Related sections