<script lang="ts">
import { untrack } from "svelte";
import { resolve } from "$app/paths";
import ArrowRight from "$icon/arrow-right";
import Bell from "$icon/bell";
import Button, { buttonIcon } from "#lib/components/ui/Button.svelte";
import Dropdown from "#lib/components/ui/Dropdown.svelte";
import { createLoad } from "#lib/action.svelte.ts";
import {
createFixtureNotifications,
getNotifications,
type Notifications
} from "#lib/notifications.svelte.ts";
import { notificationCategory } from "#lib/components/notifications/notification.ts";
import NotificationList from "#lib/components/notifications/NotificationList.svelte";
import NotificationToolbar from "#lib/components/notifications/NotificationToolbar.svelte";
import type {
CategoryFilter,
NotificationSummary,
ReadFilter
} from "#lib/components/notifications/types.ts";
interface Props {
/** optional fixture override for storybook; omitted → loads from deliberi */
notifications?: NotificationSummary[];
}
let { notifications: fixtures }: Props = $props();
const shared: Notifications = untrack(() =>
fixtures
? createFixtureNotifications(fixtures)
: (getNotifications() ?? createFixtureNotifications([]))
);
const PREVIEW_LIMIT = 5;
let readFilter = $state<ReadFilter>("unread");
let categoryFilter = $state<CategoryFilter>("all");
const previewLoad = createLoad(() => shared.list({ read: readFilter, limit: PREVIEW_LIMIT }));
const loaded = $derived(previewLoad.data ?? []);
const all = $derived(shared.overlay(loaded));
const badge = $derived(shared.unread > 99 ? "99+" : String(shared.unread));
const unreadOnLoad = $derived(new Set(loaded.filter((n) => !n.read).map((n) => n.uri)));
const preview = $derived(
all
.filter((n) => readFilter !== "unread" || unreadOnLoad.has(n.uri))
.filter(
(n) => categoryFilter === "all" || notificationCategory(n.type) === categoryFilter
)
.slice(0, PREVIEW_LIMIT)
);
const dismiss = (event: MouseEvent & { currentTarget: HTMLAnchorElement }) => {
const popover = event.currentTarget.closest("[popover]");
if (popover instanceof HTMLElement) popover.hidePopover();
};
const viewAll = $derived(
resolve(
(categoryFilter === "all"
? `/notifications?read=${readFilter}`
: `/notifications?read=${readFilter}&category=${categoryFilter}`) as "/"
)
);
</script>
{#snippet count()}
{#if shared.unread > 0}
<span
data-testid="unreadCount"
data-count={shared.unread}
class="pointer-events-none absolute -right-0.5 flex h-4 min-w-4 items-center justify-center rounded-full bg-background-info-emphasis px-2xs typography-paragraph-mini text-foreground-on-emphasis"
aria-hidden="true"
>
{badge}
</span>
{/if}
{/snippet}
<div class="relative flex md:hidden">
<Button
variant="ghost"
size="small"
href={resolve("/notifications")}
icon={Bell}
aria-label="Notifications, {shared.unread} unread"
/>
{@render count()}
</div>
<div class="relative hidden md:flex">
<Dropdown
square
id="notifications-menu"
group="topbar"
variant="button"
buttonVariant="ghost"
size="small"
align="right"
label="Notifications, {shared.unread} unread"
menuClass="w-[36rem] divide-y divide-border-default p-0 [--panel-border:0px]"
>
{#snippet trigger()}
<Bell class={buttonIcon({ size: "regular" })} aria-hidden="true" />
{/snippet}
<div class="p-xs">
<NotificationToolbar
{readFilter}
{categoryFilter}
hasNotifications={preview.length > 0}
preferences={false}
onReadFilter={(filter) => (readFilter = filter)}
onCategoryFilter={(filter) => (categoryFilter = filter)}
onMarkAllRead={shared.markAllRead}
/>
</div>
<NotificationList
loading={previewLoad.loading && !previewLoad.data}
notifications={preview}
{readFilter}
border={false}
onToggleRead={shared.setRead}
/>
<a
href={viewAll}
onclick={dismiss}
class="flex items-center justify-end gap-2xs p-xs typography-paragraph-small text-foreground-muted no-underline hover:bg-background-subtle hover:text-foreground-default hover:no-underline md:px-md"
>
View all
<ArrowRight class="size-3.5" aria-hidden="true" />
</a>
</Dropdown>
{@render count()}
</div>