@12-apps/notifications 4.13.0 → 4.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -3,7 +3,7 @@ import {
3
3
  createInboxStore,
4
4
  useBadgeState,
5
5
  useUnreadCount
6
- } from "./chunk-2IAHFIXS.js";
6
+ } from "./chunk-AN6IX4AE.js";
7
7
  import {
8
8
  messagesOf
9
9
  } from "./chunk-M2TVBVH2.js";
@@ -285,7 +285,7 @@ import { Suspense, lazy, useEffect, useState } from "react";
285
285
  import { jsx as jsx2 } from "react/jsx-runtime";
286
286
  function lazyNotificationsPanel(parts) {
287
287
  const Bound = lazy(async () => {
288
- const { NotificationsPanel } = await import("./panel-MKI4PTNZ.js");
288
+ const { NotificationsPanel } = await import("./panel-JL5RDEHY.js");
289
289
  return {
290
290
  default: /* @__PURE__ */ __name((props) => /* @__PURE__ */ jsx2(NotificationsPanel, { ...props, ...parts }), "default")
291
291
  };
@@ -395,4 +395,4 @@ export {
395
395
  httpNotificationsTransport,
396
396
  createWebNotifications
397
397
  };
398
- //# sourceMappingURL=chunk-4DTUD74E.js.map
398
+ //# sourceMappingURL=chunk-643HNPQJ.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/react/api.ts","../src/react/transport.ts","../src/react/create-web-notifications.tsx","../src/react/bell-badge.ts","../src/react/live-seen.ts","../src/react/bell-button.tsx","../src/react/panel-lazy.tsx","../src/react/page-lazy.tsx"],"sourcesContent":["import type { ChannelRow } from '../preferences-core';\nimport type { NotificationChannel } from '../types';\nimport type { ListNotificationsResult } from '../wire';\n\nimport type { NotificationsResult, NotificationsTransport } from './transport';\n\n/**\n * The wire client, bound to one mount (12-15).\n *\n * Every path this package's screens can call, in one place — which is what\n * makes the api half's route table and the web half's URLs one contract instead\n * of two lists that drift.\n */\n\n/** `GET <mount>/notification-preferences` and the PUT's answer. */\nexport interface PreferencesPayload {\n preferences: Record<string, ChannelRow>;\n availability: Record<NotificationChannel, boolean>;\n /** The host's taxonomy, so the screen renders it without being told twice. */\n categories: string[];\n}\n\n/** `GET <mount>/push-subscriptions`. */\nexport interface PushRegistrationPayload {\n /** null = web push is not configured on this deployment. */\n vapidPublicKey: string | null;\n count: number;\n /**\n * Whether the endpoint asked about is still registered to the caller. Present\n * only when one was passed — see {@link NotificationsApiClient.getPushRegistration}.\n */\n registered?: boolean;\n}\n\nexport interface NotificationsApiClient {\n listNotifications(input: {\n cursor?: string | null;\n limit?: number;\n filter?: 'all' | 'unread';\n }): Promise<ListNotificationsResult>;\n unreadCount(): Promise<number>;\n markRead(ids: readonly string[]): Promise<NotificationsResult<{ updated: number }>>;\n markAllRead(): Promise<NotificationsResult<{ updated: number }>>;\n remove(ids: readonly string[]): Promise<NotificationsResult<{ deleted: number }>>;\n getPreferences(): Promise<PreferencesPayload>;\n savePreference(\n category: string,\n channel: NotificationChannel,\n enabled: boolean,\n ): Promise<NotificationsResult<PreferencesPayload>>;\n /**\n * The deployment's VAPID key and the caller's device count — and, when an\n * `endpoint` is passed, whether the SERVER still has that exact subscription\n * under the caller's id. The browser holding a subscription object is not\n * evidence of that: a re-own or a 404/410 prune drops the row and leaves the\n * browser's object in place.\n */\n getPushRegistration(input?: { endpoint?: string }): Promise<PushRegistrationPayload>;\n savePushSubscription(input: {\n endpoint: string;\n keys: { p256dh: string; auth: string };\n }): Promise<NotificationsResult<{ count: number }>>;\n removePushSubscription(endpoint: string): Promise<NotificationsResult<{ count: number }>>;\n}\n\nexport function createNotificationsApiClient(\n apiBase: string,\n transport: NotificationsTransport,\n): NotificationsApiClient {\n const base = apiBase.replace(/\\/$/, '');\n const url = (path: string): string => `${base}${path}`;\n\n return {\n listNotifications({ cursor, limit, filter }) {\n const params = new URLSearchParams();\n if (limit !== undefined) params.set('limit', String(limit));\n if (cursor) params.set('cursor', cursor);\n if (filter) params.set('filter', filter);\n const query = params.toString();\n return transport.get<ListNotificationsResult>(\n url(`/notifications${query ? `?${query}` : ''}`),\n );\n },\n async unreadCount() {\n const { count } = await transport.get<{ count: number }>(\n url('/notifications/unread-count'),\n );\n return count;\n },\n markRead: (ids) =>\n transport.send(url('/notifications/mark-read'), 'POST', { ids: [...ids] }),\n markAllRead: () => transport.send(url('/notifications/mark-read'), 'POST', { all: true }),\n remove: (ids) => transport.send(url('/notifications/delete'), 'POST', { ids: [...ids] }),\n getPreferences: () => transport.get<PreferencesPayload>(url('/notification-preferences')),\n savePreference: (category, channel, enabled) =>\n transport.send(url('/notification-preferences'), 'PUT', {\n [category]: { [channel]: enabled },\n }),\n getPushRegistration: ({ endpoint } = {}) =>\n transport.get<PushRegistrationPayload>(\n url(\n endpoint\n ? `/push-subscriptions?endpoint=${encodeURIComponent(endpoint)}`\n : '/push-subscriptions',\n ),\n ),\n savePushSubscription: (input) => transport.send(url('/push-subscriptions'), 'POST', input),\n removePushSubscription: (endpoint) =>\n transport.send(url('/push-subscriptions'), 'DELETE', { endpoint }),\n };\n}\n","/**\n * How the notification screens reach their data (12-15) — the report-builder\n * transport doctrine: this is the ONLY way the surface performs I/O, so a\n * caller supplying one has substituted the entire backend without stubbing a\n * global. The default is same-origin `fetch` riding the browser's cookies.\n */\n\n/** A write outcome the screens branch on — never a thrown mutation. */\nexport type NotificationsResult<T> = { ok: true; data: T } | { ok: false; error: string };\n\n/** A failed read, carrying the status the screens branch on (401 = signed out). */\nexport class NotificationsHttpError extends Error {\n readonly status: number;\n constructor(status: number, message: string) {\n super(message);\n this.name = 'NotificationsHttpError';\n this.status = status;\n Object.setPrototypeOf(this, NotificationsHttpError.prototype);\n }\n}\n\nexport interface NotificationsTransport {\n /** A read. Returns the payload INSIDE the `{ data }` envelope. */\n get<T>(path: string): Promise<T>;\n /** A write. Returns a {@link NotificationsResult} rather than rejecting. */\n send<T>(path: string, method: string, body?: unknown): Promise<NotificationsResult<T>>;\n}\n\n/**\n * @param fallbackError What a failed write says when the server sent no\n * sentence of its own — REQUIRED, the host's words. `createWebNotifications`\n * already passes its (equally required) `messages.operationFailed`; only a\n * host constructing the transport directly writes it here. The old default\n * was one application's Portuguese, and the only string in this package the\n * required-messages port did not cover.\n */\nexport function httpNotificationsTransport(fallbackError: string): NotificationsTransport {\n return {\n async get<T>(path: string): Promise<T> {\n const response = await fetch(path, {\n credentials: 'same-origin',\n headers: { Accept: 'application/json' },\n });\n const payload = (await response.json().catch(() => null)) as\n | { data?: T; error?: string }\n | null;\n if (!response.ok) {\n throw new NotificationsHttpError(\n response.status,\n payload?.error ?? `HTTP ${response.status} for ${path}`,\n );\n }\n return (payload?.data ?? payload) as T;\n },\n\n async send<T>(path: string, method: string, body?: unknown): Promise<NotificationsResult<T>> {\n try {\n const response = await fetch(path, {\n method,\n credentials: 'same-origin',\n headers: {\n Accept: 'application/json',\n ...(body === undefined ? {} : { 'Content-Type': 'application/json' }),\n },\n ...(body === undefined ? {} : { body: JSON.stringify(body) }),\n });\n if (response.status === 204) return { ok: true, data: undefined as T };\n const payload = (await response.json().catch(() => null)) as\n | { data?: T; error?: string }\n | null;\n if (!response.ok) return { ok: false, error: payload?.error ?? fallbackError };\n return { ok: true, data: (payload?.data ?? payload) as T };\n } catch {\n return { ok: false, error: fallbackError };\n }\n },\n };\n}\n","import { useState, type ComponentType, type JSX } from 'react';\n\nimport { messagesOf, type NotificationMessages } from '../messages';\n\nimport { createNotificationsApiClient, type NotificationsApiClient } from './api';\nimport { useInboxBellBadge, useLiveBellBadge, type BellBadge } from './bell-badge';\nimport { BellButton, LiveBellButton, type BellButtonProps } from './bell-button';\nimport {\n useUnreadCount,\n type NotificationsSignalHook,\n type NotificationsSubscribe,\n} from './hooks';\nimport { createInboxStore, type InboxStore } from './inbox-state';\nimport type { LiveActivitiesConfig } from './live-config';\nimport { createLiveSeenStore, type LiveSeenStore } from './live-seen';\nimport { lazyNotificationsPanel } from './panel-lazy';\nimport type { NotificationsPanelProps } from './panel';\nimport { lazyPreferencesPage } from './page-lazy';\nimport type { PreferencesScreenProps } from './preferences-screen';\nimport { httpNotificationsTransport, type NotificationsTransport } from './transport';\nimport type { WebPushSetupConfig } from './web-push-setup';\n\n/**\n * The one thing this package exposes to a FRONTEND host (12-15).\n *\n * Everything the notification centre IS — the bell with its live badge, the\n * slide-over inbox with its optimistic mark-read / delete / mark-all and its\n * cursor pager, the preferences matrix with its availability hints and the\n * per-browser push enable step, and every wire call between them — lives inside\n * this package. The host names where the API is mounted, and that is the whole\n * wiring.\n *\n * `page` is the standalone surface (the preferences screen), which is the one\n * thing a host routes to. The bell and the panel are a PAIR a host drops into\n * its own chrome, and they share one store, so a read in the panel moves the\n * badge in the same tick.\n */\n\nexport interface NotificationsWebConfig {\n /** The account mount the routes live under, e.g. `/api/account`. */\n apiBase: string;\n /** How the surface reaches its data. Default: same-origin fetch. */\n transport?: NotificationsTransport;\n /** User-facing copy overrides (pt-BR product copy by default). */\n messages: NotificationMessages;\n /**\n * How the surface learns an inbox changed without asking — the host's message\n * bus. Without it the badge keeps its 60 s poll, which is the standing\n * contract rather than a fallback: a dropped event must cost latency, never\n * correctness.\n */\n subscribe?: NotificationsSubscribe;\n /**\n * The same wiring as a HOOK, for a host whose realtime connection lives in\n * React context — see `NotificationsSignalHook`. `subscribe` is read at\n * factory time, which such a host cannot reach.\n */\n useSignal?: NotificationsSignalHook;\n /** The browser push enable step's host seams (SW path, platform hint). */\n webPush?: WebPushSetupConfig;\n /**\n * LIVE ACTIVITIES — the ongoing-state entries pinned above the inbox list.\n *\n * Opt-in, and absent means absent: a host that passes nothing gets the panel\n * it had, with no section, no heading and no reserved space. See\n * `./live-config` for the two things a host has to supply (where they come\n * from, and what the section says) and `../live` for what one IS.\n */\n liveActivities?: LiveActivitiesConfig;\n}\n\nexport interface WebNotifications {\n /**\n * The routed surface: the preferences screen.\n *\n * Loaded on demand — see `page-lazy.tsx`. A host that mounts only the bell and\n * the panel never downloads it, and a host that routes to it fetches it while\n * entering that route.\n */\n page: ComponentType<PreferencesScreenProps>;\n /** The bell, already bound to the shared store. */\n BellButton: ComponentType<BellButtonProps>;\n /**\n * The inbox slide-over, sharing that store.\n *\n * Loaded the first time it is opened — see `panel-lazy.tsx`. Until then a\n * host's chrome carries the bell and nothing else.\n */\n Panel: ComponentType<NotificationsPanelProps>;\n /**\n * Bell + panel as ONE element, for a host that just wants the feature in its\n * header and does not want to own the open/closed state.\n */\n BellWithPanel: ComponentType<{\n enabled?: boolean;\n onNavigate?: (link: string) => void;\n }>;\n /**\n * The unread INBOX count.\n *\n * For a host with its own trigger chrome only when that host configured no\n * live activities — otherwise it is a bell that ignores everything happening\n * right now, and `useBellBadge` below is the door. Still the right hook for\n * anything that genuinely wants \"how many unread rows\".\n */\n useUnreadCount: (options?: BadgeHookOptions) => number;\n /**\n * The badge's NUMBER AND TONE, for a host with its own trigger chrome.\n *\n * What `useUnreadCount` should have been for a host that also configured live\n * activities, and the reason it is a second door rather than a change to that\n * one: a count alone cannot express a bell, because a live entry is present\n * without being news (see `./bell-badge`). A host that renders\n * `useUnreadCount` in its own chrome gets a badge that ignores everything\n * happening right now — which is not a subtle wrongness, it is the pinned\n * pedido on screen going uncounted.\n *\n * Identical to what this package's own `BellButton` draws, because it is the\n * hook that bell uses. Without live activities configured it is\n * `useUnreadCount` plus `hasNew: count > 0`.\n */\n useBellBadge: (options?: BadgeHookOptions) => BellBadge;\n /** The shared client state, for host glue. */\n store: InboxStore;\n /** The bound wire client. */\n api: NotificationsApiClient;\n /** The copy in force, so a host's own chrome can reuse a sentence. */\n messages: NotificationMessages;\n}\n\n/**\n * What a host passes either badge hook: whether to run at all, and — for a host\n * that holds its own realtime connection — whether that connection is up\n * (`BadgeSyncOptions.live`).\n */\nexport interface BadgeHookOptions {\n enabled?: boolean;\n live?: boolean;\n}\n\n/** What the factory passes both badge hooks: whatever realtime wiring it has. */\ntype SubscribeOption = {\n subscribe?: NotificationsSubscribe;\n useSignal?: NotificationsSignalHook;\n};\n\n/**\n * The two badge hooks, bound to this factory's store.\n *\n * `useBellBadge` is chosen ONCE here, the same way `Bell` is below and for the\n * same reason: `live.useActivities` is a hook, so which implementation runs\n * must not be a per-render decision.\n */\nfunction bindBadgeHooks(\n store: InboxStore,\n subscribeOption: SubscribeOption,\n liveSeen: LiveSeenStore,\n live: LiveActivitiesConfig | undefined,\n): Pick<WebNotifications, 'useUnreadCount' | 'useBellBadge'> {\n return {\n useUnreadCount: (options = {}) => useUnreadCount(store, { ...options, ...subscribeOption }),\n useBellBadge: live\n ? (options = {}) => useLiveBellBadge(store, live, liveSeen, { ...options, ...subscribeOption })\n : (options = {}) => useInboxBellBadge(store, { ...options, ...subscribeOption }),\n };\n}\n\nexport function createWebNotifications(config: NotificationsWebConfig): WebNotifications {\n const messages = messagesOf(config);\n const api = createNotificationsApiClient(\n config.apiBase,\n config.transport ?? httpNotificationsTransport(messages.operationFailed),\n );\n const store = createInboxStore(api);\n const webPush = config.webPush ?? {};\n const subscribe = config.subscribe;\n const subscribeOption = {\n ...(subscribe ? { subscribe } : {}),\n ...(config.useSignal ? { useSignal: config.useSignal } : {}),\n };\n\n // One store per factory, shared by the bell that READS it and the panel that\n // WRITES it — the same arrangement as the inbox store above, and for the same\n // reason: two independent copies would disagree about what the reader saw.\n const liveSeen = createLiveSeenStore();\n\n // Chosen ONCE, here, because `useActivities` is a hook and the choice must\n // not be made per render: a bell that read an optional config inside itself\n // would be calling a hook conditionally.\n const live = config.liveActivities;\n const Bell: ComponentType<BellButtonProps> = live\n ? (props) => (\n <LiveBellButton\n {...props}\n store={store}\n messages={messages}\n live={live}\n seen={liveSeen}\n {...subscribeOption}\n />\n )\n : (props) => (\n <BellButton {...props} store={store} messages={messages} {...subscribeOption} />\n );\n const Panel = lazyNotificationsPanel({\n store,\n messages,\n ...(live ? { live, liveSeen } : {}),\n });\n\n const badgeHooks = bindBadgeHooks(store, subscribeOption, liveSeen, live);\n\n function BellWithPanel({\n enabled = true,\n onNavigate,\n }: {\n enabled?: boolean;\n onNavigate?: (link: string) => void;\n }): JSX.Element {\n const [open, setOpen] = useState(false);\n return (\n <>\n <Bell enabled={enabled} onClick={() => setOpen(true)} />\n <Panel\n open={open}\n onClose={() => setOpen(false)}\n {...(onNavigate ? { onNavigate } : {})}\n />\n </>\n );\n }\n\n return {\n page: lazyPreferencesPage({ api, messages, webPush }),\n BellButton: Bell,\n Panel,\n BellWithPanel,\n ...badgeHooks,\n store,\n api,\n messages,\n };\n}\n","/**\n * What the bell shows: ONE number, and whether any of it is news.\n *\n * The badge answers two different questions with one glyph, and keeping them\n * apart is the whole design:\n *\n * - the COUNT is how many things the centre is holding for the reader;\n * - the TONE is whether any of them has happened since they last looked.\n *\n * An inbox row makes those the same question — an unread row is by definition\n * both present and unseen — which is why the distinction did not exist before\n * live activities did. A live activity separates them: a pedido that has been\n * `Preparo` for ten minutes is still worth a `1`, and shouting about it every\n * render is how a badge teaches people to stop reading it.\n *\n * ## This is here so a host can DRAW it\n *\n * The numbers were already correct inside this package's own `BellButton`, and\n * unreachable from a host that cannot take that component — a header whose cart\n * and search buttons are one styled icon-button is importing a second trigger\n * style the moment it does. Such a host had `useUnreadCount` and nothing else,\n * so its bell showed NOTHING while a pinned pedido sat inside the panel it\n * opens. Both bells now read these hooks, so a host cannot drift from what this\n * package renders.\n *\n * ## What it does NOT yet do\n *\n * A live subject usually also writes inbox rows as it moves, and this counts\n * both: a pedido with one unread row about it reads `2`. Subtracting the double\n * needs the server to say which unread rows name which subject, and that was\n * built, reviewed and pulled — for reasons about the CONTRACT rather than the\n * arithmetic, and worth recording so the next attempt starts past them:\n *\n * - it added a field to `GET /notifications/unread-count`, and at least one\n * adopter publishes that response as a closed schema to LLM clients. An\n * additive field is a breaking change against `additionalProperties: false`.\n * - the scan is per READER, so every host paid it — including the two SPAs in\n * that adopter that share one factory and configure no live activities at\n * all, and read the count through `useUnreadCount`, which never sees the\n * breakdown.\n * - it narrowed `NotificationsApiClient.unreadCount()` from `Promise<number>`,\n * which is a breaking change on a commit the release rules cut as a minor.\n *\n * The way through is an opt-in the surface asks for — a host with no live\n * activities then sends nothing different and receives nothing different.\n *\n * (An earlier revision of this docblock blamed a missing index instead. That\n * was wrong: `[userId, deletedAt, readAt]` is a full equality prefix over the\n * filter, and the `ORDER BY` the scan carried was not load-bearing, since a\n * tally does not care what order it counts in.)\n */\nimport { useMemo, useSyncExternalStore } from 'react';\n\nimport { useBadgeState, type BadgeSyncOptions } from './hooks';\nimport type { InboxStore } from './inbox-state';\nimport type { LiveActivitiesConfig } from './live-config';\nimport { hasUnseenActivity, type LiveSeenStore } from './live-seen';\n\n/** The bell's whole state — see the file docblock for what each half means. */\nexport interface BellBadge {\n /** What the badge shows. `0` renders no badge at all. */\n count: number;\n /**\n * Whether any of it has arrived or moved since the reader last looked.\n *\n * The trigger paints this as its accent colour; a host with its own chrome\n * decides how to say it, but it should be a difference somebody notices.\n */\n hasNew: boolean;\n}\n\n/**\n * The badge for a host with no live activities: unread rows, and that is all.\n *\n * `hasNew` is `count > 0` here, and not as a simplification — an UNREAD row is\n * one the reader has not seen, so for this host presence and novelty really are\n * the same fact.\n */\nexport function useInboxBellBadge(store: InboxStore, options: BadgeSyncOptions = {}): BellBadge {\n // `useBadgeState` already blanks itself when disabled — the gate lives there,\n // once, rather than at each of the three hooks that layer on it.\n const { unread } = useBadgeState(store, options);\n // MEMOISED, unlike the number `useUnreadCount` returns. `useSyncExternalStore`\n // re-renders on every `patch` and `patch` always allocates, so a poll that\n // comes back with an unchanged count would otherwise hand a host a new object\n // every 60 s — enough to re-fire a `useEffect` keyed on it, or defeat a\n // `React.memo` on the trigger, forever.\n return useMemo(() => ({ count: unread, hasNew: unread > 0 }), [unread]);\n}\n\n/**\n * The badge for a host that configured live activities.\n *\n * A SECOND hook rather than a flag on the one above, for the reason the bell\n * itself is two components: `live.useActivities` is a hook, so a single hook\n * reading an optional config would be calling one conditionally — which React\n * reports as a crash somewhere else entirely. The factory knows statically\n * which host it is building for and binds one.\n *\n * ## `enabled` is enforced HERE, not taken on trust\n *\n * A host is explicitly allowed to ignore the `active` hint and always answer —\n * `./live-config` calls that \"behaving correctly and merely paying for it\" — so\n * a signed-out header, which still MOUNTS the bell, can be handed a list of\n * somebody's pedidos. The guard below is the only thing between that and a\n * badge counting them.\n *\n * Defensive against the CONTRACT, not against an observed adopter: today's one\n * honours the hint on every lever it has. That is exactly why the guard needs\n * saying — nothing about the current tree would fail if it went, and the case\n * that covers it has to build the ignoring host itself.\n *\n * ## What it costs the host, stated plainly\n *\n * The bell is mounted for as long as the app is, so unlike the panel's copy of\n * this hook there is no \"nobody is looking\" state to stand down in — `active`\n * is simply `enabled`. A host that answers by polling therefore polls for every\n * signed-in reader whether or not they ever open the centre. That is the price\n * of a badge that knows about live activities at all, and the reason to answer\n * this hook from a pushed cache rather than from an interval.\n */\nexport function useLiveBellBadge(\n store: InboxStore,\n live: LiveActivitiesConfig,\n seen: LiveSeenStore,\n options: BadgeSyncOptions = {},\n): BellBadge {\n const enabled = options.enabled ?? true;\n const { unread } = useBadgeState(store, options);\n const activities = live.useActivities({ active: enabled });\n const seenAt = useSyncExternalStore(seen.subscribe, seen.read, seen.read);\n // The store's own half is already blanked by `useBadgeState`; the `enabled`\n // guard here is for the ACTIVITIES half, which comes from a host hook that\n // may have ignored the hint.\n //\n // A live entry COUNTS. It is a notification — it is the one the reader most\n // wants to know about — and the panel it opens lists it.\n const count = enabled ? unread + activities.length : 0;\n const hasNew = enabled && (unread > 0 || hasUnseenActivity(activities, seenAt));\n // Memoised on the two RESULTS, not on `activities`. A host's hook returns a\n // fresh array every render — the storefront's maps its query's rows, so\n // structural sharing keeps the DATA identical and the array new — so an\n // `activities` dependency would invalidate on every render and the memo would\n // buy nothing at all. `hasUnseenActivity` runs unmemoised in front of it,\n // which is a `.some()` over the handful of things happening at once.\n return useMemo(() => ({ count, hasNew }), [count, hasNew]);\n}\n","/**\n * What the reader has already been shown, so the bell can say NEW rather than\n * merely PRESENT.\n *\n * A live activity is unlike an inbox row in the one way that matters here: it\n * stays on the panel for as long as the thing is happening, so its presence\n * cannot mean \"you have not seen this\". A pedido that has been `Preparo` for\n * ten minutes is still live and still worth counting, but nothing has happened\n * — and a badge that shouts for a subject the reader has already looked at is a\n * badge people stop reading.\n *\n * So presence and novelty are answered separately: the COUNT comes from how\n * many are live, and the TONE comes from this. The panel writes it — being on\n * screen is what seen means — and the bell reads it.\n *\n * ## Per subject, not one watermark\n *\n * A single \"newest instant already seen\" is smaller and was the first cut, and\n * it is wrong in a way that shows up in normal use: a pedido placed ten minutes\n * ago but only now reaching the client arrives with an `updatedAt` BEHIND the\n * watermark, and would be silently marked as already seen. The reader has never\n * laid eyes on it. Keyed by subject, an id that has not been recorded is new\n * whatever its clock says.\n *\n * Bounded by pruning rather than by expiry: every write keeps only the subjects\n * that are live at that moment, so the record can never outgrow the number of\n * things happening at once. A subject that finishes and later comes back is\n * news again, which is correct — it is a different occurrence.\n */\nimport type { LiveActivity } from '../live';\n\nconst STORAGE_KEY = '12a.notifications.live-seen';\n\n/** id -> the `updatedAt` that was on screen. */\ntype SeenMap = Readonly<Record<string, string>>;\n\nconst EMPTY: SeenMap = {};\n\n/** ms since epoch, or `null` for an absent or unparseable stamp. */\nfunction instant(iso: string | undefined): number | null {\n if (iso === undefined) return null;\n const ms = Date.parse(iso);\n return Number.isNaN(ms) ? null : ms;\n}\n\n/**\n * Read/write through `try`, every time.\n *\n * `localStorage` is not merely absent in SSR and in a worker — the ACCESSOR\n * itself throws in a browser set to block site data. A notification bell that\n * cannot render because storage is blocked is a worse failure than one that\n * forgets what was seen, and forgetting degrades in the safe direction: towards\n * saying something is happening.\n */\nfunction readStored(): SeenMap {\n try {\n const raw = globalThis.localStorage?.getItem(STORAGE_KEY);\n if (!raw) return EMPTY;\n const parsed: unknown = JSON.parse(raw);\n if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) return EMPTY;\n // Anything can be in storage — another version of this package, or a person\n // with the devtools open. Keep only what has the shape this reads.\n const clean: Record<string, string> = {};\n for (const [id, value] of Object.entries(parsed)) {\n if (typeof value === 'string') clean[id] = value;\n }\n return clean;\n } catch {\n return EMPTY;\n }\n}\n\nfunction writeStored(value: SeenMap): void {\n try {\n globalThis.localStorage?.setItem(STORAGE_KEY, JSON.stringify(value));\n } catch {\n // Blocked or full. The badge stays new a while longer; nothing else breaks.\n }\n}\n\nexport interface LiveSeenStore {\n /** What has been shown, keyed by subject id. */\n read: () => SeenMap;\n /** Record that exactly these are on screen now, forgetting subjects that are not. */\n mark: (activities: readonly LiveActivity[]) => void;\n subscribe: (listener: () => void) => () => void;\n}\n\nexport function createLiveSeenStore(): LiveSeenStore {\n // Mirrored in memory as well as in storage: `useSyncExternalStore` compares\n // snapshots by IDENTITY and calls `read` on every render, so parsing storage\n // there would hand it a fresh object each time and re-render for ever.\n let current = readStored();\n const listeners = new Set<() => void>();\n\n return {\n read: () => current,\n mark: (activities) => {\n const next: Record<string, string> = {};\n for (const activity of activities) next[activity.id] = activity.updatedAt;\n // Identity is the snapshot, so an unchanged map must not become a new\n // object — see `read` above.\n const ids = Object.keys(next);\n const same =\n ids.length === Object.keys(current).length &&\n ids.every((id) => current[id] === next[id]);\n if (same) return;\n current = next;\n writeStored(next);\n for (const listener of listeners) listener();\n },\n subscribe: (listener) => {\n listeners.add(listener);\n return () => {\n listeners.delete(listener);\n };\n },\n };\n}\n\n/**\n * Whether any of these has moved, or arrived, since the reader last looked.\n *\n * An id with nothing recorded is new — that is the case the per-subject record\n * exists for. An unparseable stamp is treated as new too: the alternative is\n * silently never alerting for a host whose clock format this does not read.\n */\nexport function hasUnseenActivity(\n activities: readonly LiveActivity[],\n seen: SeenMap,\n): boolean {\n return activities.some((activity) => {\n const shown = instant(seen[activity.id]);\n if (shown === null) return true;\n const now = instant(activity.updatedAt);\n return now === null || now > shown;\n });\n}\n","/**\n * Bare bell trigger with the live unread badge — for hosts that do not already\n * have a styled icon-button slot.\n *\n * A host with its own trigger chrome uses `useBellBadge` + `Panel` directly,\n * and NOT `useUnreadCount`, which is what this sentence used to say. That\n * advice was taken, verbatim and by name, by a storefront whose header needed\n * its own trigger — and it gave that storefront a bell showing nothing at all\n * while a live pedido sat in the panel it opens, because `useUnreadCount`\n * counts inbox rows and knows nothing about what is happening right now.\n */\nimport type { JSX } from 'react';\n\nimport { Badge } from '@12-apps/ui/data-display/Badge';\nimport { Box } from '@12-apps/ui/mui/Box';\n\nimport type { NotificationMessages } from '../messages';\n\nimport { useInboxBellBadge, useLiveBellBadge } from './bell-badge';\nimport { BellIcon } from './bell-icon';\nimport type { NotificationsSignalHook, NotificationsSubscribe } from './hooks';\nimport type { LiveActivitiesConfig } from './live-config';\nimport type { LiveSeenStore } from './live-seen';\nimport type { InboxStore } from './inbox-state';\n\nconst triggerSx = {\n display: 'inline-flex',\n alignItems: 'center',\n justifyContent: 'center',\n p: 0.5,\n border: 'none',\n background: 'none',\n cursor: 'pointer',\n color: 'text.primary',\n lineHeight: 0,\n '& *': { cursor: 'pointer' },\n '&:hover': { color: 'primary.main' },\n '&:focus-visible': {\n outline: '2px solid',\n outlineColor: 'primary.main',\n outlineOffset: '2px',\n borderRadius: '50%',\n },\n} as const;\n\nexport interface BellButtonProps {\n onClick: () => void;\n /** Signed-out hosts still mount the bell; `false` silences it. */\n enabled?: boolean;\n}\n\n/**\n * The trigger itself, given a count and whether any of it is NEW.\n *\n * Presentational, and shared by both bells below, so the two can never drift on\n * what the badge looks like — only on where the number comes from.\n *\n * ## The two tones\n *\n * `primary` says *something happened*; `neutral` says *something is present*. A\n * live activity is the reason that distinction has to exist: it stays on the\n * panel for as long as the thing is happening, so a bell that painted every\n * live entry as new would be permanently red for a pedido the reader already\n * looked at, and a bell that ignored them would say nothing at all while one\n * was running. Grey keeps the count honest without spending attention twice.\n */\nfunction BellTrigger({\n onClick,\n count,\n hasNew,\n messages,\n}: {\n onClick: () => void;\n count: number;\n hasNew: boolean;\n messages: NotificationMessages;\n}): JSX.Element {\n return (\n <Box\n component=\"button\"\n type=\"button\"\n onClick={onClick}\n // `openBellWithUnread` rather than a new message, and not for want of\n // precision: `NotificationMessages` is REQUIRED of every host, so adding\n // a field is a breaking change to a package several apps already mount.\n // The sentence a host wrote for \"you have N\" is the sentence this wants.\n aria-label={count > 0 ? messages.openBellWithUnread(count) : messages.openBell}\n data-testid=\"notifications-bell\"\n sx={triggerSx}\n >\n <Badge\n content={count > 0 ? count : undefined}\n color={hasNew ? 'primary' : 'neutral'}\n variant=\"count\"\n max={99}\n data-testid=\"notifications-badge\"\n // The tone is carried by a colour, and a colour is not something a\n // test can read — nor, on its own, a signal every reader can. This is\n // what the tests assert on.\n data-tone={hasNew ? 'new' : 'seen'}\n >\n <BellIcon size={28} />\n </Badge>\n </Box>\n );\n}\n\nexport function BellButton({\n onClick,\n enabled = true,\n store,\n messages,\n subscribe,\n useSignal,\n}: BellButtonProps & {\n store: InboxStore;\n messages: NotificationMessages;\n subscribe?: NotificationsSubscribe;\n useSignal?: NotificationsSignalHook;\n}): JSX.Element {\n const badge = useInboxBellBadge(store, {\n enabled,\n ...(subscribe ? { subscribe } : {}),\n ...(useSignal ? { useSignal } : {}),\n });\n return <BellTrigger onClick={onClick} {...badge} messages={messages} />;\n}\n\n/**\n * The bell for a host that configured live activities.\n *\n * A SECOND component rather than a flag on the one above, because the host's\n * `useActivities` is a hook: reading an optional config inside one component\n * would mean calling it conditionally, which React reports as a crash in some\n * unrelated component rather than here. The factory knows statically which host\n * it is building for and picks one.\n *\n * What the number MEANS, and what it costs the host, is `bell-badge.ts` — the\n * same hook a host with its own trigger chrome reaches through the factory's\n * `useBellBadge`, so the two bells can never disagree about the count.\n */\nexport function LiveBellButton({\n onClick,\n enabled = true,\n store,\n messages,\n subscribe,\n useSignal,\n live,\n seen,\n}: BellButtonProps & {\n store: InboxStore;\n messages: NotificationMessages;\n subscribe?: NotificationsSubscribe;\n useSignal?: NotificationsSignalHook;\n live: LiveActivitiesConfig;\n seen: LiveSeenStore;\n}): JSX.Element {\n const badge = useLiveBellBadge(store, live, seen, {\n enabled,\n ...(subscribe ? { subscribe } : {}),\n ...(useSignal ? { useSignal } : {}),\n });\n return <BellTrigger onClick={onClick} {...badge} messages={messages} />;\n}\n","/**\n * The inbox slide-over, fetched the first time somebody opens it.\n *\n * The bell and the panel are a PAIR a host drops into its chrome, and that is\n * still true — but only the BELL is on screen when a page paints. The panel is\n * behind a tap, and a static import made every host pay for it up front: the\n * design-system `Drawer` and, through it, MUI's `SwipeableDrawer`, `Modal`,\n * `Slide` and the focus trap, plus the row, the empty state and the pager. On a\n * storefront that is a slide-over most visits never open, parsed before the\n * first screen can render.\n *\n * ## Why the gate is \"ever opened\" rather than `open`\n *\n * `lazy` fetches when a component first RENDERS, so a boundary that still\n * rendered the panel while closed would fetch immediately and buy nothing. This\n * renders `null` until the panel has been open once, which is what actually\n * defers the download to the tap.\n *\n * And once opened it STAYS mounted. Unmounting on close would throw away the\n * drawer's transition state, so the panel would vanish instead of sliding out,\n * and the entrance animation would re-run on every reopen — which someone\n * working through an inbox does repeatedly. The fetch happens once.\n *\n * The initial state reads `open` rather than starting at `false`, so a host that\n * mounts the panel already open renders it in the same commit instead of a frame\n * later.\n *\n * ## Why `null` for the fallback\n *\n * The only frame this can show anything is the one right after the tap, where a\n * spinner reads as a stall rather than as progress. The chunk is small and\n * same-origin.\n */\nimport { Suspense, lazy, useEffect, useState, type ComponentType, type JSX } from 'react';\n\nimport type { NotificationMessages } from '../messages';\n\nimport type { InboxStore } from './inbox-state';\nimport type { LiveActivitiesConfig } from './live-config';\nimport type { LiveSeenStore } from './live-seen';\nimport type { NotificationsPanelProps } from './panel';\n\n/** What the factory binds into the panel, and the host never passes. */\ninterface PanelParts {\n store: InboxStore;\n messages: NotificationMessages;\n /** Absent unless the host turned live activities on — see `./live-config`. */\n live?: LiveActivitiesConfig;\n /** Travels with `live`: where the panel records what the reader has seen. */\n liveSeen?: LiveSeenStore;\n}\n\nexport function lazyNotificationsPanel(\n parts: PanelParts,\n): ComponentType<NotificationsPanelProps> {\n const Bound = lazy(async () => {\n const { NotificationsPanel } = await import('./panel');\n return {\n default: (props: NotificationsPanelProps): JSX.Element => (\n <NotificationsPanel {...props} {...parts} />\n ),\n };\n });\n\n return function NotificationsPanelSlot(props: NotificationsPanelProps): JSX.Element | null {\n const [everOpened, setEverOpened] = useState(props.open);\n\n useEffect(() => {\n if (props.open) setEverOpened(true);\n }, [props.open]);\n\n if (!everOpened) return null;\n\n return (\n <Suspense fallback={null}>\n <Bound {...props} />\n </Suspense>\n );\n };\n}\n","/**\n * The routed preferences screen, fetched when a host actually routes to it.\n *\n * `createWebNotifications` returns two different KINDS of thing, and its own\n * docstring says so: `page` is \"the standalone surface … the one thing a host\n * routes to\", while the bell and the panel \"are a PAIR a host drops into its own\n * chrome\". Chrome is on screen from the first paint; a routed surface is not.\n *\n * A static import made that distinction invisible to a bundler. Every host that\n * put the bell in its header also shipped the preferences matrix — its channel\n * toggles, the per-browser push enable step, and the design-system `Switch`\n * behind them — in the same chunk as the header. A storefront paid for a\n * settings screen a shopper never opens, before its first screen could render;\n * a host that renders its OWN preferences page paid for this one twice.\n *\n * So `page` now loads on demand. Nothing else moves: the bell, the panel and\n * `BellWithPanel` stay exactly as eager as the chrome they belong to, because\n * that is what they are.\n *\n * NO PREFETCH, deliberately, and this is the opposite call from a surface a\n * host opens from chrome it already has. A routed surface is reached by\n * NAVIGATION, and every host here already code-splits its routes — so the\n * fetch happens while the route is being entered, which is the moment a\n * prefetch would have been trying to anticipate. Warming it at factory time\n * would put the screen back on the boot path of every app, which is the whole\n * cost this removes.\n */\nimport { Suspense, lazy, type ComponentType, type JSX } from 'react';\n\nimport type { NotificationMessages } from '../messages';\n\nimport type { NotificationsApiClient } from './api';\nimport type { PreferencesScreenProps } from './preferences-screen';\nimport type { WebPushSetupConfig } from './web-push-setup';\n\n/** What the factory binds into the screen, and the host never passes. */\ninterface PreferencesPageParts {\n api: NotificationsApiClient;\n messages: NotificationMessages;\n webPush: WebPushSetupConfig;\n}\n\n/**\n * The routed screen, bound and loaded on first render.\n *\n * `lazy` memoises its factory, so the binding below happens once however many\n * times a host mounts the page — the same guarantee the direct call gave.\n *\n * The fallback is `null` because a host routes to this: whatever it renders\n * around the route is already on screen, and a second spinner inside it would\n * be one more thing appearing and disappearing during a navigation the host is\n * already indicating.\n */\nexport function lazyPreferencesPage(\n parts: PreferencesPageParts,\n): ComponentType<PreferencesScreenProps> {\n const Bound = lazy(async () => {\n const { PreferencesScreen } = await import('./preferences-screen');\n return {\n default: (props: PreferencesScreenProps): JSX.Element => (\n <PreferencesScreen {...props} {...parts} />\n ),\n };\n });\n\n return function NotificationsPreferencesPage(props: PreferencesScreenProps): JSX.Element {\n return (\n <Suspense fallback={null}>\n <Bound {...props} />\n </Suspense>\n );\n };\n}\n"],"mappings":"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","names":["useState","jsx","Suspense","lazy","jsx","lazy","Suspense","jsx","useState"]}
@@ -144,6 +144,7 @@ var NOTHING_TO_SHOW = {
144
144
  function useBadgeState(store, options = {}) {
145
145
  const enabled = options.enabled ?? true;
146
146
  const subscribe = options.subscribe;
147
+ const relaxed = subscribe !== void 0 || options.live === true;
147
148
  const live = useInboxState(store);
148
149
  const state = enabled ? live : NOTHING_TO_SHOW;
149
150
  options.useSignal?.(() => {
@@ -155,7 +156,7 @@ function useBadgeState(store, options = {}) {
155
156
  const unsubscribe = subscribe?.(() => store.invalidate());
156
157
  const interval = setInterval(
157
158
  () => store.refreshBadge(),
158
- subscribe ? BADGE_RECONCILE_MS : BADGE_POLL_MS
159
+ relaxed ? BADGE_RECONCILE_MS : BADGE_POLL_MS
159
160
  );
160
161
  const onFocus = /* @__PURE__ */ __name(() => store.refreshBadge(), "onFocus");
161
162
  globalThis.addEventListener?.("focus", onFocus);
@@ -164,7 +165,7 @@ function useBadgeState(store, options = {}) {
164
165
  globalThis.removeEventListener?.("focus", onFocus);
165
166
  unsubscribe?.();
166
167
  };
167
- }, [store, enabled, subscribe]);
168
+ }, [store, enabled, subscribe, relaxed]);
168
169
  return state;
169
170
  }
170
171
  __name(useBadgeState, "useBadgeState");
@@ -224,4 +225,4 @@ export {
224
225
  useInboxList,
225
226
  BellIcon
226
227
  };
227
- //# sourceMappingURL=chunk-2IAHFIXS.js.map
228
+ //# sourceMappingURL=chunk-AN6IX4AE.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/react/inbox-state.ts","../src/react/hooks.ts","../src/react/bell-icon.tsx"],"sourcesContent":["import type { InboxNotification } from '../wire';\n\nimport type { NotificationsApiClient } from './api';\n\n/**\n * The inbox's client state, as ONE store shared by the bell and the panel.\n *\n * They have to share it: marking a row read in the panel must move the badge in\n * the same tick, and an arrival must add a row to the list AND to the count.\n * the origin got that for free from a react-query cache the host had already\n * mounted; a published package cannot assume one — a query client is a host\n * decision, and requiring a particular one (or a particular version of one) is\n * the kind of dependency that keeps a package out of a host that made the other\n * choice. So the sharing is explicit and dependency-free: one subscribable\n * store, read through `useSyncExternalStore`.\n *\n * Optimistic on every write, with invalidate-on-error: the badge and the list\n * update instantly, and a failed write refetches the server truth rather than\n * leaving the screen asserting something the database does not say.\n */\n\nexport const PAGE_SIZE = 20;\n\n/** The badge's poll while nothing is pushing to us. */\nexport const BADGE_POLL_MS = 60_000;\n\n/**\n * The badge's interval while a realtime connection is live.\n *\n * Five minutes, not \"never\": this is the reconcile that catches an event the bus\n * dropped, and it costs one COUNT per open tab per five minutes. Deliberately\n * far slower than an operational screen's — a bell badge is ambient, and the\n * arrival that matters is pushed within milliseconds anyway. The poll does NOT\n * stop, which is the standing contract: a dropped event must cost latency and\n * never correctness.\n */\nexport const BADGE_RECONCILE_MS = 300_000;\n\nexport type InboxListStatus = 'idle' | 'pending' | 'ready' | 'error';\n\nexport interface InboxState {\n unread: number;\n items: InboxNotification[];\n status: InboxListStatus;\n /** A cursor means there is another page. */\n nextCursor: string | null;\n loadingMore: boolean;\n}\n\nexport interface InboxStore {\n getState(): InboxState;\n subscribe(listener: () => void): () => void;\n /** Load the first page (idempotent while one is in flight). */\n open(): void;\n /** Refetch the badge count. */\n refreshBadge(): void;\n /** Refetch both — what a realtime hint or a failed write triggers. */\n invalidate(): void;\n loadMore(): void;\n markRead(ids: readonly string[]): void;\n markAllRead(): void;\n remove(id: string): void;\n}\n\nconst EMPTY: InboxState = {\n unread: 0,\n items: [],\n status: 'idle',\n nextCursor: null,\n loadingMore: false,\n};\n\n/** The mutable cell the functions below share, so each one stays small. */\ninterface Cell {\n state: InboxState;\n listeners: Set<() => void>;\n /** Fences a stale reload: a newer one must always win. */\n request: number;\n}\n\nfunction patch(cell: Cell, next: Partial<InboxState>): void {\n cell.state = { ...cell.state, ...next };\n for (const listener of cell.listeners) listener();\n}\n\n/** Refetch the badge count. The number is always one the server just gave us. */\nfunction refreshBadge(cell: Cell, api: NotificationsApiClient): void {\n void api\n .unreadCount()\n .then((unread) => patch(cell, { unread }))\n .catch(() => undefined);\n}\n\n/**\n * Reload page one, discarding whatever the optimistic path had produced.\n * `request` fences it: a reload that started before a newer one must not land\n * after it and reinstate stale rows.\n */\nfunction reloadList(cell: Cell, api: NotificationsApiClient): void {\n const token = (cell.request += 1);\n patch(cell, { status: cell.state.items.length > 0 ? cell.state.status : 'pending' });\n void api\n .listNotifications({ limit: PAGE_SIZE })\n .then((page) => {\n if (token !== cell.request) return;\n patch(cell, { items: page.items, nextCursor: page.nextCursor, status: 'ready' });\n })\n .catch(() => {\n if (token !== cell.request) return;\n patch(cell, { status: 'error' });\n });\n}\n\nfunction invalidate(cell: Cell, api: NotificationsApiClient): void {\n refreshBadge(cell, api);\n if (cell.state.status !== 'idle') reloadList(cell, api);\n}\n\n/** Apply an optimistic edit; on failure, take the server's word instead. */\nfunction write(\n cell: Cell,\n api: NotificationsApiClient,\n apply: () => void,\n send: () => Promise<{ ok: boolean }>,\n): void {\n apply();\n void send()\n .then((result) => {\n if (!result.ok) invalidate(cell, api);\n })\n .catch(() => invalidate(cell, api));\n}\n\nfunction bumpUnread(cell: Cell, delta: number): void {\n patch(cell, { unread: Math.max(0, cell.state.unread + delta) });\n}\n\nfunction loadMore(cell: Cell, api: NotificationsApiClient): void {\n const cursor = cell.state.nextCursor;\n if (!cursor || cell.state.loadingMore) return;\n patch(cell, { loadingMore: true });\n void api\n .listNotifications({ cursor, limit: PAGE_SIZE })\n .then((page) => {\n patch(cell, {\n items: [...cell.state.items, ...page.items],\n nextCursor: page.nextCursor,\n loadingMore: false,\n });\n })\n .catch(() => patch(cell, { loadingMore: false }));\n}\n\nfunction markRead(cell: Cell, api: NotificationsApiClient, ids: readonly string[]): void {\n const readAt = new Date().toISOString();\n let flipped = 0;\n const items = cell.state.items.map((item) => {\n if (!ids.includes(item.id) || item.readAt !== null) return item;\n flipped += 1;\n return { ...item, readAt };\n });\n if (flipped === 0) return;\n write(\n cell,\n api,\n () => {\n patch(cell, { items });\n bumpUnread(cell, -flipped);\n },\n () => api.markRead(ids),\n );\n}\n\nfunction remove(cell: Cell, api: NotificationsApiClient, id: string): void {\n const target = cell.state.items.find((item) => item.id === id);\n if (!target) return;\n const items = cell.state.items.filter((item) => item.id !== id);\n write(\n cell,\n api,\n () => {\n patch(cell, { items });\n if (target.readAt === null) bumpUnread(cell, -1);\n },\n () => api.remove([id]),\n );\n}\n\nexport function createInboxStore(api: NotificationsApiClient): InboxStore {\n const cell: Cell = { state: EMPTY, listeners: new Set(), request: 0 };\n return {\n getState: () => cell.state,\n subscribe(listener) {\n cell.listeners.add(listener);\n return () => cell.listeners.delete(listener);\n },\n open() {\n if (cell.state.status === 'idle') reloadList(cell, api);\n },\n refreshBadge: () => refreshBadge(cell, api),\n invalidate: () => invalidate(cell, api),\n loadMore: () => loadMore(cell, api),\n markRead: (ids) => markRead(cell, api, ids),\n markAllRead() {\n const readAt = new Date().toISOString();\n const items = cell.state.items.map((item) => ({ ...item, readAt: item.readAt ?? readAt }));\n write(\n cell,\n api,\n () => patch(cell, { items, unread: 0 }),\n () => api.markAllRead(),\n );\n },\n remove: (id) => remove(cell, api, id),\n };\n}\n","import { useEffect, useSyncExternalStore } from 'react';\n\nimport {\n BADGE_POLL_MS,\n BADGE_RECONCILE_MS,\n type InboxState,\n type InboxStore,\n} from './inbox-state';\n\n/**\n * The two hooks the bell and the panel use, and the realtime seam between them.\n *\n * A host that has a message bus passes `subscribe`; one that has not passes\n * nothing and keeps the 60 s poll. The bell ships in this package and mounts in\n * whatever embeds it, so it must not require the host to have adopted anything.\n */\n\n/**\n * How the surface learns an inbox changed without asking.\n *\n * Called once per mounted bell with a callback that means only \"ask again\" — no\n * payload, so the number on screen is always one the server just gave us.\n * Returns its own teardown. A host wires this to whatever it already has.\n */\nexport type NotificationsSubscribe = (onHint: () => void) => () => void;\n\n/**\n * The same wiring, as a HOOK — for a host whose realtime connection lives in\n * React context rather than in a module.\n *\n * `subscribe` above is supplied at FACTORY time, which is module scope, and a\n * context-bound connection cannot be reached from there: the provider holding\n * it is inside the tree. A host in that shape (a `<UserRealtimeProvider>` and a\n * `useUserTopics` hook, which is the common one) had no way to pass anything at\n * all, and the badge simply never heard an event.\n *\n * So this is the second door, and it is the one `@12-apps/app-shell` already\n * uses for the same problem — its consent dialog takes a `useSignal` hook for\n * exactly this reason. Two packages solving one problem two ways is how an\n * adopter ends up believing the feature is unavailable to it.\n *\n * Called during render, so it may use context and hooks freely. Pass one or\n * the other; passing both runs both, which is a host's business.\n */\nexport type NotificationsSignalHook = (onHint: () => void) => void;\n\nexport function useInboxState(store: InboxStore): InboxState {\n return useSyncExternalStore(store.subscribe, store.getState, store.getState);\n}\n\n/** What both badge hooks below take, and what the bell passes them. */\nexport interface BadgeSyncOptions {\n enabled?: boolean;\n subscribe?: NotificationsSubscribe;\n useSignal?: NotificationsSignalHook;\n /**\n * The host's own word that its realtime channel is up RIGHT NOW.\n *\n * `subscribe` relaxes the poll because a live subscription is implied by its\n * presence. `useSignal` cannot say the same — a hook handed a callback knows\n * nothing about whether its connection opened — and neither can a host that\n * wires the invalidate itself, outside this package, because its connection\n * is session-scoped and must stay behind `enabled` (which `useSignal` is\n * called in front of). Such a host had no way to relax the badge at all, and\n * every signed-in reader polled once a minute on top of a working stream.\n *\n * `true` relaxes the poll to {@link BADGE_RECONCILE_MS}, exactly as a\n * `subscribe` does; anything else keeps {@link BADGE_POLL_MS}. It is a STATUS,\n * so pass it live — a stream that drops takes the badge straight back to the\n * fast poll on the next render.\n */\n live?: boolean;\n}\n\n/**\n * What a disabled badge reads, instead of whatever the store happens to hold.\n *\n * A CONSTANT, so `useSyncExternalStore`'s identity comparison sees no change\n * across the renders of a signed-out session.\n */\nconst NOTHING_TO_SHOW: InboxState = {\n unread: 0,\n items: [],\n status: 'idle',\n nextCursor: null,\n loadingMore: false,\n};\n\n/**\n * The badge's server state, kept fresh: pushed while a subscription is live,\n * polled otherwise.\n *\n * The whole state rather than the count, because every badge hook that layers\n * on top of it needs the poll and the subscription mounted exactly ONCE per\n * bell — read through two hooks, a bell that showed both a number and a tone\n * would open two of everything.\n *\n * ## `enabled` gates the ANSWER, not only the fetching\n *\n * It gates the poll and the subscription, which is the obvious half. It also\n * blanks the returned state, which is the half that was missing and matters\n * more: the store is per FACTORY and a host builds one at module scope for the\n * whole app, so signing out does not empty it — `refreshBadge` swallows the 401\n * and leaves the last number in place. Without this, a hook told there is\n * nobody signed in hands back the PREVIOUS reader's unread count and their\n * inbox rows.\n *\n * Deliberately here rather than at each caller. It was at each caller, three\n * times, in three shapes, and two of them were dead weight no test could reach\n * — which is what an invariant looks like just before one copy of it goes\n * missing.\n *\n * INTERNAL. Not exported from `./index`: it hands back rows as well as a count,\n * and a host wanting a number has `useUnreadCount` or the factory's\n * `useBellBadge`.\n */\nexport function useBadgeState(store: InboxStore, options: BadgeSyncOptions = {}): InboxState {\n const enabled = options.enabled ?? true;\n const subscribe = options.subscribe;\n // Pushed to by a subscription this package holds, or by one the host holds\n // and says is up — see `BadgeSyncOptions.live`.\n const relaxed = subscribe !== undefined || options.live === true;\n const live = useInboxState(store);\n const state = enabled ? live : NOTHING_TO_SHOW;\n\n // Called unconditionally — it is a hook, so it cannot sit behind `enabled`.\n // The host's own hook decides what to do when there is nothing to hear.\n options.useSignal?.(() => {\n if (enabled) store.invalidate();\n });\n\n useEffect(() => {\n if (!enabled) return;\n store.refreshBadge();\n const unsubscribe = subscribe?.(() => store.invalidate());\n // A live channel relaxes the poll to the reconcile interval; without one it\n // stays the 60 s poll.\n const interval = setInterval(\n () => store.refreshBadge(),\n relaxed ? BADGE_RECONCILE_MS : BADGE_POLL_MS,\n );\n const onFocus = (): void => store.refreshBadge();\n globalThis.addEventListener?.('focus', onFocus);\n return () => {\n clearInterval(interval);\n globalThis.removeEventListener?.('focus', onFocus);\n unsubscribe?.();\n };\n }, [store, enabled, subscribe, relaxed]);\n\n return state;\n}\n\n/**\n * The bell badge number, for a host with its own trigger chrome.\n *\n * A host that also publishes live activities wants `useBellBadge` from the\n * factory instead — this one counts inbox rows and knows nothing about what is\n * happening right now.\n */\nexport function useUnreadCount(store: InboxStore, options: BadgeSyncOptions = {}): number {\n return useBadgeState(store, options).unread;\n}\n\n/** The panel's list — only fetches while the panel is open. */\nexport function useInboxList(store: InboxStore, open: boolean): InboxState {\n const state = useInboxState(store);\n useEffect(() => {\n if (open) store.open();\n }, [store, open]);\n return state;\n}\n","/** Inline SVG bell (no icon-library dependency in this package). */\nimport type { JSX } from 'react';\n\nimport { Box } from '@12-apps/ui/mui/Box';\n\nexport function BellIcon({\n size = 28,\n dim = false,\n}: {\n size?: number;\n dim?: boolean;\n}): JSX.Element {\n return (\n <Box\n component=\"svg\"\n viewBox=\"0 0 24 24\"\n aria-hidden\n sx={{\n width: size,\n height: size,\n fill: 'none',\n stroke: 'currentColor',\n opacity: dim ? 0.4 : 1,\n }}\n strokeWidth={1.8}\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n >\n <path d=\"M6 9a6 6 0 0 1 12 0c0 5 2 6 2 6H4s2-1 2-6\" />\n <path d=\"M10 20a2 2 0 0 0 4 0\" />\n </Box>\n );\n}\n"],"mappings":";;;;;AAqBO,IAAM,YAAY;AAGlB,IAAM,gBAAgB;AAYtB,IAAM,qBAAqB;AA4BlC,IAAM,QAAoB;AAAA,EACxB,QAAQ;AAAA,EACR,OAAO,CAAC;AAAA,EACR,QAAQ;AAAA,EACR,YAAY;AAAA,EACZ,aAAa;AACf;AAUA,SAAS,MAAM,MAAY,MAAiC;AAC1D,OAAK,QAAQ,EAAE,GAAG,KAAK,OAAO,GAAG,KAAK;AACtC,aAAW,YAAY,KAAK,UAAW,UAAS;AAClD;AAHS;AAMT,SAAS,aAAa,MAAY,KAAmC;AACnE,OAAK,IACF,YAAY,EACZ,KAAK,CAAC,WAAW,MAAM,MAAM,EAAE,OAAO,CAAC,CAAC,EACxC,MAAM,MAAM,MAAS;AAC1B;AALS;AAYT,SAAS,WAAW,MAAY,KAAmC;AACjE,QAAM,QAAS,KAAK,WAAW;AAC/B,QAAM,MAAM,EAAE,QAAQ,KAAK,MAAM,MAAM,SAAS,IAAI,KAAK,MAAM,SAAS,UAAU,CAAC;AACnF,OAAK,IACF,kBAAkB,EAAE,OAAO,UAAU,CAAC,EACtC,KAAK,CAAC,SAAS;AACd,QAAI,UAAU,KAAK,QAAS;AAC5B,UAAM,MAAM,EAAE,OAAO,KAAK,OAAO,YAAY,KAAK,YAAY,QAAQ,QAAQ,CAAC;AAAA,EACjF,CAAC,EACA,MAAM,MAAM;AACX,QAAI,UAAU,KAAK,QAAS;AAC5B,UAAM,MAAM,EAAE,QAAQ,QAAQ,CAAC;AAAA,EACjC,CAAC;AACL;AAbS;AAeT,SAAS,WAAW,MAAY,KAAmC;AACjE,eAAa,MAAM,GAAG;AACtB,MAAI,KAAK,MAAM,WAAW,OAAQ,YAAW,MAAM,GAAG;AACxD;AAHS;AAMT,SAAS,MACP,MACA,KACA,OACA,MACM;AACN,QAAM;AACN,OAAK,KAAK,EACP,KAAK,CAAC,WAAW;AAChB,QAAI,CAAC,OAAO,GAAI,YAAW,MAAM,GAAG;AAAA,EACtC,CAAC,EACA,MAAM,MAAM,WAAW,MAAM,GAAG,CAAC;AACtC;AAZS;AAcT,SAAS,WAAW,MAAY,OAAqB;AACnD,QAAM,MAAM,EAAE,QAAQ,KAAK,IAAI,GAAG,KAAK,MAAM,SAAS,KAAK,EAAE,CAAC;AAChE;AAFS;AAIT,SAAS,SAAS,MAAY,KAAmC;AAC/D,QAAM,SAAS,KAAK,MAAM;AAC1B,MAAI,CAAC,UAAU,KAAK,MAAM,YAAa;AACvC,QAAM,MAAM,EAAE,aAAa,KAAK,CAAC;AACjC,OAAK,IACF,kBAAkB,EAAE,QAAQ,OAAO,UAAU,CAAC,EAC9C,KAAK,CAAC,SAAS;AACd,UAAM,MAAM;AAAA,MACV,OAAO,CAAC,GAAG,KAAK,MAAM,OAAO,GAAG,KAAK,KAAK;AAAA,MAC1C,YAAY,KAAK;AAAA,MACjB,aAAa;AAAA,IACf,CAAC;AAAA,EACH,CAAC,EACA,MAAM,MAAM,MAAM,MAAM,EAAE,aAAa,MAAM,CAAC,CAAC;AACpD;AAdS;AAgBT,SAAS,SAAS,MAAY,KAA6B,KAA8B;AACvF,QAAM,UAAS,oBAAI,KAAK,GAAE,YAAY;AACtC,MAAI,UAAU;AACd,QAAM,QAAQ,KAAK,MAAM,MAAM,IAAI,CAAC,SAAS;AAC3C,QAAI,CAAC,IAAI,SAAS,KAAK,EAAE,KAAK,KAAK,WAAW,KAAM,QAAO;AAC3D,eAAW;AACX,WAAO,EAAE,GAAG,MAAM,OAAO;AAAA,EAC3B,CAAC;AACD,MAAI,YAAY,EAAG;AACnB;AAAA,IACE;AAAA,IACA;AAAA,IACA,MAAM;AACJ,YAAM,MAAM,EAAE,MAAM,CAAC;AACrB,iBAAW,MAAM,CAAC,OAAO;AAAA,IAC3B;AAAA,IACA,MAAM,IAAI,SAAS,GAAG;AAAA,EACxB;AACF;AAlBS;AAoBT,SAAS,OAAO,MAAY,KAA6B,IAAkB;AACzE,QAAM,SAAS,KAAK,MAAM,MAAM,KAAK,CAAC,SAAS,KAAK,OAAO,EAAE;AAC7D,MAAI,CAAC,OAAQ;AACb,QAAM,QAAQ,KAAK,MAAM,MAAM,OAAO,CAAC,SAAS,KAAK,OAAO,EAAE;AAC9D;AAAA,IACE;AAAA,IACA;AAAA,IACA,MAAM;AACJ,YAAM,MAAM,EAAE,MAAM,CAAC;AACrB,UAAI,OAAO,WAAW,KAAM,YAAW,MAAM,EAAE;AAAA,IACjD;AAAA,IACA,MAAM,IAAI,OAAO,CAAC,EAAE,CAAC;AAAA,EACvB;AACF;AAbS;AAeF,SAAS,iBAAiB,KAAyC;AACxE,QAAM,OAAa,EAAE,OAAO,OAAO,WAAW,oBAAI,IAAI,GAAG,SAAS,EAAE;AACpE,SAAO;AAAA,IACL,UAAU,6BAAM,KAAK,OAAX;AAAA,IACV,UAAU,UAAU;AAClB,WAAK,UAAU,IAAI,QAAQ;AAC3B,aAAO,MAAM,KAAK,UAAU,OAAO,QAAQ;AAAA,IAC7C;AAAA,IACA,OAAO;AACL,UAAI,KAAK,MAAM,WAAW,OAAQ,YAAW,MAAM,GAAG;AAAA,IACxD;AAAA,IACA,cAAc,6BAAM,aAAa,MAAM,GAAG,GAA5B;AAAA,IACd,YAAY,6BAAM,WAAW,MAAM,GAAG,GAA1B;AAAA,IACZ,UAAU,6BAAM,SAAS,MAAM,GAAG,GAAxB;AAAA,IACV,UAAU,wBAAC,QAAQ,SAAS,MAAM,KAAK,GAAG,GAAhC;AAAA,IACV,cAAc;AACZ,YAAM,UAAS,oBAAI,KAAK,GAAE,YAAY;AACtC,YAAM,QAAQ,KAAK,MAAM,MAAM,IAAI,CAAC,UAAU,EAAE,GAAG,MAAM,QAAQ,KAAK,UAAU,OAAO,EAAE;AACzF;AAAA,QACE;AAAA,QACA;AAAA,QACA,MAAM,MAAM,MAAM,EAAE,OAAO,QAAQ,EAAE,CAAC;AAAA,QACtC,MAAM,IAAI,YAAY;AAAA,MACxB;AAAA,IACF;AAAA,IACA,QAAQ,wBAAC,OAAO,OAAO,MAAM,KAAK,EAAE,GAA5B;AAAA,EACV;AACF;AA3BgB;;;AC5LhB,SAAS,WAAW,4BAA4B;AA8CzC,SAAS,cAAc,OAA+B;AAC3D,SAAO,qBAAqB,MAAM,WAAW,MAAM,UAAU,MAAM,QAAQ;AAC7E;AAFgB;AAkChB,IAAM,kBAA8B;AAAA,EAClC,QAAQ;AAAA,EACR,OAAO,CAAC;AAAA,EACR,QAAQ;AAAA,EACR,YAAY;AAAA,EACZ,aAAa;AACf;AA8BO,SAAS,cAAc,OAAmB,UAA4B,CAAC,GAAe;AAC3F,QAAM,UAAU,QAAQ,WAAW;AACnC,QAAM,YAAY,QAAQ;AAG1B,QAAM,UAAU,cAAc,UAAa,QAAQ,SAAS;AAC5D,QAAM,OAAO,cAAc,KAAK;AAChC,QAAM,QAAQ,UAAU,OAAO;AAI/B,UAAQ,YAAY,MAAM;AACxB,QAAI,QAAS,OAAM,WAAW;AAAA,EAChC,CAAC;AAED,YAAU,MAAM;AACd,QAAI,CAAC,QAAS;AACd,UAAM,aAAa;AACnB,UAAM,cAAc,YAAY,MAAM,MAAM,WAAW,CAAC;AAGxD,UAAM,WAAW;AAAA,MACf,MAAM,MAAM,aAAa;AAAA,MACzB,UAAU,qBAAqB;AAAA,IACjC;AACA,UAAM,UAAU,6BAAY,MAAM,aAAa,GAA/B;AAChB,eAAW,mBAAmB,SAAS,OAAO;AAC9C,WAAO,MAAM;AACX,oBAAc,QAAQ;AACtB,iBAAW,sBAAsB,SAAS,OAAO;AACjD,oBAAc;AAAA,IAChB;AAAA,EACF,GAAG,CAAC,OAAO,SAAS,WAAW,OAAO,CAAC;AAEvC,SAAO;AACT;AAnCgB;AA4CT,SAAS,eAAe,OAAmB,UAA4B,CAAC,GAAW;AACxF,SAAO,cAAc,OAAO,OAAO,EAAE;AACvC;AAFgB;AAKT,SAAS,aAAa,OAAmB,MAA2B;AACzE,QAAM,QAAQ,cAAc,KAAK;AACjC,YAAU,MAAM;AACd,QAAI,KAAM,OAAM,KAAK;AAAA,EACvB,GAAG,CAAC,OAAO,IAAI,CAAC;AAChB,SAAO;AACT;AANgB;;;AClKhB,SAAS,WAAW;AAUhB,SAeE,KAfF;AARG,SAAS,SAAS;AAAA,EACvB,OAAO;AAAA,EACP,MAAM;AACR,GAGgB;AACd,SACE;AAAA,IAAC;AAAA;AAAA,MACC,WAAU;AAAA,MACV,SAAQ;AAAA,MACR,eAAW;AAAA,MACX,IAAI;AAAA,QACF,OAAO;AAAA,QACP,QAAQ;AAAA,QACR,MAAM;AAAA,QACN,QAAQ;AAAA,QACR,SAAS,MAAM,MAAM;AAAA,MACvB;AAAA,MACA,aAAa;AAAA,MACb,eAAc;AAAA,MACd,gBAAe;AAAA,MAEf;AAAA,4BAAC,UAAK,GAAE,6CAA4C;AAAA,QACpD,oBAAC,UAAK,GAAE,wBAAuB;AAAA;AAAA;AAAA,EACjC;AAEJ;AA3BgB;","names":[]}
@@ -201,6 +201,23 @@ interface BadgeSyncOptions {
201
201
  enabled?: boolean;
202
202
  subscribe?: NotificationsSubscribe;
203
203
  useSignal?: NotificationsSignalHook;
204
+ /**
205
+ * The host's own word that its realtime channel is up RIGHT NOW.
206
+ *
207
+ * `subscribe` relaxes the poll because a live subscription is implied by its
208
+ * presence. `useSignal` cannot say the same — a hook handed a callback knows
209
+ * nothing about whether its connection opened — and neither can a host that
210
+ * wires the invalidate itself, outside this package, because its connection
211
+ * is session-scoped and must stay behind `enabled` (which `useSignal` is
212
+ * called in front of). Such a host had no way to relax the badge at all, and
213
+ * every signed-in reader polled once a minute on top of a working stream.
214
+ *
215
+ * `true` relaxes the poll to {@link BADGE_RECONCILE_MS}, exactly as a
216
+ * `subscribe` does; anything else keeps {@link BADGE_POLL_MS}. It is a STATUS,
217
+ * so pass it live — a stream that drops takes the badge straight back to the
218
+ * fast poll on the next render.
219
+ */
220
+ live?: boolean;
204
221
  }
205
222
  /**
206
223
  * The bell badge number, for a host with its own trigger chrome.
@@ -464,9 +481,7 @@ interface WebNotifications {
464
481
  * right now, and `useBellBadge` below is the door. Still the right hook for
465
482
  * anything that genuinely wants "how many unread rows".
466
483
  */
467
- useUnreadCount: (options?: {
468
- enabled?: boolean;
469
- }) => number;
484
+ useUnreadCount: (options?: BadgeHookOptions) => number;
470
485
  /**
471
486
  * The badge's NUMBER AND TONE, for a host with its own trigger chrome.
472
487
  *
@@ -482,9 +497,7 @@ interface WebNotifications {
482
497
  * hook that bell uses. Without live activities configured it is
483
498
  * `useUnreadCount` plus `hasNew: count > 0`.
484
499
  */
485
- useBellBadge: (options?: {
486
- enabled?: boolean;
487
- }) => BellBadge;
500
+ useBellBadge: (options?: BadgeHookOptions) => BellBadge;
488
501
  /** The shared client state, for host glue. */
489
502
  store: InboxStore;
490
503
  /** The bound wire client. */
@@ -492,6 +505,15 @@ interface WebNotifications {
492
505
  /** The copy in force, so a host's own chrome can reuse a sentence. */
493
506
  messages: NotificationMessages;
494
507
  }
508
+ /**
509
+ * What a host passes either badge hook: whether to run at all, and — for a host
510
+ * that holds its own realtime connection — whether that connection is up
511
+ * (`BadgeSyncOptions.live`).
512
+ */
513
+ interface BadgeHookOptions {
514
+ enabled?: boolean;
515
+ live?: boolean;
516
+ }
495
517
  declare function createWebNotifications(config: NotificationsWebConfig): WebNotifications;
496
518
 
497
- export { useUnreadCount as A, BADGE_POLL_MS as B, type InboxListStatus as I, type LiveActivitiesConfig as L, type NotificationsApiClient as N, PAGE_SIZE as P, type WebNotifications as W, BADGE_RECONCILE_MS as a, type BadgeSyncOptions as b, createWebNotifications as c, type BellBadge as d, type BellButtonProps as e, type InboxState as f, type InboxStore as g, type LiveActivitiesHook as h, type LiveActivityMessages as i, NotificationsHttpError as j, type NotificationsPanelProps as k, type NotificationsResult as l, type NotificationsSignalHook as m, type NotificationsSubscribe as n, type NotificationsTransport as o, type NotificationsWebConfig as p, type PreferencesPayload as q, type PreferencesScreenProps as r, type PushRegistrationPayload as s, type WebPushPlatformHint as t, type WebPushSetupConfig as u, createInboxStore as v, createNotificationsApiClient as w, httpNotificationsTransport as x, useInboxList as y, useInboxState as z };
519
+ export { useInboxState as A, BADGE_POLL_MS as B, useUnreadCount as C, type InboxListStatus as I, type LiveActivitiesConfig as L, type NotificationsApiClient as N, PAGE_SIZE as P, type WebNotifications as W, BADGE_RECONCILE_MS as a, type BadgeHookOptions as b, createWebNotifications as c, type BadgeSyncOptions as d, type BellBadge as e, type BellButtonProps as f, type InboxState as g, type InboxStore as h, type LiveActivitiesHook as i, type LiveActivityMessages as j, NotificationsHttpError as k, type NotificationsPanelProps as l, type NotificationsResult as m, type NotificationsSignalHook as n, type NotificationsSubscribe as o, type NotificationsTransport as p, type NotificationsWebConfig as q, type PreferencesPayload as r, type PreferencesScreenProps as s, type PushRegistrationPayload as t, type WebPushPlatformHint as u, type WebPushSetupConfig as v, createInboxStore as w, createNotificationsApiClient as x, httpNotificationsTransport as y, useInboxList as z };
@@ -1,5 +1,5 @@
1
1
  import { c as createEmailPreviewScreen } from '../preview-screen-DYJRAnAY.js';
2
- import { c as createWebNotifications } from '../create-web-notifications-BODiQRK-.js';
2
+ import { c as createWebNotifications } from '../create-web-notifications-32Myo-ww.js';
3
3
  import 'react';
4
4
  import '../wire-CJka1AvM.js';
5
5
  import '../types-DPiePHJD.js';
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  createWebNotifications
3
- } from "../chunk-4DTUD74E.js";
4
- import "../chunk-2IAHFIXS.js";
3
+ } from "../chunk-643HNPQJ.js";
4
+ import "../chunk-AN6IX4AE.js";
5
5
  import {
6
6
  createEmailPreviewScreen
7
7
  } from "../chunk-KEYRE245.js";
@@ -5,7 +5,7 @@ import {
5
5
  import {
6
6
  BellIcon,
7
7
  useInboxList
8
- } from "./chunk-2IAHFIXS.js";
8
+ } from "./chunk-AN6IX4AE.js";
9
9
  import "./chunk-RTURLH5U.js";
10
10
  import {
11
11
  __name
@@ -295,4 +295,4 @@ __name(NotificationsPanel, "NotificationsPanel");
295
295
  export {
296
296
  NotificationsPanel
297
297
  };
298
- //# sourceMappingURL=panel-MKI4PTNZ.js.map
298
+ //# sourceMappingURL=panel-JL5RDEHY.js.map
@@ -1,5 +1,5 @@
1
- import { L as LiveActivitiesConfig, N as NotificationsApiClient } from '../create-web-notifications-BODiQRK-.js';
2
- export { B as BADGE_POLL_MS, a as BADGE_RECONCILE_MS, b as BadgeSyncOptions, d as BellBadge, e as BellButtonProps, I as InboxListStatus, f as InboxState, g as InboxStore, h as LiveActivitiesHook, i as LiveActivityMessages, j as NotificationsHttpError, k as NotificationsPanelProps, l as NotificationsResult, m as NotificationsSignalHook, n as NotificationsSubscribe, o as NotificationsTransport, p as NotificationsWebConfig, P as PAGE_SIZE, q as PreferencesPayload, r as PreferencesScreenProps, s as PushRegistrationPayload, W as WebNotifications, t as WebPushPlatformHint, u as WebPushSetupConfig, v as createInboxStore, w as createNotificationsApiClient, c as createWebNotifications, x as httpNotificationsTransport, y as useInboxList, z as useInboxState, A as useUnreadCount } from '../create-web-notifications-BODiQRK-.js';
1
+ import { L as LiveActivitiesConfig, N as NotificationsApiClient } from '../create-web-notifications-32Myo-ww.js';
2
+ export { B as BADGE_POLL_MS, a as BADGE_RECONCILE_MS, b as BadgeHookOptions, d as BadgeSyncOptions, e as BellBadge, f as BellButtonProps, I as InboxListStatus, g as InboxState, h as InboxStore, i as LiveActivitiesHook, j as LiveActivityMessages, k as NotificationsHttpError, l as NotificationsPanelProps, m as NotificationsResult, n as NotificationsSignalHook, o as NotificationsSubscribe, p as NotificationsTransport, q as NotificationsWebConfig, P as PAGE_SIZE, r as PreferencesPayload, s as PreferencesScreenProps, t as PushRegistrationPayload, W as WebNotifications, u as WebPushPlatformHint, v as WebPushSetupConfig, w as createInboxStore, x as createNotificationsApiClient, c as createWebNotifications, y as httpNotificationsTransport, z as useInboxList, A as useInboxState, C as useUnreadCount } from '../create-web-notifications-32Myo-ww.js';
3
3
  import { JSX, ReactNode } from 'react';
4
4
  import { b as LiveActivity } from '../live-DYxEFO49.js';
5
5
  export { c as LiveActivityLane, d as LiveActivityStep, l as liveActivityLane } from '../live-DYxEFO49.js';
@@ -3,7 +3,7 @@ import {
3
3
  createNotificationsApiClient,
4
4
  createWebNotifications,
5
5
  httpNotificationsTransport
6
- } from "../chunk-4DTUD74E.js";
6
+ } from "../chunk-643HNPQJ.js";
7
7
  import {
8
8
  LiveSection,
9
9
  relativeTime
@@ -17,7 +17,7 @@ import {
17
17
  useInboxList,
18
18
  useInboxState,
19
19
  useUnreadCount
20
- } from "../chunk-2IAHFIXS.js";
20
+ } from "../chunk-AN6IX4AE.js";
21
21
  import {
22
22
  disableWebPush,
23
23
  enableWebPush,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@12-apps/notifications",
3
- "version": "4.13.0",
3
+ "version": "4.14.0",
4
4
  "type": "module",
5
5
  "sideEffects": false,
6
6
  "description": "Plug-and-play notification system (12-15): an always-on in-app inbox, per-user × per-category channel preferences, and email / SMS / WhatsApp / web-push transports behind vendor DRIVERS so a second provider is a config entry. Framework-free core (.), host-mounted backend surface (./server: inbox / preferences / push-subscription endpoints, the channel router with delivery records + retry sweep, the permission fan-out, duck-typed Prisma seam), Hono adapter (./hono), React surface (./react: bell + badge, inbox drawer, preferences screen), VAPID sender (./web-push) and the package-owned Prisma partial + migrations. Standardized adoption contract in ADOPTING.md.",
@@ -71,7 +71,7 @@
71
71
  "prisma:sync:check": "node scripts/sync-notifications-schema.mjs --check"
72
72
  },
73
73
  "dependencies": {
74
- "@12-apps/ui": "^6.27.1"
74
+ "@12-apps/ui": "^6.28.0"
75
75
  },
76
76
  "peerDependencies": {
77
77
  "@12-apps/wiring": ">=1.3.0",
@@ -103,7 +103,7 @@ export interface WebNotifications {
103
103
  * right now, and `useBellBadge` below is the door. Still the right hook for
104
104
  * anything that genuinely wants "how many unread rows".
105
105
  */
106
- useUnreadCount: (options?: { enabled?: boolean }) => number;
106
+ useUnreadCount: (options?: BadgeHookOptions) => number;
107
107
  /**
108
108
  * The badge's NUMBER AND TONE, for a host with its own trigger chrome.
109
109
  *
@@ -119,7 +119,7 @@ export interface WebNotifications {
119
119
  * hook that bell uses. Without live activities configured it is
120
120
  * `useUnreadCount` plus `hasNew: count > 0`.
121
121
  */
122
- useBellBadge: (options?: { enabled?: boolean }) => BellBadge;
122
+ useBellBadge: (options?: BadgeHookOptions) => BellBadge;
123
123
  /** The shared client state, for host glue. */
124
124
  store: InboxStore;
125
125
  /** The bound wire client. */
@@ -128,6 +128,16 @@ export interface WebNotifications {
128
128
  messages: NotificationMessages;
129
129
  }
130
130
 
131
+ /**
132
+ * What a host passes either badge hook: whether to run at all, and — for a host
133
+ * that holds its own realtime connection — whether that connection is up
134
+ * (`BadgeSyncOptions.live`).
135
+ */
136
+ export interface BadgeHookOptions {
137
+ enabled?: boolean;
138
+ live?: boolean;
139
+ }
140
+
131
141
  /** What the factory passes both badge hooks: whatever realtime wiring it has. */
132
142
  type SubscribeOption = {
133
143
  subscribe?: NotificationsSubscribe;
@@ -53,6 +53,23 @@ export interface BadgeSyncOptions {
53
53
  enabled?: boolean;
54
54
  subscribe?: NotificationsSubscribe;
55
55
  useSignal?: NotificationsSignalHook;
56
+ /**
57
+ * The host's own word that its realtime channel is up RIGHT NOW.
58
+ *
59
+ * `subscribe` relaxes the poll because a live subscription is implied by its
60
+ * presence. `useSignal` cannot say the same — a hook handed a callback knows
61
+ * nothing about whether its connection opened — and neither can a host that
62
+ * wires the invalidate itself, outside this package, because its connection
63
+ * is session-scoped and must stay behind `enabled` (which `useSignal` is
64
+ * called in front of). Such a host had no way to relax the badge at all, and
65
+ * every signed-in reader polled once a minute on top of a working stream.
66
+ *
67
+ * `true` relaxes the poll to {@link BADGE_RECONCILE_MS}, exactly as a
68
+ * `subscribe` does; anything else keeps {@link BADGE_POLL_MS}. It is a STATUS,
69
+ * so pass it live — a stream that drops takes the badge straight back to the
70
+ * fast poll on the next render.
71
+ */
72
+ live?: boolean;
56
73
  }
57
74
 
58
75
  /**
@@ -100,6 +117,9 @@ const NOTHING_TO_SHOW: InboxState = {
100
117
  export function useBadgeState(store: InboxStore, options: BadgeSyncOptions = {}): InboxState {
101
118
  const enabled = options.enabled ?? true;
102
119
  const subscribe = options.subscribe;
120
+ // Pushed to by a subscription this package holds, or by one the host holds
121
+ // and says is up — see `BadgeSyncOptions.live`.
122
+ const relaxed = subscribe !== undefined || options.live === true;
103
123
  const live = useInboxState(store);
104
124
  const state = enabled ? live : NOTHING_TO_SHOW;
105
125
 
@@ -113,11 +133,11 @@ export function useBadgeState(store: InboxStore, options: BadgeSyncOptions = {})
113
133
  if (!enabled) return;
114
134
  store.refreshBadge();
115
135
  const unsubscribe = subscribe?.(() => store.invalidate());
116
- // A live subscription relaxes the poll to the reconcile interval; without
117
- // one it stays the 60 s poll.
136
+ // A live channel relaxes the poll to the reconcile interval; without one it
137
+ // stays the 60 s poll.
118
138
  const interval = setInterval(
119
139
  () => store.refreshBadge(),
120
- subscribe ? BADGE_RECONCILE_MS : BADGE_POLL_MS,
140
+ relaxed ? BADGE_RECONCILE_MS : BADGE_POLL_MS,
121
141
  );
122
142
  const onFocus = (): void => store.refreshBadge();
123
143
  globalThis.addEventListener?.('focus', onFocus);
@@ -126,7 +146,7 @@ export function useBadgeState(store: InboxStore, options: BadgeSyncOptions = {})
126
146
  globalThis.removeEventListener?.('focus', onFocus);
127
147
  unsubscribe?.();
128
148
  };
129
- }, [store, enabled, subscribe]);
149
+ }, [store, enabled, subscribe, relaxed]);
130
150
 
131
151
  return state;
132
152
  }
@@ -9,6 +9,7 @@
9
9
 
10
10
  export {
11
11
  createWebNotifications,
12
+ type BadgeHookOptions,
12
13
  type NotificationsWebConfig,
13
14
  type WebNotifications,
14
15
  } from './create-web-notifications';
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/react/inbox-state.ts","../src/react/hooks.ts","../src/react/bell-icon.tsx"],"sourcesContent":["import type { InboxNotification } from '../wire';\n\nimport type { NotificationsApiClient } from './api';\n\n/**\n * The inbox's client state, as ONE store shared by the bell and the panel.\n *\n * They have to share it: marking a row read in the panel must move the badge in\n * the same tick, and an arrival must add a row to the list AND to the count.\n * the origin got that for free from a react-query cache the host had already\n * mounted; a published package cannot assume one — a query client is a host\n * decision, and requiring a particular one (or a particular version of one) is\n * the kind of dependency that keeps a package out of a host that made the other\n * choice. So the sharing is explicit and dependency-free: one subscribable\n * store, read through `useSyncExternalStore`.\n *\n * Optimistic on every write, with invalidate-on-error: the badge and the list\n * update instantly, and a failed write refetches the server truth rather than\n * leaving the screen asserting something the database does not say.\n */\n\nexport const PAGE_SIZE = 20;\n\n/** The badge's poll while nothing is pushing to us. */\nexport const BADGE_POLL_MS = 60_000;\n\n/**\n * The badge's interval while a realtime connection is live.\n *\n * Five minutes, not \"never\": this is the reconcile that catches an event the bus\n * dropped, and it costs one COUNT per open tab per five minutes. Deliberately\n * far slower than an operational screen's — a bell badge is ambient, and the\n * arrival that matters is pushed within milliseconds anyway. The poll does NOT\n * stop, which is the standing contract: a dropped event must cost latency and\n * never correctness.\n */\nexport const BADGE_RECONCILE_MS = 300_000;\n\nexport type InboxListStatus = 'idle' | 'pending' | 'ready' | 'error';\n\nexport interface InboxState {\n unread: number;\n items: InboxNotification[];\n status: InboxListStatus;\n /** A cursor means there is another page. */\n nextCursor: string | null;\n loadingMore: boolean;\n}\n\nexport interface InboxStore {\n getState(): InboxState;\n subscribe(listener: () => void): () => void;\n /** Load the first page (idempotent while one is in flight). */\n open(): void;\n /** Refetch the badge count. */\n refreshBadge(): void;\n /** Refetch both — what a realtime hint or a failed write triggers. */\n invalidate(): void;\n loadMore(): void;\n markRead(ids: readonly string[]): void;\n markAllRead(): void;\n remove(id: string): void;\n}\n\nconst EMPTY: InboxState = {\n unread: 0,\n items: [],\n status: 'idle',\n nextCursor: null,\n loadingMore: false,\n};\n\n/** The mutable cell the functions below share, so each one stays small. */\ninterface Cell {\n state: InboxState;\n listeners: Set<() => void>;\n /** Fences a stale reload: a newer one must always win. */\n request: number;\n}\n\nfunction patch(cell: Cell, next: Partial<InboxState>): void {\n cell.state = { ...cell.state, ...next };\n for (const listener of cell.listeners) listener();\n}\n\n/** Refetch the badge count. The number is always one the server just gave us. */\nfunction refreshBadge(cell: Cell, api: NotificationsApiClient): void {\n void api\n .unreadCount()\n .then((unread) => patch(cell, { unread }))\n .catch(() => undefined);\n}\n\n/**\n * Reload page one, discarding whatever the optimistic path had produced.\n * `request` fences it: a reload that started before a newer one must not land\n * after it and reinstate stale rows.\n */\nfunction reloadList(cell: Cell, api: NotificationsApiClient): void {\n const token = (cell.request += 1);\n patch(cell, { status: cell.state.items.length > 0 ? cell.state.status : 'pending' });\n void api\n .listNotifications({ limit: PAGE_SIZE })\n .then((page) => {\n if (token !== cell.request) return;\n patch(cell, { items: page.items, nextCursor: page.nextCursor, status: 'ready' });\n })\n .catch(() => {\n if (token !== cell.request) return;\n patch(cell, { status: 'error' });\n });\n}\n\nfunction invalidate(cell: Cell, api: NotificationsApiClient): void {\n refreshBadge(cell, api);\n if (cell.state.status !== 'idle') reloadList(cell, api);\n}\n\n/** Apply an optimistic edit; on failure, take the server's word instead. */\nfunction write(\n cell: Cell,\n api: NotificationsApiClient,\n apply: () => void,\n send: () => Promise<{ ok: boolean }>,\n): void {\n apply();\n void send()\n .then((result) => {\n if (!result.ok) invalidate(cell, api);\n })\n .catch(() => invalidate(cell, api));\n}\n\nfunction bumpUnread(cell: Cell, delta: number): void {\n patch(cell, { unread: Math.max(0, cell.state.unread + delta) });\n}\n\nfunction loadMore(cell: Cell, api: NotificationsApiClient): void {\n const cursor = cell.state.nextCursor;\n if (!cursor || cell.state.loadingMore) return;\n patch(cell, { loadingMore: true });\n void api\n .listNotifications({ cursor, limit: PAGE_SIZE })\n .then((page) => {\n patch(cell, {\n items: [...cell.state.items, ...page.items],\n nextCursor: page.nextCursor,\n loadingMore: false,\n });\n })\n .catch(() => patch(cell, { loadingMore: false }));\n}\n\nfunction markRead(cell: Cell, api: NotificationsApiClient, ids: readonly string[]): void {\n const readAt = new Date().toISOString();\n let flipped = 0;\n const items = cell.state.items.map((item) => {\n if (!ids.includes(item.id) || item.readAt !== null) return item;\n flipped += 1;\n return { ...item, readAt };\n });\n if (flipped === 0) return;\n write(\n cell,\n api,\n () => {\n patch(cell, { items });\n bumpUnread(cell, -flipped);\n },\n () => api.markRead(ids),\n );\n}\n\nfunction remove(cell: Cell, api: NotificationsApiClient, id: string): void {\n const target = cell.state.items.find((item) => item.id === id);\n if (!target) return;\n const items = cell.state.items.filter((item) => item.id !== id);\n write(\n cell,\n api,\n () => {\n patch(cell, { items });\n if (target.readAt === null) bumpUnread(cell, -1);\n },\n () => api.remove([id]),\n );\n}\n\nexport function createInboxStore(api: NotificationsApiClient): InboxStore {\n const cell: Cell = { state: EMPTY, listeners: new Set(), request: 0 };\n return {\n getState: () => cell.state,\n subscribe(listener) {\n cell.listeners.add(listener);\n return () => cell.listeners.delete(listener);\n },\n open() {\n if (cell.state.status === 'idle') reloadList(cell, api);\n },\n refreshBadge: () => refreshBadge(cell, api),\n invalidate: () => invalidate(cell, api),\n loadMore: () => loadMore(cell, api),\n markRead: (ids) => markRead(cell, api, ids),\n markAllRead() {\n const readAt = new Date().toISOString();\n const items = cell.state.items.map((item) => ({ ...item, readAt: item.readAt ?? readAt }));\n write(\n cell,\n api,\n () => patch(cell, { items, unread: 0 }),\n () => api.markAllRead(),\n );\n },\n remove: (id) => remove(cell, api, id),\n };\n}\n","import { useEffect, useSyncExternalStore } from 'react';\n\nimport {\n BADGE_POLL_MS,\n BADGE_RECONCILE_MS,\n type InboxState,\n type InboxStore,\n} from './inbox-state';\n\n/**\n * The two hooks the bell and the panel use, and the realtime seam between them.\n *\n * A host that has a message bus passes `subscribe`; one that has not passes\n * nothing and keeps the 60 s poll. The bell ships in this package and mounts in\n * whatever embeds it, so it must not require the host to have adopted anything.\n */\n\n/**\n * How the surface learns an inbox changed without asking.\n *\n * Called once per mounted bell with a callback that means only \"ask again\" — no\n * payload, so the number on screen is always one the server just gave us.\n * Returns its own teardown. A host wires this to whatever it already has.\n */\nexport type NotificationsSubscribe = (onHint: () => void) => () => void;\n\n/**\n * The same wiring, as a HOOK — for a host whose realtime connection lives in\n * React context rather than in a module.\n *\n * `subscribe` above is supplied at FACTORY time, which is module scope, and a\n * context-bound connection cannot be reached from there: the provider holding\n * it is inside the tree. A host in that shape (a `<UserRealtimeProvider>` and a\n * `useUserTopics` hook, which is the common one) had no way to pass anything at\n * all, and the badge simply never heard an event.\n *\n * So this is the second door, and it is the one `@12-apps/app-shell` already\n * uses for the same problem — its consent dialog takes a `useSignal` hook for\n * exactly this reason. Two packages solving one problem two ways is how an\n * adopter ends up believing the feature is unavailable to it.\n *\n * Called during render, so it may use context and hooks freely. Pass one or\n * the other; passing both runs both, which is a host's business.\n */\nexport type NotificationsSignalHook = (onHint: () => void) => void;\n\nexport function useInboxState(store: InboxStore): InboxState {\n return useSyncExternalStore(store.subscribe, store.getState, store.getState);\n}\n\n/** What both badge hooks below take, and what the bell passes them. */\nexport interface BadgeSyncOptions {\n enabled?: boolean;\n subscribe?: NotificationsSubscribe;\n useSignal?: NotificationsSignalHook;\n}\n\n/**\n * What a disabled badge reads, instead of whatever the store happens to hold.\n *\n * A CONSTANT, so `useSyncExternalStore`'s identity comparison sees no change\n * across the renders of a signed-out session.\n */\nconst NOTHING_TO_SHOW: InboxState = {\n unread: 0,\n items: [],\n status: 'idle',\n nextCursor: null,\n loadingMore: false,\n};\n\n/**\n * The badge's server state, kept fresh: pushed while a subscription is live,\n * polled otherwise.\n *\n * The whole state rather than the count, because every badge hook that layers\n * on top of it needs the poll and the subscription mounted exactly ONCE per\n * bell — read through two hooks, a bell that showed both a number and a tone\n * would open two of everything.\n *\n * ## `enabled` gates the ANSWER, not only the fetching\n *\n * It gates the poll and the subscription, which is the obvious half. It also\n * blanks the returned state, which is the half that was missing and matters\n * more: the store is per FACTORY and a host builds one at module scope for the\n * whole app, so signing out does not empty it — `refreshBadge` swallows the 401\n * and leaves the last number in place. Without this, a hook told there is\n * nobody signed in hands back the PREVIOUS reader's unread count and their\n * inbox rows.\n *\n * Deliberately here rather than at each caller. It was at each caller, three\n * times, in three shapes, and two of them were dead weight no test could reach\n * — which is what an invariant looks like just before one copy of it goes\n * missing.\n *\n * INTERNAL. Not exported from `./index`: it hands back rows as well as a count,\n * and a host wanting a number has `useUnreadCount` or the factory's\n * `useBellBadge`.\n */\nexport function useBadgeState(store: InboxStore, options: BadgeSyncOptions = {}): InboxState {\n const enabled = options.enabled ?? true;\n const subscribe = options.subscribe;\n const live = useInboxState(store);\n const state = enabled ? live : NOTHING_TO_SHOW;\n\n // Called unconditionally — it is a hook, so it cannot sit behind `enabled`.\n // The host's own hook decides what to do when there is nothing to hear.\n options.useSignal?.(() => {\n if (enabled) store.invalidate();\n });\n\n useEffect(() => {\n if (!enabled) return;\n store.refreshBadge();\n const unsubscribe = subscribe?.(() => store.invalidate());\n // A live subscription relaxes the poll to the reconcile interval; without\n // one it stays the 60 s poll.\n const interval = setInterval(\n () => store.refreshBadge(),\n subscribe ? BADGE_RECONCILE_MS : BADGE_POLL_MS,\n );\n const onFocus = (): void => store.refreshBadge();\n globalThis.addEventListener?.('focus', onFocus);\n return () => {\n clearInterval(interval);\n globalThis.removeEventListener?.('focus', onFocus);\n unsubscribe?.();\n };\n }, [store, enabled, subscribe]);\n\n return state;\n}\n\n/**\n * The bell badge number, for a host with its own trigger chrome.\n *\n * A host that also publishes live activities wants `useBellBadge` from the\n * factory instead — this one counts inbox rows and knows nothing about what is\n * happening right now.\n */\nexport function useUnreadCount(store: InboxStore, options: BadgeSyncOptions = {}): number {\n return useBadgeState(store, options).unread;\n}\n\n/** The panel's list — only fetches while the panel is open. */\nexport function useInboxList(store: InboxStore, open: boolean): InboxState {\n const state = useInboxState(store);\n useEffect(() => {\n if (open) store.open();\n }, [store, open]);\n return state;\n}\n","/** Inline SVG bell (no icon-library dependency in this package). */\nimport type { JSX } from 'react';\n\nimport { Box } from '@12-apps/ui/mui/Box';\n\nexport function BellIcon({\n size = 28,\n dim = false,\n}: {\n size?: number;\n dim?: boolean;\n}): JSX.Element {\n return (\n <Box\n component=\"svg\"\n viewBox=\"0 0 24 24\"\n aria-hidden\n sx={{\n width: size,\n height: size,\n fill: 'none',\n stroke: 'currentColor',\n opacity: dim ? 0.4 : 1,\n }}\n strokeWidth={1.8}\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n >\n <path d=\"M6 9a6 6 0 0 1 12 0c0 5 2 6 2 6H4s2-1 2-6\" />\n <path d=\"M10 20a2 2 0 0 0 4 0\" />\n </Box>\n );\n}\n"],"mappings":";;;;;AAqBO,IAAM,YAAY;AAGlB,IAAM,gBAAgB;AAYtB,IAAM,qBAAqB;AA4BlC,IAAM,QAAoB;AAAA,EACxB,QAAQ;AAAA,EACR,OAAO,CAAC;AAAA,EACR,QAAQ;AAAA,EACR,YAAY;AAAA,EACZ,aAAa;AACf;AAUA,SAAS,MAAM,MAAY,MAAiC;AAC1D,OAAK,QAAQ,EAAE,GAAG,KAAK,OAAO,GAAG,KAAK;AACtC,aAAW,YAAY,KAAK,UAAW,UAAS;AAClD;AAHS;AAMT,SAAS,aAAa,MAAY,KAAmC;AACnE,OAAK,IACF,YAAY,EACZ,KAAK,CAAC,WAAW,MAAM,MAAM,EAAE,OAAO,CAAC,CAAC,EACxC,MAAM,MAAM,MAAS;AAC1B;AALS;AAYT,SAAS,WAAW,MAAY,KAAmC;AACjE,QAAM,QAAS,KAAK,WAAW;AAC/B,QAAM,MAAM,EAAE,QAAQ,KAAK,MAAM,MAAM,SAAS,IAAI,KAAK,MAAM,SAAS,UAAU,CAAC;AACnF,OAAK,IACF,kBAAkB,EAAE,OAAO,UAAU,CAAC,EACtC,KAAK,CAAC,SAAS;AACd,QAAI,UAAU,KAAK,QAAS;AAC5B,UAAM,MAAM,EAAE,OAAO,KAAK,OAAO,YAAY,KAAK,YAAY,QAAQ,QAAQ,CAAC;AAAA,EACjF,CAAC,EACA,MAAM,MAAM;AACX,QAAI,UAAU,KAAK,QAAS;AAC5B,UAAM,MAAM,EAAE,QAAQ,QAAQ,CAAC;AAAA,EACjC,CAAC;AACL;AAbS;AAeT,SAAS,WAAW,MAAY,KAAmC;AACjE,eAAa,MAAM,GAAG;AACtB,MAAI,KAAK,MAAM,WAAW,OAAQ,YAAW,MAAM,GAAG;AACxD;AAHS;AAMT,SAAS,MACP,MACA,KACA,OACA,MACM;AACN,QAAM;AACN,OAAK,KAAK,EACP,KAAK,CAAC,WAAW;AAChB,QAAI,CAAC,OAAO,GAAI,YAAW,MAAM,GAAG;AAAA,EACtC,CAAC,EACA,MAAM,MAAM,WAAW,MAAM,GAAG,CAAC;AACtC;AAZS;AAcT,SAAS,WAAW,MAAY,OAAqB;AACnD,QAAM,MAAM,EAAE,QAAQ,KAAK,IAAI,GAAG,KAAK,MAAM,SAAS,KAAK,EAAE,CAAC;AAChE;AAFS;AAIT,SAAS,SAAS,MAAY,KAAmC;AAC/D,QAAM,SAAS,KAAK,MAAM;AAC1B,MAAI,CAAC,UAAU,KAAK,MAAM,YAAa;AACvC,QAAM,MAAM,EAAE,aAAa,KAAK,CAAC;AACjC,OAAK,IACF,kBAAkB,EAAE,QAAQ,OAAO,UAAU,CAAC,EAC9C,KAAK,CAAC,SAAS;AACd,UAAM,MAAM;AAAA,MACV,OAAO,CAAC,GAAG,KAAK,MAAM,OAAO,GAAG,KAAK,KAAK;AAAA,MAC1C,YAAY,KAAK;AAAA,MACjB,aAAa;AAAA,IACf,CAAC;AAAA,EACH,CAAC,EACA,MAAM,MAAM,MAAM,MAAM,EAAE,aAAa,MAAM,CAAC,CAAC;AACpD;AAdS;AAgBT,SAAS,SAAS,MAAY,KAA6B,KAA8B;AACvF,QAAM,UAAS,oBAAI,KAAK,GAAE,YAAY;AACtC,MAAI,UAAU;AACd,QAAM,QAAQ,KAAK,MAAM,MAAM,IAAI,CAAC,SAAS;AAC3C,QAAI,CAAC,IAAI,SAAS,KAAK,EAAE,KAAK,KAAK,WAAW,KAAM,QAAO;AAC3D,eAAW;AACX,WAAO,EAAE,GAAG,MAAM,OAAO;AAAA,EAC3B,CAAC;AACD,MAAI,YAAY,EAAG;AACnB;AAAA,IACE;AAAA,IACA;AAAA,IACA,MAAM;AACJ,YAAM,MAAM,EAAE,MAAM,CAAC;AACrB,iBAAW,MAAM,CAAC,OAAO;AAAA,IAC3B;AAAA,IACA,MAAM,IAAI,SAAS,GAAG;AAAA,EACxB;AACF;AAlBS;AAoBT,SAAS,OAAO,MAAY,KAA6B,IAAkB;AACzE,QAAM,SAAS,KAAK,MAAM,MAAM,KAAK,CAAC,SAAS,KAAK,OAAO,EAAE;AAC7D,MAAI,CAAC,OAAQ;AACb,QAAM,QAAQ,KAAK,MAAM,MAAM,OAAO,CAAC,SAAS,KAAK,OAAO,EAAE;AAC9D;AAAA,IACE;AAAA,IACA;AAAA,IACA,MAAM;AACJ,YAAM,MAAM,EAAE,MAAM,CAAC;AACrB,UAAI,OAAO,WAAW,KAAM,YAAW,MAAM,EAAE;AAAA,IACjD;AAAA,IACA,MAAM,IAAI,OAAO,CAAC,EAAE,CAAC;AAAA,EACvB;AACF;AAbS;AAeF,SAAS,iBAAiB,KAAyC;AACxE,QAAM,OAAa,EAAE,OAAO,OAAO,WAAW,oBAAI,IAAI,GAAG,SAAS,EAAE;AACpE,SAAO;AAAA,IACL,UAAU,6BAAM,KAAK,OAAX;AAAA,IACV,UAAU,UAAU;AAClB,WAAK,UAAU,IAAI,QAAQ;AAC3B,aAAO,MAAM,KAAK,UAAU,OAAO,QAAQ;AAAA,IAC7C;AAAA,IACA,OAAO;AACL,UAAI,KAAK,MAAM,WAAW,OAAQ,YAAW,MAAM,GAAG;AAAA,IACxD;AAAA,IACA,cAAc,6BAAM,aAAa,MAAM,GAAG,GAA5B;AAAA,IACd,YAAY,6BAAM,WAAW,MAAM,GAAG,GAA1B;AAAA,IACZ,UAAU,6BAAM,SAAS,MAAM,GAAG,GAAxB;AAAA,IACV,UAAU,wBAAC,QAAQ,SAAS,MAAM,KAAK,GAAG,GAAhC;AAAA,IACV,cAAc;AACZ,YAAM,UAAS,oBAAI,KAAK,GAAE,YAAY;AACtC,YAAM,QAAQ,KAAK,MAAM,MAAM,IAAI,CAAC,UAAU,EAAE,GAAG,MAAM,QAAQ,KAAK,UAAU,OAAO,EAAE;AACzF;AAAA,QACE;AAAA,QACA;AAAA,QACA,MAAM,MAAM,MAAM,EAAE,OAAO,QAAQ,EAAE,CAAC;AAAA,QACtC,MAAM,IAAI,YAAY;AAAA,MACxB;AAAA,IACF;AAAA,IACA,QAAQ,wBAAC,OAAO,OAAO,MAAM,KAAK,EAAE,GAA5B;AAAA,EACV;AACF;AA3BgB;;;AC5LhB,SAAS,WAAW,4BAA4B;AA8CzC,SAAS,cAAc,OAA+B;AAC3D,SAAO,qBAAqB,MAAM,WAAW,MAAM,UAAU,MAAM,QAAQ;AAC7E;AAFgB;AAiBhB,IAAM,kBAA8B;AAAA,EAClC,QAAQ;AAAA,EACR,OAAO,CAAC;AAAA,EACR,QAAQ;AAAA,EACR,YAAY;AAAA,EACZ,aAAa;AACf;AA8BO,SAAS,cAAc,OAAmB,UAA4B,CAAC,GAAe;AAC3F,QAAM,UAAU,QAAQ,WAAW;AACnC,QAAM,YAAY,QAAQ;AAC1B,QAAM,OAAO,cAAc,KAAK;AAChC,QAAM,QAAQ,UAAU,OAAO;AAI/B,UAAQ,YAAY,MAAM;AACxB,QAAI,QAAS,OAAM,WAAW;AAAA,EAChC,CAAC;AAED,YAAU,MAAM;AACd,QAAI,CAAC,QAAS;AACd,UAAM,aAAa;AACnB,UAAM,cAAc,YAAY,MAAM,MAAM,WAAW,CAAC;AAGxD,UAAM,WAAW;AAAA,MACf,MAAM,MAAM,aAAa;AAAA,MACzB,YAAY,qBAAqB;AAAA,IACnC;AACA,UAAM,UAAU,6BAAY,MAAM,aAAa,GAA/B;AAChB,eAAW,mBAAmB,SAAS,OAAO;AAC9C,WAAO,MAAM;AACX,oBAAc,QAAQ;AACtB,iBAAW,sBAAsB,SAAS,OAAO;AACjD,oBAAc;AAAA,IAChB;AAAA,EACF,GAAG,CAAC,OAAO,SAAS,SAAS,CAAC;AAE9B,SAAO;AACT;AAhCgB;AAyCT,SAAS,eAAe,OAAmB,UAA4B,CAAC,GAAW;AACxF,SAAO,cAAc,OAAO,OAAO,EAAE;AACvC;AAFgB;AAKT,SAAS,aAAa,OAAmB,MAA2B;AACzE,QAAM,QAAQ,cAAc,KAAK;AACjC,YAAU,MAAM;AACd,QAAI,KAAM,OAAM,KAAK;AAAA,EACvB,GAAG,CAAC,OAAO,IAAI,CAAC;AAChB,SAAO;AACT;AANgB;;;AC9IhB,SAAS,WAAW;AAUhB,SAeE,KAfF;AARG,SAAS,SAAS;AAAA,EACvB,OAAO;AAAA,EACP,MAAM;AACR,GAGgB;AACd,SACE;AAAA,IAAC;AAAA;AAAA,MACC,WAAU;AAAA,MACV,SAAQ;AAAA,MACR,eAAW;AAAA,MACX,IAAI;AAAA,QACF,OAAO;AAAA,QACP,QAAQ;AAAA,QACR,MAAM;AAAA,QACN,QAAQ;AAAA,QACR,SAAS,MAAM,MAAM;AAAA,MACvB;AAAA,MACA,aAAa;AAAA,MACb,eAAc;AAAA,MACd,gBAAe;AAAA,MAEf;AAAA,4BAAC,UAAK,GAAE,6CAA4C;AAAA,QACpD,oBAAC,UAAK,GAAE,wBAAuB;AAAA;AAAA;AAAA,EACjC;AAEJ;AA3BgB;","names":[]}
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/react/api.ts","../src/react/transport.ts","../src/react/create-web-notifications.tsx","../src/react/bell-badge.ts","../src/react/live-seen.ts","../src/react/bell-button.tsx","../src/react/panel-lazy.tsx","../src/react/page-lazy.tsx"],"sourcesContent":["import type { ChannelRow } from '../preferences-core';\nimport type { NotificationChannel } from '../types';\nimport type { ListNotificationsResult } from '../wire';\n\nimport type { NotificationsResult, NotificationsTransport } from './transport';\n\n/**\n * The wire client, bound to one mount (12-15).\n *\n * Every path this package's screens can call, in one place — which is what\n * makes the api half's route table and the web half's URLs one contract instead\n * of two lists that drift.\n */\n\n/** `GET <mount>/notification-preferences` and the PUT's answer. */\nexport interface PreferencesPayload {\n preferences: Record<string, ChannelRow>;\n availability: Record<NotificationChannel, boolean>;\n /** The host's taxonomy, so the screen renders it without being told twice. */\n categories: string[];\n}\n\n/** `GET <mount>/push-subscriptions`. */\nexport interface PushRegistrationPayload {\n /** null = web push is not configured on this deployment. */\n vapidPublicKey: string | null;\n count: number;\n /**\n * Whether the endpoint asked about is still registered to the caller. Present\n * only when one was passed — see {@link NotificationsApiClient.getPushRegistration}.\n */\n registered?: boolean;\n}\n\nexport interface NotificationsApiClient {\n listNotifications(input: {\n cursor?: string | null;\n limit?: number;\n filter?: 'all' | 'unread';\n }): Promise<ListNotificationsResult>;\n unreadCount(): Promise<number>;\n markRead(ids: readonly string[]): Promise<NotificationsResult<{ updated: number }>>;\n markAllRead(): Promise<NotificationsResult<{ updated: number }>>;\n remove(ids: readonly string[]): Promise<NotificationsResult<{ deleted: number }>>;\n getPreferences(): Promise<PreferencesPayload>;\n savePreference(\n category: string,\n channel: NotificationChannel,\n enabled: boolean,\n ): Promise<NotificationsResult<PreferencesPayload>>;\n /**\n * The deployment's VAPID key and the caller's device count — and, when an\n * `endpoint` is passed, whether the SERVER still has that exact subscription\n * under the caller's id. The browser holding a subscription object is not\n * evidence of that: a re-own or a 404/410 prune drops the row and leaves the\n * browser's object in place.\n */\n getPushRegistration(input?: { endpoint?: string }): Promise<PushRegistrationPayload>;\n savePushSubscription(input: {\n endpoint: string;\n keys: { p256dh: string; auth: string };\n }): Promise<NotificationsResult<{ count: number }>>;\n removePushSubscription(endpoint: string): Promise<NotificationsResult<{ count: number }>>;\n}\n\nexport function createNotificationsApiClient(\n apiBase: string,\n transport: NotificationsTransport,\n): NotificationsApiClient {\n const base = apiBase.replace(/\\/$/, '');\n const url = (path: string): string => `${base}${path}`;\n\n return {\n listNotifications({ cursor, limit, filter }) {\n const params = new URLSearchParams();\n if (limit !== undefined) params.set('limit', String(limit));\n if (cursor) params.set('cursor', cursor);\n if (filter) params.set('filter', filter);\n const query = params.toString();\n return transport.get<ListNotificationsResult>(\n url(`/notifications${query ? `?${query}` : ''}`),\n );\n },\n async unreadCount() {\n const { count } = await transport.get<{ count: number }>(\n url('/notifications/unread-count'),\n );\n return count;\n },\n markRead: (ids) =>\n transport.send(url('/notifications/mark-read'), 'POST', { ids: [...ids] }),\n markAllRead: () => transport.send(url('/notifications/mark-read'), 'POST', { all: true }),\n remove: (ids) => transport.send(url('/notifications/delete'), 'POST', { ids: [...ids] }),\n getPreferences: () => transport.get<PreferencesPayload>(url('/notification-preferences')),\n savePreference: (category, channel, enabled) =>\n transport.send(url('/notification-preferences'), 'PUT', {\n [category]: { [channel]: enabled },\n }),\n getPushRegistration: ({ endpoint } = {}) =>\n transport.get<PushRegistrationPayload>(\n url(\n endpoint\n ? `/push-subscriptions?endpoint=${encodeURIComponent(endpoint)}`\n : '/push-subscriptions',\n ),\n ),\n savePushSubscription: (input) => transport.send(url('/push-subscriptions'), 'POST', input),\n removePushSubscription: (endpoint) =>\n transport.send(url('/push-subscriptions'), 'DELETE', { endpoint }),\n };\n}\n","/**\n * How the notification screens reach their data (12-15) — the report-builder\n * transport doctrine: this is the ONLY way the surface performs I/O, so a\n * caller supplying one has substituted the entire backend without stubbing a\n * global. The default is same-origin `fetch` riding the browser's cookies.\n */\n\n/** A write outcome the screens branch on — never a thrown mutation. */\nexport type NotificationsResult<T> = { ok: true; data: T } | { ok: false; error: string };\n\n/** A failed read, carrying the status the screens branch on (401 = signed out). */\nexport class NotificationsHttpError extends Error {\n readonly status: number;\n constructor(status: number, message: string) {\n super(message);\n this.name = 'NotificationsHttpError';\n this.status = status;\n Object.setPrototypeOf(this, NotificationsHttpError.prototype);\n }\n}\n\nexport interface NotificationsTransport {\n /** A read. Returns the payload INSIDE the `{ data }` envelope. */\n get<T>(path: string): Promise<T>;\n /** A write. Returns a {@link NotificationsResult} rather than rejecting. */\n send<T>(path: string, method: string, body?: unknown): Promise<NotificationsResult<T>>;\n}\n\n/**\n * @param fallbackError What a failed write says when the server sent no\n * sentence of its own — REQUIRED, the host's words. `createWebNotifications`\n * already passes its (equally required) `messages.operationFailed`; only a\n * host constructing the transport directly writes it here. The old default\n * was one application's Portuguese, and the only string in this package the\n * required-messages port did not cover.\n */\nexport function httpNotificationsTransport(fallbackError: string): NotificationsTransport {\n return {\n async get<T>(path: string): Promise<T> {\n const response = await fetch(path, {\n credentials: 'same-origin',\n headers: { Accept: 'application/json' },\n });\n const payload = (await response.json().catch(() => null)) as\n | { data?: T; error?: string }\n | null;\n if (!response.ok) {\n throw new NotificationsHttpError(\n response.status,\n payload?.error ?? `HTTP ${response.status} for ${path}`,\n );\n }\n return (payload?.data ?? payload) as T;\n },\n\n async send<T>(path: string, method: string, body?: unknown): Promise<NotificationsResult<T>> {\n try {\n const response = await fetch(path, {\n method,\n credentials: 'same-origin',\n headers: {\n Accept: 'application/json',\n ...(body === undefined ? {} : { 'Content-Type': 'application/json' }),\n },\n ...(body === undefined ? {} : { body: JSON.stringify(body) }),\n });\n if (response.status === 204) return { ok: true, data: undefined as T };\n const payload = (await response.json().catch(() => null)) as\n | { data?: T; error?: string }\n | null;\n if (!response.ok) return { ok: false, error: payload?.error ?? fallbackError };\n return { ok: true, data: (payload?.data ?? payload) as T };\n } catch {\n return { ok: false, error: fallbackError };\n }\n },\n };\n}\n","import { useState, type ComponentType, type JSX } from 'react';\n\nimport { messagesOf, type NotificationMessages } from '../messages';\n\nimport { createNotificationsApiClient, type NotificationsApiClient } from './api';\nimport { useInboxBellBadge, useLiveBellBadge, type BellBadge } from './bell-badge';\nimport { BellButton, LiveBellButton, type BellButtonProps } from './bell-button';\nimport {\n useUnreadCount,\n type NotificationsSignalHook,\n type NotificationsSubscribe,\n} from './hooks';\nimport { createInboxStore, type InboxStore } from './inbox-state';\nimport type { LiveActivitiesConfig } from './live-config';\nimport { createLiveSeenStore, type LiveSeenStore } from './live-seen';\nimport { lazyNotificationsPanel } from './panel-lazy';\nimport type { NotificationsPanelProps } from './panel';\nimport { lazyPreferencesPage } from './page-lazy';\nimport type { PreferencesScreenProps } from './preferences-screen';\nimport { httpNotificationsTransport, type NotificationsTransport } from './transport';\nimport type { WebPushSetupConfig } from './web-push-setup';\n\n/**\n * The one thing this package exposes to a FRONTEND host (12-15).\n *\n * Everything the notification centre IS — the bell with its live badge, the\n * slide-over inbox with its optimistic mark-read / delete / mark-all and its\n * cursor pager, the preferences matrix with its availability hints and the\n * per-browser push enable step, and every wire call between them — lives inside\n * this package. The host names where the API is mounted, and that is the whole\n * wiring.\n *\n * `page` is the standalone surface (the preferences screen), which is the one\n * thing a host routes to. The bell and the panel are a PAIR a host drops into\n * its own chrome, and they share one store, so a read in the panel moves the\n * badge in the same tick.\n */\n\nexport interface NotificationsWebConfig {\n /** The account mount the routes live under, e.g. `/api/account`. */\n apiBase: string;\n /** How the surface reaches its data. Default: same-origin fetch. */\n transport?: NotificationsTransport;\n /** User-facing copy overrides (pt-BR product copy by default). */\n messages: NotificationMessages;\n /**\n * How the surface learns an inbox changed without asking — the host's message\n * bus. Without it the badge keeps its 60 s poll, which is the standing\n * contract rather than a fallback: a dropped event must cost latency, never\n * correctness.\n */\n subscribe?: NotificationsSubscribe;\n /**\n * The same wiring as a HOOK, for a host whose realtime connection lives in\n * React context — see `NotificationsSignalHook`. `subscribe` is read at\n * factory time, which such a host cannot reach.\n */\n useSignal?: NotificationsSignalHook;\n /** The browser push enable step's host seams (SW path, platform hint). */\n webPush?: WebPushSetupConfig;\n /**\n * LIVE ACTIVITIES — the ongoing-state entries pinned above the inbox list.\n *\n * Opt-in, and absent means absent: a host that passes nothing gets the panel\n * it had, with no section, no heading and no reserved space. See\n * `./live-config` for the two things a host has to supply (where they come\n * from, and what the section says) and `../live` for what one IS.\n */\n liveActivities?: LiveActivitiesConfig;\n}\n\nexport interface WebNotifications {\n /**\n * The routed surface: the preferences screen.\n *\n * Loaded on demand — see `page-lazy.tsx`. A host that mounts only the bell and\n * the panel never downloads it, and a host that routes to it fetches it while\n * entering that route.\n */\n page: ComponentType<PreferencesScreenProps>;\n /** The bell, already bound to the shared store. */\n BellButton: ComponentType<BellButtonProps>;\n /**\n * The inbox slide-over, sharing that store.\n *\n * Loaded the first time it is opened — see `panel-lazy.tsx`. Until then a\n * host's chrome carries the bell and nothing else.\n */\n Panel: ComponentType<NotificationsPanelProps>;\n /**\n * Bell + panel as ONE element, for a host that just wants the feature in its\n * header and does not want to own the open/closed state.\n */\n BellWithPanel: ComponentType<{\n enabled?: boolean;\n onNavigate?: (link: string) => void;\n }>;\n /**\n * The unread INBOX count.\n *\n * For a host with its own trigger chrome only when that host configured no\n * live activities — otherwise it is a bell that ignores everything happening\n * right now, and `useBellBadge` below is the door. Still the right hook for\n * anything that genuinely wants \"how many unread rows\".\n */\n useUnreadCount: (options?: { enabled?: boolean }) => number;\n /**\n * The badge's NUMBER AND TONE, for a host with its own trigger chrome.\n *\n * What `useUnreadCount` should have been for a host that also configured live\n * activities, and the reason it is a second door rather than a change to that\n * one: a count alone cannot express a bell, because a live entry is present\n * without being news (see `./bell-badge`). A host that renders\n * `useUnreadCount` in its own chrome gets a badge that ignores everything\n * happening right now — which is not a subtle wrongness, it is the pinned\n * pedido on screen going uncounted.\n *\n * Identical to what this package's own `BellButton` draws, because it is the\n * hook that bell uses. Without live activities configured it is\n * `useUnreadCount` plus `hasNew: count > 0`.\n */\n useBellBadge: (options?: { enabled?: boolean }) => BellBadge;\n /** The shared client state, for host glue. */\n store: InboxStore;\n /** The bound wire client. */\n api: NotificationsApiClient;\n /** The copy in force, so a host's own chrome can reuse a sentence. */\n messages: NotificationMessages;\n}\n\n/** What the factory passes both badge hooks: whatever realtime wiring it has. */\ntype SubscribeOption = {\n subscribe?: NotificationsSubscribe;\n useSignal?: NotificationsSignalHook;\n};\n\n/**\n * The two badge hooks, bound to this factory's store.\n *\n * `useBellBadge` is chosen ONCE here, the same way `Bell` is below and for the\n * same reason: `live.useActivities` is a hook, so which implementation runs\n * must not be a per-render decision.\n */\nfunction bindBadgeHooks(\n store: InboxStore,\n subscribeOption: SubscribeOption,\n liveSeen: LiveSeenStore,\n live: LiveActivitiesConfig | undefined,\n): Pick<WebNotifications, 'useUnreadCount' | 'useBellBadge'> {\n return {\n useUnreadCount: (options = {}) => useUnreadCount(store, { ...options, ...subscribeOption }),\n useBellBadge: live\n ? (options = {}) => useLiveBellBadge(store, live, liveSeen, { ...options, ...subscribeOption })\n : (options = {}) => useInboxBellBadge(store, { ...options, ...subscribeOption }),\n };\n}\n\nexport function createWebNotifications(config: NotificationsWebConfig): WebNotifications {\n const messages = messagesOf(config);\n const api = createNotificationsApiClient(\n config.apiBase,\n config.transport ?? httpNotificationsTransport(messages.operationFailed),\n );\n const store = createInboxStore(api);\n const webPush = config.webPush ?? {};\n const subscribe = config.subscribe;\n const subscribeOption = {\n ...(subscribe ? { subscribe } : {}),\n ...(config.useSignal ? { useSignal: config.useSignal } : {}),\n };\n\n // One store per factory, shared by the bell that READS it and the panel that\n // WRITES it — the same arrangement as the inbox store above, and for the same\n // reason: two independent copies would disagree about what the reader saw.\n const liveSeen = createLiveSeenStore();\n\n // Chosen ONCE, here, because `useActivities` is a hook and the choice must\n // not be made per render: a bell that read an optional config inside itself\n // would be calling a hook conditionally.\n const live = config.liveActivities;\n const Bell: ComponentType<BellButtonProps> = live\n ? (props) => (\n <LiveBellButton\n {...props}\n store={store}\n messages={messages}\n live={live}\n seen={liveSeen}\n {...subscribeOption}\n />\n )\n : (props) => (\n <BellButton {...props} store={store} messages={messages} {...subscribeOption} />\n );\n const Panel = lazyNotificationsPanel({\n store,\n messages,\n ...(live ? { live, liveSeen } : {}),\n });\n\n const badgeHooks = bindBadgeHooks(store, subscribeOption, liveSeen, live);\n\n function BellWithPanel({\n enabled = true,\n onNavigate,\n }: {\n enabled?: boolean;\n onNavigate?: (link: string) => void;\n }): JSX.Element {\n const [open, setOpen] = useState(false);\n return (\n <>\n <Bell enabled={enabled} onClick={() => setOpen(true)} />\n <Panel\n open={open}\n onClose={() => setOpen(false)}\n {...(onNavigate ? { onNavigate } : {})}\n />\n </>\n );\n }\n\n return {\n page: lazyPreferencesPage({ api, messages, webPush }),\n BellButton: Bell,\n Panel,\n BellWithPanel,\n ...badgeHooks,\n store,\n api,\n messages,\n };\n}\n","/**\n * What the bell shows: ONE number, and whether any of it is news.\n *\n * The badge answers two different questions with one glyph, and keeping them\n * apart is the whole design:\n *\n * - the COUNT is how many things the centre is holding for the reader;\n * - the TONE is whether any of them has happened since they last looked.\n *\n * An inbox row makes those the same question — an unread row is by definition\n * both present and unseen — which is why the distinction did not exist before\n * live activities did. A live activity separates them: a pedido that has been\n * `Preparo` for ten minutes is still worth a `1`, and shouting about it every\n * render is how a badge teaches people to stop reading it.\n *\n * ## This is here so a host can DRAW it\n *\n * The numbers were already correct inside this package's own `BellButton`, and\n * unreachable from a host that cannot take that component — a header whose cart\n * and search buttons are one styled icon-button is importing a second trigger\n * style the moment it does. Such a host had `useUnreadCount` and nothing else,\n * so its bell showed NOTHING while a pinned pedido sat inside the panel it\n * opens. Both bells now read these hooks, so a host cannot drift from what this\n * package renders.\n *\n * ## What it does NOT yet do\n *\n * A live subject usually also writes inbox rows as it moves, and this counts\n * both: a pedido with one unread row about it reads `2`. Subtracting the double\n * needs the server to say which unread rows name which subject, and that was\n * built, reviewed and pulled — for reasons about the CONTRACT rather than the\n * arithmetic, and worth recording so the next attempt starts past them:\n *\n * - it added a field to `GET /notifications/unread-count`, and at least one\n * adopter publishes that response as a closed schema to LLM clients. An\n * additive field is a breaking change against `additionalProperties: false`.\n * - the scan is per READER, so every host paid it — including the two SPAs in\n * that adopter that share one factory and configure no live activities at\n * all, and read the count through `useUnreadCount`, which never sees the\n * breakdown.\n * - it narrowed `NotificationsApiClient.unreadCount()` from `Promise<number>`,\n * which is a breaking change on a commit the release rules cut as a minor.\n *\n * The way through is an opt-in the surface asks for — a host with no live\n * activities then sends nothing different and receives nothing different.\n *\n * (An earlier revision of this docblock blamed a missing index instead. That\n * was wrong: `[userId, deletedAt, readAt]` is a full equality prefix over the\n * filter, and the `ORDER BY` the scan carried was not load-bearing, since a\n * tally does not care what order it counts in.)\n */\nimport { useMemo, useSyncExternalStore } from 'react';\n\nimport { useBadgeState, type BadgeSyncOptions } from './hooks';\nimport type { InboxStore } from './inbox-state';\nimport type { LiveActivitiesConfig } from './live-config';\nimport { hasUnseenActivity, type LiveSeenStore } from './live-seen';\n\n/** The bell's whole state — see the file docblock for what each half means. */\nexport interface BellBadge {\n /** What the badge shows. `0` renders no badge at all. */\n count: number;\n /**\n * Whether any of it has arrived or moved since the reader last looked.\n *\n * The trigger paints this as its accent colour; a host with its own chrome\n * decides how to say it, but it should be a difference somebody notices.\n */\n hasNew: boolean;\n}\n\n/**\n * The badge for a host with no live activities: unread rows, and that is all.\n *\n * `hasNew` is `count > 0` here, and not as a simplification — an UNREAD row is\n * one the reader has not seen, so for this host presence and novelty really are\n * the same fact.\n */\nexport function useInboxBellBadge(store: InboxStore, options: BadgeSyncOptions = {}): BellBadge {\n // `useBadgeState` already blanks itself when disabled — the gate lives there,\n // once, rather than at each of the three hooks that layer on it.\n const { unread } = useBadgeState(store, options);\n // MEMOISED, unlike the number `useUnreadCount` returns. `useSyncExternalStore`\n // re-renders on every `patch` and `patch` always allocates, so a poll that\n // comes back with an unchanged count would otherwise hand a host a new object\n // every 60 s — enough to re-fire a `useEffect` keyed on it, or defeat a\n // `React.memo` on the trigger, forever.\n return useMemo(() => ({ count: unread, hasNew: unread > 0 }), [unread]);\n}\n\n/**\n * The badge for a host that configured live activities.\n *\n * A SECOND hook rather than a flag on the one above, for the reason the bell\n * itself is two components: `live.useActivities` is a hook, so a single hook\n * reading an optional config would be calling one conditionally — which React\n * reports as a crash somewhere else entirely. The factory knows statically\n * which host it is building for and binds one.\n *\n * ## `enabled` is enforced HERE, not taken on trust\n *\n * A host is explicitly allowed to ignore the `active` hint and always answer —\n * `./live-config` calls that \"behaving correctly and merely paying for it\" — so\n * a signed-out header, which still MOUNTS the bell, can be handed a list of\n * somebody's pedidos. The guard below is the only thing between that and a\n * badge counting them.\n *\n * Defensive against the CONTRACT, not against an observed adopter: today's one\n * honours the hint on every lever it has. That is exactly why the guard needs\n * saying — nothing about the current tree would fail if it went, and the case\n * that covers it has to build the ignoring host itself.\n *\n * ## What it costs the host, stated plainly\n *\n * The bell is mounted for as long as the app is, so unlike the panel's copy of\n * this hook there is no \"nobody is looking\" state to stand down in — `active`\n * is simply `enabled`. A host that answers by polling therefore polls for every\n * signed-in reader whether or not they ever open the centre. That is the price\n * of a badge that knows about live activities at all, and the reason to answer\n * this hook from a pushed cache rather than from an interval.\n */\nexport function useLiveBellBadge(\n store: InboxStore,\n live: LiveActivitiesConfig,\n seen: LiveSeenStore,\n options: BadgeSyncOptions = {},\n): BellBadge {\n const enabled = options.enabled ?? true;\n const { unread } = useBadgeState(store, options);\n const activities = live.useActivities({ active: enabled });\n const seenAt = useSyncExternalStore(seen.subscribe, seen.read, seen.read);\n // The store's own half is already blanked by `useBadgeState`; the `enabled`\n // guard here is for the ACTIVITIES half, which comes from a host hook that\n // may have ignored the hint.\n //\n // A live entry COUNTS. It is a notification — it is the one the reader most\n // wants to know about — and the panel it opens lists it.\n const count = enabled ? unread + activities.length : 0;\n const hasNew = enabled && (unread > 0 || hasUnseenActivity(activities, seenAt));\n // Memoised on the two RESULTS, not on `activities`. A host's hook returns a\n // fresh array every render — the storefront's maps its query's rows, so\n // structural sharing keeps the DATA identical and the array new — so an\n // `activities` dependency would invalidate on every render and the memo would\n // buy nothing at all. `hasUnseenActivity` runs unmemoised in front of it,\n // which is a `.some()` over the handful of things happening at once.\n return useMemo(() => ({ count, hasNew }), [count, hasNew]);\n}\n","/**\n * What the reader has already been shown, so the bell can say NEW rather than\n * merely PRESENT.\n *\n * A live activity is unlike an inbox row in the one way that matters here: it\n * stays on the panel for as long as the thing is happening, so its presence\n * cannot mean \"you have not seen this\". A pedido that has been `Preparo` for\n * ten minutes is still live and still worth counting, but nothing has happened\n * — and a badge that shouts for a subject the reader has already looked at is a\n * badge people stop reading.\n *\n * So presence and novelty are answered separately: the COUNT comes from how\n * many are live, and the TONE comes from this. The panel writes it — being on\n * screen is what seen means — and the bell reads it.\n *\n * ## Per subject, not one watermark\n *\n * A single \"newest instant already seen\" is smaller and was the first cut, and\n * it is wrong in a way that shows up in normal use: a pedido placed ten minutes\n * ago but only now reaching the client arrives with an `updatedAt` BEHIND the\n * watermark, and would be silently marked as already seen. The reader has never\n * laid eyes on it. Keyed by subject, an id that has not been recorded is new\n * whatever its clock says.\n *\n * Bounded by pruning rather than by expiry: every write keeps only the subjects\n * that are live at that moment, so the record can never outgrow the number of\n * things happening at once. A subject that finishes and later comes back is\n * news again, which is correct — it is a different occurrence.\n */\nimport type { LiveActivity } from '../live';\n\nconst STORAGE_KEY = '12a.notifications.live-seen';\n\n/** id -> the `updatedAt` that was on screen. */\ntype SeenMap = Readonly<Record<string, string>>;\n\nconst EMPTY: SeenMap = {};\n\n/** ms since epoch, or `null` for an absent or unparseable stamp. */\nfunction instant(iso: string | undefined): number | null {\n if (iso === undefined) return null;\n const ms = Date.parse(iso);\n return Number.isNaN(ms) ? null : ms;\n}\n\n/**\n * Read/write through `try`, every time.\n *\n * `localStorage` is not merely absent in SSR and in a worker — the ACCESSOR\n * itself throws in a browser set to block site data. A notification bell that\n * cannot render because storage is blocked is a worse failure than one that\n * forgets what was seen, and forgetting degrades in the safe direction: towards\n * saying something is happening.\n */\nfunction readStored(): SeenMap {\n try {\n const raw = globalThis.localStorage?.getItem(STORAGE_KEY);\n if (!raw) return EMPTY;\n const parsed: unknown = JSON.parse(raw);\n if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) return EMPTY;\n // Anything can be in storage — another version of this package, or a person\n // with the devtools open. Keep only what has the shape this reads.\n const clean: Record<string, string> = {};\n for (const [id, value] of Object.entries(parsed)) {\n if (typeof value === 'string') clean[id] = value;\n }\n return clean;\n } catch {\n return EMPTY;\n }\n}\n\nfunction writeStored(value: SeenMap): void {\n try {\n globalThis.localStorage?.setItem(STORAGE_KEY, JSON.stringify(value));\n } catch {\n // Blocked or full. The badge stays new a while longer; nothing else breaks.\n }\n}\n\nexport interface LiveSeenStore {\n /** What has been shown, keyed by subject id. */\n read: () => SeenMap;\n /** Record that exactly these are on screen now, forgetting subjects that are not. */\n mark: (activities: readonly LiveActivity[]) => void;\n subscribe: (listener: () => void) => () => void;\n}\n\nexport function createLiveSeenStore(): LiveSeenStore {\n // Mirrored in memory as well as in storage: `useSyncExternalStore` compares\n // snapshots by IDENTITY and calls `read` on every render, so parsing storage\n // there would hand it a fresh object each time and re-render for ever.\n let current = readStored();\n const listeners = new Set<() => void>();\n\n return {\n read: () => current,\n mark: (activities) => {\n const next: Record<string, string> = {};\n for (const activity of activities) next[activity.id] = activity.updatedAt;\n // Identity is the snapshot, so an unchanged map must not become a new\n // object — see `read` above.\n const ids = Object.keys(next);\n const same =\n ids.length === Object.keys(current).length &&\n ids.every((id) => current[id] === next[id]);\n if (same) return;\n current = next;\n writeStored(next);\n for (const listener of listeners) listener();\n },\n subscribe: (listener) => {\n listeners.add(listener);\n return () => {\n listeners.delete(listener);\n };\n },\n };\n}\n\n/**\n * Whether any of these has moved, or arrived, since the reader last looked.\n *\n * An id with nothing recorded is new — that is the case the per-subject record\n * exists for. An unparseable stamp is treated as new too: the alternative is\n * silently never alerting for a host whose clock format this does not read.\n */\nexport function hasUnseenActivity(\n activities: readonly LiveActivity[],\n seen: SeenMap,\n): boolean {\n return activities.some((activity) => {\n const shown = instant(seen[activity.id]);\n if (shown === null) return true;\n const now = instant(activity.updatedAt);\n return now === null || now > shown;\n });\n}\n","/**\n * Bare bell trigger with the live unread badge — for hosts that do not already\n * have a styled icon-button slot.\n *\n * A host with its own trigger chrome uses `useBellBadge` + `Panel` directly,\n * and NOT `useUnreadCount`, which is what this sentence used to say. That\n * advice was taken, verbatim and by name, by a storefront whose header needed\n * its own trigger — and it gave that storefront a bell showing nothing at all\n * while a live pedido sat in the panel it opens, because `useUnreadCount`\n * counts inbox rows and knows nothing about what is happening right now.\n */\nimport type { JSX } from 'react';\n\nimport { Badge } from '@12-apps/ui/data-display/Badge';\nimport { Box } from '@12-apps/ui/mui/Box';\n\nimport type { NotificationMessages } from '../messages';\n\nimport { useInboxBellBadge, useLiveBellBadge } from './bell-badge';\nimport { BellIcon } from './bell-icon';\nimport type { NotificationsSignalHook, NotificationsSubscribe } from './hooks';\nimport type { LiveActivitiesConfig } from './live-config';\nimport type { LiveSeenStore } from './live-seen';\nimport type { InboxStore } from './inbox-state';\n\nconst triggerSx = {\n display: 'inline-flex',\n alignItems: 'center',\n justifyContent: 'center',\n p: 0.5,\n border: 'none',\n background: 'none',\n cursor: 'pointer',\n color: 'text.primary',\n lineHeight: 0,\n '& *': { cursor: 'pointer' },\n '&:hover': { color: 'primary.main' },\n '&:focus-visible': {\n outline: '2px solid',\n outlineColor: 'primary.main',\n outlineOffset: '2px',\n borderRadius: '50%',\n },\n} as const;\n\nexport interface BellButtonProps {\n onClick: () => void;\n /** Signed-out hosts still mount the bell; `false` silences it. */\n enabled?: boolean;\n}\n\n/**\n * The trigger itself, given a count and whether any of it is NEW.\n *\n * Presentational, and shared by both bells below, so the two can never drift on\n * what the badge looks like — only on where the number comes from.\n *\n * ## The two tones\n *\n * `primary` says *something happened*; `neutral` says *something is present*. A\n * live activity is the reason that distinction has to exist: it stays on the\n * panel for as long as the thing is happening, so a bell that painted every\n * live entry as new would be permanently red for a pedido the reader already\n * looked at, and a bell that ignored them would say nothing at all while one\n * was running. Grey keeps the count honest without spending attention twice.\n */\nfunction BellTrigger({\n onClick,\n count,\n hasNew,\n messages,\n}: {\n onClick: () => void;\n count: number;\n hasNew: boolean;\n messages: NotificationMessages;\n}): JSX.Element {\n return (\n <Box\n component=\"button\"\n type=\"button\"\n onClick={onClick}\n // `openBellWithUnread` rather than a new message, and not for want of\n // precision: `NotificationMessages` is REQUIRED of every host, so adding\n // a field is a breaking change to a package several apps already mount.\n // The sentence a host wrote for \"you have N\" is the sentence this wants.\n aria-label={count > 0 ? messages.openBellWithUnread(count) : messages.openBell}\n data-testid=\"notifications-bell\"\n sx={triggerSx}\n >\n <Badge\n content={count > 0 ? count : undefined}\n color={hasNew ? 'primary' : 'neutral'}\n variant=\"count\"\n max={99}\n data-testid=\"notifications-badge\"\n // The tone is carried by a colour, and a colour is not something a\n // test can read — nor, on its own, a signal every reader can. This is\n // what the tests assert on.\n data-tone={hasNew ? 'new' : 'seen'}\n >\n <BellIcon size={28} />\n </Badge>\n </Box>\n );\n}\n\nexport function BellButton({\n onClick,\n enabled = true,\n store,\n messages,\n subscribe,\n useSignal,\n}: BellButtonProps & {\n store: InboxStore;\n messages: NotificationMessages;\n subscribe?: NotificationsSubscribe;\n useSignal?: NotificationsSignalHook;\n}): JSX.Element {\n const badge = useInboxBellBadge(store, {\n enabled,\n ...(subscribe ? { subscribe } : {}),\n ...(useSignal ? { useSignal } : {}),\n });\n return <BellTrigger onClick={onClick} {...badge} messages={messages} />;\n}\n\n/**\n * The bell for a host that configured live activities.\n *\n * A SECOND component rather than a flag on the one above, because the host's\n * `useActivities` is a hook: reading an optional config inside one component\n * would mean calling it conditionally, which React reports as a crash in some\n * unrelated component rather than here. The factory knows statically which host\n * it is building for and picks one.\n *\n * What the number MEANS, and what it costs the host, is `bell-badge.ts` — the\n * same hook a host with its own trigger chrome reaches through the factory's\n * `useBellBadge`, so the two bells can never disagree about the count.\n */\nexport function LiveBellButton({\n onClick,\n enabled = true,\n store,\n messages,\n subscribe,\n useSignal,\n live,\n seen,\n}: BellButtonProps & {\n store: InboxStore;\n messages: NotificationMessages;\n subscribe?: NotificationsSubscribe;\n useSignal?: NotificationsSignalHook;\n live: LiveActivitiesConfig;\n seen: LiveSeenStore;\n}): JSX.Element {\n const badge = useLiveBellBadge(store, live, seen, {\n enabled,\n ...(subscribe ? { subscribe } : {}),\n ...(useSignal ? { useSignal } : {}),\n });\n return <BellTrigger onClick={onClick} {...badge} messages={messages} />;\n}\n","/**\n * The inbox slide-over, fetched the first time somebody opens it.\n *\n * The bell and the panel are a PAIR a host drops into its chrome, and that is\n * still true — but only the BELL is on screen when a page paints. The panel is\n * behind a tap, and a static import made every host pay for it up front: the\n * design-system `Drawer` and, through it, MUI's `SwipeableDrawer`, `Modal`,\n * `Slide` and the focus trap, plus the row, the empty state and the pager. On a\n * storefront that is a slide-over most visits never open, parsed before the\n * first screen can render.\n *\n * ## Why the gate is \"ever opened\" rather than `open`\n *\n * `lazy` fetches when a component first RENDERS, so a boundary that still\n * rendered the panel while closed would fetch immediately and buy nothing. This\n * renders `null` until the panel has been open once, which is what actually\n * defers the download to the tap.\n *\n * And once opened it STAYS mounted. Unmounting on close would throw away the\n * drawer's transition state, so the panel would vanish instead of sliding out,\n * and the entrance animation would re-run on every reopen — which someone\n * working through an inbox does repeatedly. The fetch happens once.\n *\n * The initial state reads `open` rather than starting at `false`, so a host that\n * mounts the panel already open renders it in the same commit instead of a frame\n * later.\n *\n * ## Why `null` for the fallback\n *\n * The only frame this can show anything is the one right after the tap, where a\n * spinner reads as a stall rather than as progress. The chunk is small and\n * same-origin.\n */\nimport { Suspense, lazy, useEffect, useState, type ComponentType, type JSX } from 'react';\n\nimport type { NotificationMessages } from '../messages';\n\nimport type { InboxStore } from './inbox-state';\nimport type { LiveActivitiesConfig } from './live-config';\nimport type { LiveSeenStore } from './live-seen';\nimport type { NotificationsPanelProps } from './panel';\n\n/** What the factory binds into the panel, and the host never passes. */\ninterface PanelParts {\n store: InboxStore;\n messages: NotificationMessages;\n /** Absent unless the host turned live activities on — see `./live-config`. */\n live?: LiveActivitiesConfig;\n /** Travels with `live`: where the panel records what the reader has seen. */\n liveSeen?: LiveSeenStore;\n}\n\nexport function lazyNotificationsPanel(\n parts: PanelParts,\n): ComponentType<NotificationsPanelProps> {\n const Bound = lazy(async () => {\n const { NotificationsPanel } = await import('./panel');\n return {\n default: (props: NotificationsPanelProps): JSX.Element => (\n <NotificationsPanel {...props} {...parts} />\n ),\n };\n });\n\n return function NotificationsPanelSlot(props: NotificationsPanelProps): JSX.Element | null {\n const [everOpened, setEverOpened] = useState(props.open);\n\n useEffect(() => {\n if (props.open) setEverOpened(true);\n }, [props.open]);\n\n if (!everOpened) return null;\n\n return (\n <Suspense fallback={null}>\n <Bound {...props} />\n </Suspense>\n );\n };\n}\n","/**\n * The routed preferences screen, fetched when a host actually routes to it.\n *\n * `createWebNotifications` returns two different KINDS of thing, and its own\n * docstring says so: `page` is \"the standalone surface … the one thing a host\n * routes to\", while the bell and the panel \"are a PAIR a host drops into its own\n * chrome\". Chrome is on screen from the first paint; a routed surface is not.\n *\n * A static import made that distinction invisible to a bundler. Every host that\n * put the bell in its header also shipped the preferences matrix — its channel\n * toggles, the per-browser push enable step, and the design-system `Switch`\n * behind them — in the same chunk as the header. A storefront paid for a\n * settings screen a shopper never opens, before its first screen could render;\n * a host that renders its OWN preferences page paid for this one twice.\n *\n * So `page` now loads on demand. Nothing else moves: the bell, the panel and\n * `BellWithPanel` stay exactly as eager as the chrome they belong to, because\n * that is what they are.\n *\n * NO PREFETCH, deliberately, and this is the opposite call from a surface a\n * host opens from chrome it already has. A routed surface is reached by\n * NAVIGATION, and every host here already code-splits its routes — so the\n * fetch happens while the route is being entered, which is the moment a\n * prefetch would have been trying to anticipate. Warming it at factory time\n * would put the screen back on the boot path of every app, which is the whole\n * cost this removes.\n */\nimport { Suspense, lazy, type ComponentType, type JSX } from 'react';\n\nimport type { NotificationMessages } from '../messages';\n\nimport type { NotificationsApiClient } from './api';\nimport type { PreferencesScreenProps } from './preferences-screen';\nimport type { WebPushSetupConfig } from './web-push-setup';\n\n/** What the factory binds into the screen, and the host never passes. */\ninterface PreferencesPageParts {\n api: NotificationsApiClient;\n messages: NotificationMessages;\n webPush: WebPushSetupConfig;\n}\n\n/**\n * The routed screen, bound and loaded on first render.\n *\n * `lazy` memoises its factory, so the binding below happens once however many\n * times a host mounts the page — the same guarantee the direct call gave.\n *\n * The fallback is `null` because a host routes to this: whatever it renders\n * around the route is already on screen, and a second spinner inside it would\n * be one more thing appearing and disappearing during a navigation the host is\n * already indicating.\n */\nexport function lazyPreferencesPage(\n parts: PreferencesPageParts,\n): ComponentType<PreferencesScreenProps> {\n const Bound = lazy(async () => {\n const { PreferencesScreen } = await import('./preferences-screen');\n return {\n default: (props: PreferencesScreenProps): JSX.Element => (\n <PreferencesScreen {...props} {...parts} />\n ),\n };\n });\n\n return function NotificationsPreferencesPage(props: PreferencesScreenProps): JSX.Element {\n return (\n <Suspense fallback={null}>\n <Bound {...props} />\n </Suspense>\n );\n };\n}\n"],"mappings":"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","names":["useState","jsx","Suspense","lazy","jsx","lazy","Suspense","jsx","useState"]}