All examples

Notification inbox

Unread state with a count, mark read and unread, mark all, an unread view that keeps items until you leave it, per-item actions and a polite announcement for new arrivals. Fictional workspace notifications.

Notification inbox

data-inbox · data/activity/inbox

data activity
Props
  • actions
  • filter
  • hrefTrust
  • items
  • now
  • onReadChange

Live example loads in an isolated same-origin preview frame.

Source (data-inbox/InboxDemo.svelte)
<script lang="ts">
import { type InboxNotification, NotificationInbox, setRead } from '@texuli/data/activity/inbox';

// Fictional data only; a fixed clock keeps the time labels stable.
const now = '2026-10-28T12:00:00Z';
let items = $state<InboxNotification[]>([
  {
    id: 'n1',
    title: 'Ada Park mentioned you in Harbor Notes renewal',
    body: '“Can you confirm the seat count before Friday?”',
    at: '2026-10-28T10:42:00Z',
    read: false,
    href: '#deal-harbor',
  },
  {
    id: 'n2',
    title: 'Your export is ready',
    body: 'Deals, October 2026 (CSV)',
    at: '2026-10-28T08:05:00Z',
    read: false,
    href: 'https://app.example.com/exports/7',
  },
  {
    id: 'n3',
    title: 'Bea Ruiz assigned Lumen Atlas pilot to you',
    at: '2026-10-27T16:30:00Z',
    read: true,
  },
  {
    id: 'n4',
    title: 'Weekly pipeline digest',
    body: '5 deals moved, 2 closed.',
    at: '2026-10-21T09:00:00Z',
    read: true,
  },
]);
let archived = $state<string[]>([]);
let arrivals = 0;
const visible = $derived(items.filter((item) => !archived.includes(item.id)));

function simulate(): void {
  arrivals += 1;
  items = [
    {
      id: `new-${arrivals}`,
      title: `Cy Moreau commented on Café Ñandú rollout (${arrivals})`,
      at: '2026-10-28T11:58:00Z',
      read: false,
    },
    ...items,
  ];
}
</script>

<section class="demo" data-example="data-inbox" aria-labelledby="data-inbox-title">
  <h2 id="data-inbox-title">Workspace notifications</h2>
  <button type="button" class="demo-button" onclick={simulate}>Simulate a new notification</button>
  <NotificationInbox
    items={visible}
    {now}
    headingLevel={3}
    onReadChange={(ids, read) => (items = setRead(items, ids, read))}
  >
    {#snippet actions(item)}
      <button
        type="button"
        class="demo-button"
        aria-label={`Archive: ${item.title}`}
        onclick={() => (archived = [...archived, item.id])}>Archive</button
      >
    {/snippet}
  </NotificationInbox>
</section>

<style>
  .demo {
    display: grid;
    gap: 0.75rem;
    max-inline-size: 44rem;
    padding: 1.5rem;
  }
  .demo h2 {
    margin: 0;
  }
  .demo-button {
    justify-self: start;
    min-block-size: 2.25rem;
    padding-inline: 0.75rem;
    border: 1px solid var(--tx-border, ButtonBorder);
    border-radius: var(--tx-radius, 0.5rem);
    background: var(--tx-surface, ButtonFace);
    color: var(--tx-surface-foreground, ButtonText);
    font: inherit;
  }
</style>