# NotificationCenter

> Bell button with an unread badge that opens notifications in a popover or a full-height side panel, with All and Unread tabs, mark all read, a header slot and an empty state. Controlled.

Source: https://docs.nasaqui.com/components/notification-center

## Install

```bash
npx shadcn@latest add https://docs.nasaqui.com/r/notification-center.json
```

The header bell. It shows an unread count, and opens a popover with a list of [`NotificationItem`](https://docs.nasaqui.com/components/notification-item) rows, All and Unread tabs, and a "Mark all read" action. It is controlled: it fetches nothing and keeps no read state. You pass `items` and handle `onItemClick` and `onMarkAllRead`.

## When to use

- The notifications entry in an app header, for a short list of recent items.
- `variant="sheet"`: a desktop-style notification centre that slides in from the edge, with room for a widget in `header`.

## When not to use

- A full inbox page or a long, paged history: use a page with [`Timeline`](https://docs.nasaqui.com/components/timeline) or a table.
- A side-over with notification settings: use `Sheet` with `NotificationItem` rows and [`NotificationPreferences`](https://docs.nasaqui.com/components/notification-preferences).
- A transient message: use a toast.

## Import

```tsx
import { NotificationCenter, type NotificationCenterItem } from "@fadymondy/nasaq/web";
// inside this monorepo: "@nasaq/web"
```

## Quick start

```tsx
import { NotificationCenter, type NotificationCenterItem } from "@fadymondy/nasaq/web";
import { useState } from "react";

const initial: NotificationCenterItem[] = [
  { id: "1", actor: { name: "Sara Alharbi" }, title: "Sara mentioned you in MH-142", time: new Date(Date.now() - 300_000), unread: true },
  { id: "2", title: "Invoice INV-031 was paid", time: "2026-09-28T09:00:00Z" },
];

export function HeaderBell() {
  const [items, setItems] = useState(initial);
  return (
    <NotificationCenter
      items={items}
      onItemClick={(item) => setItems((all) => all.map((i) => (i.id === item.id ? { ...i, unread: false } : i)))}
      onMarkAllRead={() => setItems((all) => all.map((i) => ({ ...i, unread: false })))}
    />
  );
}
```

## Anatomy

```
NotificationCenter                 data-slot="notification-center"
├─ Popover trigger                 ghost icon Button, aria-label "Notifications, N unread"
│  └─ badge                        data-slot="notification-center-badge"   Num, "99+" above 99
└─ PopoverContent | SheetContent  data-variant="popover|sheet"
   ├─ header                       title + "Mark all read" Button
   ├─ header slot                  data-slot="notification-center-header", when `header` is set
   ├─ Tabs (underline)             All | Unread (with count)
   └─ panel                        <ul> of NotificationItem, or EmptyState
```

## API

### `NotificationCenter`

`NotificationCenterProps extends Omit<ComponentProps<"div">, "children" | "title">`.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `items` | `readonly NotificationCenterItem[]` | required | The notifications, newest first. |
| `onItemClick?` | `(item) => void` | none | A row was pressed. Mark it read here. |
| `onMarkAllRead?` | `() => void` | none | "Mark all read" was pressed. The button is disabled with no unread. |
| `unreadCount?` | `number` | count of `items` with `unread` | Use when the server total exceeds the loaded items. |
| `open?` / `defaultOpen?` / `onOpenChange?` | `boolean` / `boolean` / `(open: boolean) => void` | uncontrolled | Popover or sheet state. |
| `variant?` | `"popover" \| "sheet"` | `"popover"` | `sheet` opens a full-height side panel; the list fills its height. |
| `header?` | `ReactNode` | none | Shown under the title row, above the tabs: a date, weather or calendar widget. |
| `labels?` | `Partial<NotificationCenterLabels>` | locale strings | Overrides `title`, `all`, `unread`, `markAllRead`, `emptyAll`, `emptyAllDescription`, `emptyUnread`, `emptyUnreadDescription`, `trigger(unread)`. |
| `side?` / `align?` | Popover side / align, or `"start"` / `"end"` | `"bottom"` / `"end"` | Popover placement. With `variant="sheet"`, the edge the panel slides from (default `end`). |

### `NotificationCenterItem`

| Field | Type | Description |
| --- | --- | --- |
| `id` | `string` | Stable key. |
| `title` | `ReactNode` | Headline. |
| `description?` | `ReactNode` | Secondary line. |
| `actor?` / `icon?` | `{ name; avatar? }` / `ReactNode` | Leading avatar or icon. |
| `time?` | `Date \| number \| string` | Shown as a relative time in the active locale (Latin digits, narrow). |
| `unread?` | `boolean` | Accent dot and stronger title. |
| `href?` | `string` | Renders the row as a link. |

## Examples

### Server-side count

```tsx
import { NotificationCenter, type NotificationCenterItem } from "@fadymondy/nasaq/web";

export function Bell({ page, total }: { page: NotificationCenterItem[]; total: number }) {
  return <NotificationCenter items={page} unreadCount={total} />;
}
```

### Overriding a label

```tsx
import { NotificationCenter } from "@fadymondy/nasaq/web";

export function Bell() {
  return <NotificationCenter items={[]} labels={{ title: "التنبيهات" }} />;
}
```

## Context menu

`itemActions={(item) => ContextMenuAction[]}` opens a menu on a row on right-click, Shift+F10 or the Menu key (mark read/unread, remove…). The popover stays open. `contextMenu={false}` opts out.

## Accessibility

| Key | Action |
| --- | --- |
| `Enter` / `Space` on the bell | Opens the popover. |
| `Esc` | Closes it. |
| `Tab` | Moves through the tabs, the action and the rows. |
| `Left` / `Right` on a tab | Switches tab (follows reading direction). |

- The bell's accessible name includes the unread count; the visual badge is `aria-hidden`.
- Unread rows have an sr-only "Unread" label, localised by the provider locale.
- Rows are native buttons or links; the empty state is plain text.

## RTL & i18n

- The badge sits on the inline end of the bell, the popover aligns to the inline end, and rows use logical layout.
- Counts go through `Num`; the cap renders as `99+`.
- Built-in English and Arabic strings, chosen by the provider locale.

## Styling & tokens

- Badge uses the `accent` variant; rows use `hover` and `border` tokens.
- Target `[data-slot=notification-center]` and `[data-slot=notification-center-badge]`; extend with `className`. Never use raw hex.

## Do / Don't

- **Do** load a recent page and link to a full inbox.
- **Do** update `unread` in your state in `onItemClick`.
- **Don't** fetch inside the component; it is presentational.

## Related

- [NotificationItem](https://docs.nasaqui.com/components/notification-item) · [Popover](https://docs.nasaqui.com/components/popover) · [Tabs](https://docs.nasaqui.com/components/tabs) · [States](https://docs.nasaqui.com/components/states)

## Lab

https://docs.nasaqui.com/?path=/docs/components-alerts-notifications-notification-center--docs

## Code

### React

```tsx
import { NotificationCenter, type NotificationCenterItem } from "@fadymondy/nasaq/web";
import { useState } from "react";

const initial: NotificationCenterItem[] = [
  { id: "1", actor: { name: "Sara Alharbi" }, title: "Sara mentioned you in MH-142", time: new Date(Date.now() - 300_000), unread: true },
  { id: "2", title: "Invoice INV-031 was paid", time: "2026-09-28T09:00:00Z" },
];

export function HeaderBell() {
  const [items, setItems] = useState(initial);
  return (
    <NotificationCenter
      items={items}
      onItemClick={(item) => setItems((all) => all.map((i) => (i.id === item.id ? { ...i, unread: false } : i)))}
      onMarkAllRead={() => setItems((all) => all.map((i) => ({ ...i, unread: false })))}
    />
  );
}
```

### shadcn

```tsx
import { NotificationCenter, type NotificationCenterItem } from "@/components/ui/notification-center";
import { useState } from "react";

const initial: NotificationCenterItem[] = [
  { id: "1", actor: { name: "Sara Alharbi" }, title: "Sara mentioned you in MH-142", time: new Date(Date.now() - 300_000), unread: true },
  { id: "2", title: "Invoice INV-031 was paid", time: "2026-09-28T09:00:00Z" },
];

export function HeaderBell() {
  const [items, setItems] = useState(initial);
  return (
    <NotificationCenter
      items={items}
      onItemClick={(item) => setItems((all) => all.map((i) => (i.id === item.id ? { ...i, unread: false } : i)))}
      onMarkAllRead={() => setItems((all) => all.map((i) => ({ ...i, unread: false })))}
    />
  );
}
```

### Vue

```vue
<script setup lang="ts">
import { ref } from "vue";
import { NqNotificationCenter, type NotificationCenterItem } from "@fadymondy/nasaq/vue";

const items = ref<NotificationCenterItem[]>([
  { id: "1", actor: { name: "Sara Alharbi" }, title: "Sara mentioned you in MH-142", time: new Date(Date.now() - 300_000), unread: true },
  { id: "2", title: "Invoice INV-031 was paid", time: "2026-09-28T09:00:00Z" },
]);
</script>

<template>
  <NqNotificationCenter
    :items="items"
    @item-click="(item) => (items = items.map((i) => (i.id === item.id ? { ...i, unread: false } : i)))"
    @mark-all-read="items = items.map((i) => ({ ...i, unread: false }))"
  />
</template>
```

### Blade

```blade
<x-nq::notification-center :items="[
    ['id' => '1', 'title' => 'Sara mentioned you in MH-142', 'description' => 'Can you take a look at the checkout flow?', 'actor' => ['name' => 'Sara Alharbi'], 'time' => 1700000000000, 'unread' => true],
    ['id' => '2', 'title' => 'Invoice INV-031 was paid', 'time' => 1700000000000],
]" />
```

### HTML + Alpine

```html
<div data-slot="notification-center" data-variant="popover" x-data="nqNotificationCenter(JSON.parse('[{\u0022id\u0022:\u00221\u0022,\u0022title\u0022:\u0022Sara mentioned you in MH-142\u0022,\u0022description\u0022:\u0022Can you take a look at the checkout flow?\u0022,\u0022actor\u0022:{\u0022name\u0022:\u0022Sara Alharbi\u0022},\u0022time\u0022:1700000000000,\u0022unread\u0022:true},{\u0022id\u0022:\u00222\u0022,\u0022title\u0022:\u0022Invoice INV-031 was paid\u0022,\u0022time\u0022:1700000000000}]'), {})" x-modelable="open" x-id="['nq-notification-center']"
    class="inline-flex">
    <button data-slot="popover-trigger"
     type="button"                         x-ref="trigger" aria-haspopup="dialog" x-on:click="toggle()" x-bind:aria-expanded="open" x-bind:aria-label="triggerLabel()" aria-label="Notifications, 1 unread" class="inline-flex shrink-0 select-none items-center justify-center gap-2 whitespace-nowrap rounded-control border border-transparent font-sans text-label transition-colors duration-150 ease-nq min-h-[var(--nq-touch-min,0px)] outline-none focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-nq-focus disabled:pointer-events-none disabled:opacity-50 data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0 text-foreground hover:bg-nq-hover size-control p-0 relative">
        <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
  <path d="M10.268 21a2 2 0 0 0 3.464 0"/>
  <path d="M3.262 15.326A1 1 0 0 0 4 17h16a1 1 0 0 0 .74-1.673C19.41 13.956 18 12.499 18 8A6 6 0 0 0 6 8c0 4.499-1.411 5.956-2.738 7.326"/>
</svg>        <span data-slot="badge"     data-notification="badge" aria-hidden="true" x-show="count() &gt; 0" class="inline-flex shrink-0 items-center gap-1 whitespace-nowrap rounded-[4px] border font-medium [&_svg]:size-3 border-nq-accent/40 bg-nq-accent/15 text-nq-accent-text pointer-events-none absolute -end-1 -top-1 h-4 min-w-4 justify-center px-1 text-[10px]"><span x-text="countText()">1</span></span></button>
            <template x-teleport="body">
    <div data-slot="popover-content" x-bind="popup" x-init="popupEl = $el" x-nq-presence="open" x-anchor.bottom-end.offset.6="$refs.trigger"
        class="z-50 max-w-[var(--available-width)] rounded-floating border border-border bg-popover text-body-sm text-popover-foreground shadow-floating outline-none transition-opacity duration-150 ease-nq data-starting-style:opacity-0 data-ending-style:opacity-0 w-[min(24rem,calc(100vw-1rem))] p-0"><div data-slot="tabs" data-orientation="horizontal" x-data="nqTabs('all')" x-modelable="value" x-id="['nq-tabs']" class="flex flex-col gap-0">
    <div class="flex items-center justify-between gap-2 border-b border-border px-4 pt-3">
        <p x-bind:id="$id('nq-notification-center', 'title')" class="text-label text-foreground">Notifications</p>
        <button data-slot="button"
     type="button"                         data-notification="mark-all-read" x-on:click="markAllRead()" x-bind:disabled="count() === 0" class="inline-flex shrink-0 select-none items-center justify-center gap-2 whitespace-nowrap rounded-control border border-transparent font-sans text-label transition-colors duration-150 ease-nq min-h-[var(--nq-touch-min,0px)] outline-none focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-nq-focus disabled:pointer-events-none disabled:opacity-50 data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0 text-foreground hover:bg-nq-hover h-control-sm px-2.5 -mt-1">
        <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
  <path d="M18 6 7 17l-5-5"/>
  <path d="m22 10-7.5 7.5L13 16"/>
</svg> Mark all read</button>
    </div>
        <div data-slot="tabs-list" data-variant="underline" aria-orientation="horizontal" x-bind="list"
    class="relative z-0 flex max-w-full overflow-x-auto [scrollbar-width:none] [&::-webkit-scrollbar]:hidden gap-4 border-b border-border px-4">
    <button type="button" data-slot="tabs-tab" x-bind="tab('all')"
     data-active aria-selected="true"         class="inline-flex shrink-0 items-center gap-1.5 whitespace-nowrap text-label text-muted-foreground outline-none transition-colors duration-150 ease-nq hover:text-foreground data-active:text-foreground [&_svg]:size-4 [&_svg]:shrink-0 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-nq-focus data-disabled:pointer-events-none data-disabled:opacity-50 h-9 px-0.5">All</button>
        <button type="button" data-slot="tabs-tab" x-bind="tab('unread')"
            class="inline-flex shrink-0 items-center gap-1.5 whitespace-nowrap text-label text-muted-foreground outline-none transition-colors duration-150 ease-nq hover:text-foreground data-active:text-foreground [&_svg]:size-4 [&_svg]:shrink-0 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-nq-focus data-disabled:pointer-events-none data-disabled:opacity-50 h-9 px-0.5">Unread <span x-show="count() > 0" x-text="countText()" class="text-caption text-muted-foreground"></span></button>
        <span data-slot="tabs-indicator" aria-hidden="true"
    class="absolute -z-10 transition-[left,width] duration-200 ease-nq left-[var(--active-tab-left)] w-[var(--active-tab-width)] bottom-0 h-0.5 rounded-full bg-primary"></span>
</div>
    <div data-slot="tabs-panel" x-bind="panel('all')"
        class="outline-none focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-nq-focus"><div x-show="rows(false).length === 0"  style="display: none" >
            <div data-slot="empty-state" class="flex flex-col items-center justify-center gap-3 border-dashed border-border px-6 text-center rounded-card border-0 py-10">
            <span data-slot="state-icon" class="inline-flex size-10 items-center justify-center rounded-control border border-border bg-card [&amp;_svg]:size-5 text-muted-foreground">
            <svg aria-hidden="true" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
  <path d="M10.268 21a2 2 0 0 0 3.464 0"/>
  <path d="M17 17H4a1 1 0 0 1-.74-1.673C4.59 13.956 6 12.499 6 8a6 6 0 0 1 .258-1.742"/>
  <path d="m2 2 20 20"/>
  <path d="M8.668 3.01A6 6 0 0 1 18 8c0 2.687.77 4.653 1.707 6.05"/>
</svg>        </span>
        <div class="flex max-w-sm flex-col gap-1">
        <p class="text-label text-foreground">No notifications</p>
                    <p class="text-body-sm text-muted-foreground">Mentions, reviews and activity show up here.</p>
            </div>
    </div>
        </div>
        <ul x-show="rows(false).length > 0" class="m-0 list-none divide-y divide-border overflow-y-auto overscroll-contain p-0 max-h-96" ><template x-for="item in rows(false)" :key="item.id"><li><template x-if="item.href"><a data-slot="notification-item" x-bind:href="item.href" x-bind:data-unread="item.unread ? '' : null" x-on:click="press(item)" class="flex w-full items-start no-underline gap-3 px-4 py-3 text-start outline-none transition-colors duration-150 ease-nq hover:bg-nq-hover focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-nq-focus"><span class="relative mt-0.5 shrink-0">
    <template x-if="item.icon"><span class="flex size-8 items-center justify-center rounded-full border border-border bg-secondary text-muted-foreground [&_svg]:size-4" x-html="item.icon"></span></template>
    <template x-if="!item.icon && item.actor">
        <span data-slot="avatar" class="inline-flex shrink-0 select-none items-center justify-center overflow-hidden bg-secondary align-middle font-medium text-secondary-foreground size-8 text-caption rounded-full">
            <img data-slot="avatar-image" x-show="item.actor.avatar" x-bind:src="item.actor.avatar" x-bind:alt="item.actor.name" class="size-full object-cover">
            <span data-slot="avatar-fallback" x-show="!item.actor.avatar" role="img" x-bind:aria-label="item.actor.name" x-text="initials(item.actor.name)" class="flex size-full items-center justify-center"></span>
        </span>
    </template>
</span>
<span class="flex min-w-0 flex-1 flex-col gap-0.5">
    <span class="text-body-sm" x-bind:class="item.unread ? 'font-medium text-foreground' : 'text-muted-foreground'" x-text="item.title"></span>
    <span x-show="item.description" class="line-clamp-2 text-caption text-muted-foreground" x-text="item.description"></span>
</span>
<span class="flex shrink-0 flex-col items-end gap-1.5 pt-0.5">
    <time x-show="item.time !== undefined" x-bind:datetime="isoTime(item.time)" x-text="timeText(item.time)" class="text-caption text-muted-foreground tabular-nums"></time>
    <span x-show="item.unread" class="size-2 rounded-full bg-nq-accent"><span class="sr-only">Unread</span></span>
</span></a></template><template x-if="!item.href"><button type="button" data-slot="notification-item" x-bind:data-unread="item.unread ? '' : null" x-on:click="press(item)" class="flex w-full items-start no-underline gap-3 px-4 py-3 text-start outline-none transition-colors duration-150 ease-nq hover:bg-nq-hover focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-nq-focus"><span class="relative mt-0.5 shrink-0">
    <template x-if="item.icon"><span class="flex size-8 items-center justify-center rounded-full border border-border bg-secondary text-muted-foreground [&_svg]:size-4" x-html="item.icon"></span></template>
    <template x-if="!item.icon && item.actor">
        <span data-slot="avatar" class="inline-flex shrink-0 select-none items-center justify-center overflow-hidden bg-secondary align-middle font-medium text-secondary-foreground size-8 text-caption rounded-full">
            <img data-slot="avatar-image" x-show="item.actor.avatar" x-bind:src="item.actor.avatar" x-bind:alt="item.actor.name" class="size-full object-cover">
            <span data-slot="avatar-fallback" x-show="!item.actor.avatar" role="img" x-bind:aria-label="item.actor.name" x-text="initials(item.actor.name)" class="flex size-full items-center justify-center"></span>
        </span>
    </template>
</span>
<span class="flex min-w-0 flex-1 flex-col gap-0.5">
    <span class="text-body-sm" x-bind:class="item.unread ? 'font-medium text-foreground' : 'text-muted-foreground'" x-text="item.title"></span>
    <span x-show="item.description" class="line-clamp-2 text-caption text-muted-foreground" x-text="item.description"></span>
</span>
<span class="flex shrink-0 flex-col items-end gap-1.5 pt-0.5">
    <time x-show="item.time !== undefined" x-bind:datetime="isoTime(item.time)" x-text="timeText(item.time)" class="text-caption text-muted-foreground tabular-nums"></time>
    <span x-show="item.unread" class="size-2 rounded-full bg-nq-accent"><span class="sr-only">Unread</span></span>
</span></button></template></li></template></ul></div>
    <div data-slot="tabs-panel" x-bind="panel('unread')"
     hidden     class="outline-none focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-nq-focus"><div x-show="rows(true).length === 0"  style="display: none" >
            <div data-slot="empty-state" class="flex flex-col items-center justify-center gap-3 border-dashed border-border px-6 text-center rounded-card border-0 py-10">
            <span data-slot="state-icon" class="inline-flex size-10 items-center justify-center rounded-control border border-border bg-card [&amp;_svg]:size-5 text-muted-foreground">
            <svg aria-hidden="true" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
  <path d="M10.268 21a2 2 0 0 0 3.464 0"/>
  <path d="M17 17H4a1 1 0 0 1-.74-1.673C4.59 13.956 6 12.499 6 8a6 6 0 0 1 .258-1.742"/>
  <path d="m2 2 20 20"/>
  <path d="M8.668 3.01A6 6 0 0 1 18 8c0 2.687.77 4.653 1.707 6.05"/>
</svg>        </span>
        <div class="flex max-w-sm flex-col gap-1">
        <p class="text-label text-foreground">You are all caught up</p>
                    <p class="text-body-sm text-muted-foreground">There is nothing new to read.</p>
            </div>
    </div>
        </div>
        <ul x-show="rows(true).length > 0" class="m-0 list-none divide-y divide-border overflow-y-auto overscroll-contain p-0 max-h-96" ><template x-for="item in rows(true)" :key="item.id"><li><template x-if="item.href"><a data-slot="notification-item" x-bind:href="item.href" x-bind:data-unread="item.unread ? '' : null" x-on:click="press(item)" class="flex w-full items-start no-underline gap-3 px-4 py-3 text-start outline-none transition-colors duration-150 ease-nq hover:bg-nq-hover focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-nq-focus"><span class="relative mt-0.5 shrink-0">
    <template x-if="item.icon"><span class="flex size-8 items-center justify-center rounded-full border border-border bg-secondary text-muted-foreground [&_svg]:size-4" x-html="item.icon"></span></template>
    <template x-if="!item.icon && item.actor">
        <span data-slot="avatar" class="inline-flex shrink-0 select-none items-center justify-center overflow-hidden bg-secondary align-middle font-medium text-secondary-foreground size-8 text-caption rounded-full">
            <img data-slot="avatar-image" x-show="item.actor.avatar" x-bind:src="item.actor.avatar" x-bind:alt="item.actor.name" class="size-full object-cover">
            <span data-slot="avatar-fallback" x-show="!item.actor.avatar" role="img" x-bind:aria-label="item.actor.name" x-text="initials(item.actor.name)" class="flex size-full items-center justify-center"></span>
        </span>
    </template>
</span>
<span class="flex min-w-0 flex-1 flex-col gap-0.5">
    <span class="text-body-sm" x-bind:class="item.unread ? 'font-medium text-foreground' : 'text-muted-foreground'" x-text="item.title"></span>
    <span x-show="item.description" class="line-clamp-2 text-caption text-muted-foreground" x-text="item.description"></span>
</span>
<span class="flex shrink-0 flex-col items-end gap-1.5 pt-0.5">
    <time x-show="item.time !== undefined" x-bind:datetime="isoTime(item.time)" x-text="timeText(item.time)" class="text-caption text-muted-foreground tabular-nums"></time>
    <span x-show="item.unread" class="size-2 rounded-full bg-nq-accent"><span class="sr-only">Unread</span></span>
</span></a></template><template x-if="!item.href"><button type="button" data-slot="notification-item" x-bind:data-unread="item.unread ? '' : null" x-on:click="press(item)" class="flex w-full items-start no-underline gap-3 px-4 py-3 text-start outline-none transition-colors duration-150 ease-nq hover:bg-nq-hover focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-nq-focus"><span class="relative mt-0.5 shrink-0">
    <template x-if="item.icon"><span class="flex size-8 items-center justify-center rounded-full border border-border bg-secondary text-muted-foreground [&_svg]:size-4" x-html="item.icon"></span></template>
    <template x-if="!item.icon && item.actor">
        <span data-slot="avatar" class="inline-flex shrink-0 select-none items-center justify-center overflow-hidden bg-secondary align-middle font-medium text-secondary-foreground size-8 text-caption rounded-full">
            <img data-slot="avatar-image" x-show="item.actor.avatar" x-bind:src="item.actor.avatar" x-bind:alt="item.actor.name" class="size-full object-cover">
            <span data-slot="avatar-fallback" x-show="!item.actor.avatar" role="img" x-bind:aria-label="item.actor.name" x-text="initials(item.actor.name)" class="flex size-full items-center justify-center"></span>
        </span>
    </template>
</span>
<span class="flex min-w-0 flex-1 flex-col gap-0.5">
    <span class="text-body-sm" x-bind:class="item.unread ? 'font-medium text-foreground' : 'text-muted-foreground'" x-text="item.title"></span>
    <span x-show="item.description" class="line-clamp-2 text-caption text-muted-foreground" x-text="item.description"></span>
</span>
<span class="flex shrink-0 flex-col items-end gap-1.5 pt-0.5">
    <time x-show="item.time !== undefined" x-bind:datetime="isoTime(item.time)" x-text="timeText(item.time)" class="text-caption text-muted-foreground tabular-nums"></time>
    <span x-show="item.unread" class="size-2 rounded-full bg-nq-accent"><span class="sr-only">Unread</span></span>
</span></button></template></li></template></ul></div>
</div></div>
</template>
    </div>
```
