@12-apps/notifications 4.7.0 → 4.7.1

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.
Files changed (41) hide show
  1. package/dist/chunk-BW723CX2.js +214 -0
  2. package/dist/chunk-BW723CX2.js.map +1 -0
  3. package/dist/chunk-CQZMTFPY.js +76 -0
  4. package/dist/chunk-CQZMTFPY.js.map +1 -0
  5. package/dist/{chunk-QRAXX3GR.js → chunk-CUZW62JS.js} +2 -2
  6. package/dist/{chunk-HQU4R4SG.js → chunk-HHMRCMQU.js} +2 -2
  7. package/dist/chunk-M2TVBVH2.js +15 -0
  8. package/dist/chunk-M2TVBVH2.js.map +1 -0
  9. package/dist/chunk-MMLV4EZT.js +263 -0
  10. package/dist/chunk-MMLV4EZT.js.map +1 -0
  11. package/dist/chunk-O5BVUXPO.js +22 -0
  12. package/dist/chunk-O5BVUXPO.js.map +1 -0
  13. package/dist/{chunk-TIGTBSAQ.js → chunk-WHBMPHQE.js} +6 -4
  14. package/dist/{chunk-TIGTBSAQ.js.map → chunk-WHBMPHQE.js.map} +1 -1
  15. package/dist/{chunk-Y34FX24X.js → chunk-XE7HZVMH.js} +2 -10
  16. package/dist/chunk-XE7HZVMH.js.map +1 -0
  17. package/dist/{create-web-notifications-BpNR8qH3.d.ts → create-web-notifications-BHCzaU2y.d.ts} +13 -2
  18. package/dist/hono/index.js +4 -3
  19. package/dist/hono/index.js.map +1 -1
  20. package/dist/index.js +5 -3
  21. package/dist/manifest/server.js +5 -4
  22. package/dist/manifest/server.js.map +1 -1
  23. package/dist/manifest/web.d.ts +1 -1
  24. package/dist/manifest/web.js +3 -2
  25. package/dist/manifest/web.js.map +1 -1
  26. package/dist/panel-UFXNO4AF.js +243 -0
  27. package/dist/panel-UFXNO4AF.js.map +1 -0
  28. package/dist/preferences-screen-IOW6Y2H2.js +294 -0
  29. package/dist/preferences-screen-IOW6Y2H2.js.map +1 -0
  30. package/dist/react/index.d.ts +2 -2
  31. package/dist/react/index.js +17 -11
  32. package/dist/server/index.js +5 -4
  33. package/package.json +2 -2
  34. package/src/react/create-web-notifications.tsx +19 -10
  35. package/src/react/page-lazy.tsx +73 -0
  36. package/src/react/panel-lazy.tsx +74 -0
  37. package/dist/chunk-6HLHQDKS.js +0 -1022
  38. package/dist/chunk-6HLHQDKS.js.map +0 -1
  39. package/dist/chunk-Y34FX24X.js.map +0 -1
  40. /package/dist/{chunk-QRAXX3GR.js.map → chunk-CUZW62JS.js.map} +0 -0
  41. /package/dist/{chunk-HQU4R4SG.js.map → chunk-HHMRCMQU.js.map} +0 -0
@@ -0,0 +1,214 @@
1
+ import {
2
+ __name
3
+ } from "./chunk-7QVYU63E.js";
4
+
5
+ // src/react/bell-icon.tsx
6
+ import { Box } from "@12-apps/ui/mui/Box";
7
+ import { jsx, jsxs } from "react/jsx-runtime";
8
+ function BellIcon({
9
+ size = 28,
10
+ dim = false
11
+ }) {
12
+ return /* @__PURE__ */ jsxs(
13
+ Box,
14
+ {
15
+ component: "svg",
16
+ viewBox: "0 0 24 24",
17
+ "aria-hidden": true,
18
+ sx: {
19
+ width: size,
20
+ height: size,
21
+ fill: "none",
22
+ stroke: "currentColor",
23
+ opacity: dim ? 0.4 : 1
24
+ },
25
+ strokeWidth: 1.8,
26
+ strokeLinecap: "round",
27
+ strokeLinejoin: "round",
28
+ children: [
29
+ /* @__PURE__ */ jsx("path", { d: "M6 9a6 6 0 0 1 12 0c0 5 2 6 2 6H4s2-1 2-6" }),
30
+ /* @__PURE__ */ jsx("path", { d: "M10 20a2 2 0 0 0 4 0" })
31
+ ]
32
+ }
33
+ );
34
+ }
35
+ __name(BellIcon, "BellIcon");
36
+
37
+ // src/react/inbox-state.ts
38
+ var PAGE_SIZE = 20;
39
+ var BADGE_POLL_MS = 6e4;
40
+ var BADGE_RECONCILE_MS = 3e5;
41
+ var EMPTY = {
42
+ unread: 0,
43
+ items: [],
44
+ status: "idle",
45
+ nextCursor: null,
46
+ loadingMore: false
47
+ };
48
+ function patch(cell, next) {
49
+ cell.state = { ...cell.state, ...next };
50
+ for (const listener of cell.listeners) listener();
51
+ }
52
+ __name(patch, "patch");
53
+ function refreshBadge(cell, api) {
54
+ void api.unreadCount().then((unread) => patch(cell, { unread })).catch(() => void 0);
55
+ }
56
+ __name(refreshBadge, "refreshBadge");
57
+ function reloadList(cell, api) {
58
+ const token = cell.request += 1;
59
+ patch(cell, { status: cell.state.items.length > 0 ? cell.state.status : "pending" });
60
+ void api.listNotifications({ limit: PAGE_SIZE }).then((page) => {
61
+ if (token !== cell.request) return;
62
+ patch(cell, { items: page.items, nextCursor: page.nextCursor, status: "ready" });
63
+ }).catch(() => {
64
+ if (token !== cell.request) return;
65
+ patch(cell, { status: "error" });
66
+ });
67
+ }
68
+ __name(reloadList, "reloadList");
69
+ function invalidate(cell, api) {
70
+ refreshBadge(cell, api);
71
+ if (cell.state.status !== "idle") reloadList(cell, api);
72
+ }
73
+ __name(invalidate, "invalidate");
74
+ function write(cell, api, apply, send) {
75
+ apply();
76
+ void send().then((result) => {
77
+ if (!result.ok) invalidate(cell, api);
78
+ }).catch(() => invalidate(cell, api));
79
+ }
80
+ __name(write, "write");
81
+ function bumpUnread(cell, delta) {
82
+ patch(cell, { unread: Math.max(0, cell.state.unread + delta) });
83
+ }
84
+ __name(bumpUnread, "bumpUnread");
85
+ function loadMore(cell, api) {
86
+ const cursor = cell.state.nextCursor;
87
+ if (!cursor || cell.state.loadingMore) return;
88
+ patch(cell, { loadingMore: true });
89
+ void api.listNotifications({ cursor, limit: PAGE_SIZE }).then((page) => {
90
+ patch(cell, {
91
+ items: [...cell.state.items, ...page.items],
92
+ nextCursor: page.nextCursor,
93
+ loadingMore: false
94
+ });
95
+ }).catch(() => patch(cell, { loadingMore: false }));
96
+ }
97
+ __name(loadMore, "loadMore");
98
+ function markRead(cell, api, ids) {
99
+ const readAt = (/* @__PURE__ */ new Date()).toISOString();
100
+ let flipped = 0;
101
+ const items = cell.state.items.map((item) => {
102
+ if (!ids.includes(item.id) || item.readAt !== null) return item;
103
+ flipped += 1;
104
+ return { ...item, readAt };
105
+ });
106
+ if (flipped === 0) return;
107
+ write(
108
+ cell,
109
+ api,
110
+ () => {
111
+ patch(cell, { items });
112
+ bumpUnread(cell, -flipped);
113
+ },
114
+ () => api.markRead(ids)
115
+ );
116
+ }
117
+ __name(markRead, "markRead");
118
+ function remove(cell, api, id) {
119
+ const target = cell.state.items.find((item) => item.id === id);
120
+ if (!target) return;
121
+ const items = cell.state.items.filter((item) => item.id !== id);
122
+ write(
123
+ cell,
124
+ api,
125
+ () => {
126
+ patch(cell, { items });
127
+ if (target.readAt === null) bumpUnread(cell, -1);
128
+ },
129
+ () => api.remove([id])
130
+ );
131
+ }
132
+ __name(remove, "remove");
133
+ function createInboxStore(api) {
134
+ const cell = { state: EMPTY, listeners: /* @__PURE__ */ new Set(), request: 0 };
135
+ return {
136
+ getState: /* @__PURE__ */ __name(() => cell.state, "getState"),
137
+ subscribe(listener) {
138
+ cell.listeners.add(listener);
139
+ return () => cell.listeners.delete(listener);
140
+ },
141
+ open() {
142
+ if (cell.state.status === "idle") reloadList(cell, api);
143
+ },
144
+ refreshBadge: /* @__PURE__ */ __name(() => refreshBadge(cell, api), "refreshBadge"),
145
+ invalidate: /* @__PURE__ */ __name(() => invalidate(cell, api), "invalidate"),
146
+ loadMore: /* @__PURE__ */ __name(() => loadMore(cell, api), "loadMore"),
147
+ markRead: /* @__PURE__ */ __name((ids) => markRead(cell, api, ids), "markRead"),
148
+ markAllRead() {
149
+ const readAt = (/* @__PURE__ */ new Date()).toISOString();
150
+ const items = cell.state.items.map((item) => ({ ...item, readAt: item.readAt ?? readAt }));
151
+ write(
152
+ cell,
153
+ api,
154
+ () => patch(cell, { items, unread: 0 }),
155
+ () => api.markAllRead()
156
+ );
157
+ },
158
+ remove: /* @__PURE__ */ __name((id) => remove(cell, api, id), "remove")
159
+ };
160
+ }
161
+ __name(createInboxStore, "createInboxStore");
162
+
163
+ // src/react/hooks.ts
164
+ import { useEffect, useSyncExternalStore } from "react";
165
+ function useInboxState(store) {
166
+ return useSyncExternalStore(store.subscribe, store.getState, store.getState);
167
+ }
168
+ __name(useInboxState, "useInboxState");
169
+ function useUnreadCount(store, options = {}) {
170
+ const enabled = options.enabled ?? true;
171
+ const subscribe = options.subscribe;
172
+ const { unread } = useInboxState(store);
173
+ options.useSignal?.(() => {
174
+ if (enabled) store.invalidate();
175
+ });
176
+ useEffect(() => {
177
+ if (!enabled) return;
178
+ store.refreshBadge();
179
+ const unsubscribe = subscribe?.(() => store.invalidate());
180
+ const interval = setInterval(
181
+ () => store.refreshBadge(),
182
+ subscribe ? BADGE_RECONCILE_MS : BADGE_POLL_MS
183
+ );
184
+ const onFocus = /* @__PURE__ */ __name(() => store.refreshBadge(), "onFocus");
185
+ globalThis.addEventListener?.("focus", onFocus);
186
+ return () => {
187
+ clearInterval(interval);
188
+ globalThis.removeEventListener?.("focus", onFocus);
189
+ unsubscribe?.();
190
+ };
191
+ }, [store, enabled, subscribe]);
192
+ return enabled ? unread : 0;
193
+ }
194
+ __name(useUnreadCount, "useUnreadCount");
195
+ function useInboxList(store, open) {
196
+ const state = useInboxState(store);
197
+ useEffect(() => {
198
+ if (open) store.open();
199
+ }, [store, open]);
200
+ return state;
201
+ }
202
+ __name(useInboxList, "useInboxList");
203
+
204
+ export {
205
+ BellIcon,
206
+ PAGE_SIZE,
207
+ BADGE_POLL_MS,
208
+ BADGE_RECONCILE_MS,
209
+ createInboxStore,
210
+ useInboxState,
211
+ useUnreadCount,
212
+ useInboxList
213
+ };
214
+ //# sourceMappingURL=chunk-BW723CX2.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/react/bell-icon.tsx","../src/react/inbox-state.ts","../src/react/hooks.ts"],"sourcesContent":["/** 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","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/**\n * The bell badge number: pushed while a subscription is live, polled otherwise.\n *\n * `enabled` gates the poll AND the subscription. A signed-out header still\n * mounts the bell, and there is nothing for it to hear.\n */\nexport function useUnreadCount(\n store: InboxStore,\n options: {\n enabled?: boolean;\n subscribe?: NotificationsSubscribe;\n useSignal?: NotificationsSignalHook;\n } = {},\n): number {\n const enabled = options.enabled ?? true;\n const subscribe = options.subscribe;\n const { unread } = useInboxState(store);\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 enabled ? unread : 0;\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"],"mappings":"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","names":[]}
@@ -0,0 +1,76 @@
1
+ import {
2
+ __name
3
+ } from "./chunk-7QVYU63E.js";
4
+
5
+ // src/react/web-push-client.ts
6
+ function base64UrlToUint8Array(base64Url) {
7
+ const padding = "=".repeat((4 - base64Url.length % 4) % 4);
8
+ const base64 = (base64Url + padding).replaceAll("-", "+").replaceAll("_", "/");
9
+ const raw = atob(base64);
10
+ return Uint8Array.from(raw, (char) => char.charCodeAt(0));
11
+ }
12
+ __name(base64UrlToUint8Array, "base64UrlToUint8Array");
13
+ function pushSupported() {
14
+ return typeof navigator !== "undefined" && "serviceWorker" in navigator && typeof window !== "undefined" && "PushManager" in window && "Notification" in window;
15
+ }
16
+ __name(pushSupported, "pushSupported");
17
+ async function getExistingPushSubscription() {
18
+ if (!pushSupported()) return null;
19
+ const registration = await navigator.serviceWorker.getRegistration();
20
+ if (!registration) return null;
21
+ return registration.pushManager.getSubscription();
22
+ }
23
+ __name(getExistingPushSubscription, "getExistingPushSubscription");
24
+ async function obtainSubscription(swPath, vapidPublicKey) {
25
+ const registration = await navigator.serviceWorker.register(swPath);
26
+ await navigator.serviceWorker.ready;
27
+ return await registration.pushManager.getSubscription() ?? registration.pushManager.subscribe({
28
+ userVisibleOnly: true,
29
+ applicationServerKey: base64UrlToUint8Array(vapidPublicKey)
30
+ });
31
+ }
32
+ __name(obtainSubscription, "obtainSubscription");
33
+ async function persist(api, subscription) {
34
+ const json = subscription.toJSON();
35
+ if (!json.endpoint || !json.keys?.p256dh || !json.keys.auth) {
36
+ return { ok: false, reason: "error" };
37
+ }
38
+ const saved = await api.savePushSubscription({
39
+ endpoint: json.endpoint,
40
+ keys: { p256dh: json.keys.p256dh, auth: json.keys.auth }
41
+ });
42
+ return saved.ok ? { ok: true } : { ok: false, reason: "error" };
43
+ }
44
+ __name(persist, "persist");
45
+ async function enableWebPush(api, swPath = "/sw.js") {
46
+ if (!pushSupported()) return { ok: false, reason: "unsupported" };
47
+ const registration = await api.getPushRegistration().catch(() => null);
48
+ if (!registration?.vapidPublicKey) return { ok: false, reason: "unconfigured" };
49
+ const permission = await Notification.requestPermission();
50
+ if (permission !== "granted") return { ok: false, reason: "permission-denied" };
51
+ try {
52
+ return await persist(
53
+ api,
54
+ await obtainSubscription(swPath, registration.vapidPublicKey)
55
+ );
56
+ } catch {
57
+ return { ok: false, reason: "error" };
58
+ }
59
+ }
60
+ __name(enableWebPush, "enableWebPush");
61
+ async function disableWebPush(api) {
62
+ const subscription = await getExistingPushSubscription();
63
+ if (!subscription) return;
64
+ const endpoint = subscription.endpoint;
65
+ await subscription.unsubscribe().catch(() => false);
66
+ await api.removePushSubscription(endpoint).catch(() => void 0);
67
+ }
68
+ __name(disableWebPush, "disableWebPush");
69
+
70
+ export {
71
+ pushSupported,
72
+ getExistingPushSubscription,
73
+ enableWebPush,
74
+ disableWebPush
75
+ };
76
+ //# sourceMappingURL=chunk-CQZMTFPY.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/react/web-push-client.ts"],"sourcesContent":["import type { NotificationsApiClient } from './api';\n\n/**\n * The browser half of Web Push: register the service worker, ask permission,\n * subscribe with the deployment's VAPID public key (read from the packaged\n * `GET <mount>/push-subscriptions`) and persist the subscription so the\n * WEB_PUSH transport can reach this browser.\n *\n * A preference alone cannot reach a device that never subscribed, which is why\n * this ships with the preferences screen rather than being left to the host.\n * The one thing that IS the host's is the service-worker path — path-routed SPAs\n * each control their own scope, and the file itself lives in the host's public\n * directory.\n */\n\n/** Why enabling push failed, mapped to a user-facing hint by the caller. */\nexport type PushSetupResult =\n | { ok: true }\n | { ok: false; reason: 'unsupported' | 'unconfigured' | 'permission-denied' | 'error' };\n\n/** `PushManager.subscribe` needs the VAPID key as a Uint8Array. */\nfunction base64UrlToUint8Array(base64Url: string): Uint8Array {\n const padding = '='.repeat((4 - (base64Url.length % 4)) % 4);\n const base64 = (base64Url + padding).replaceAll('-', '+').replaceAll('_', '/');\n const raw = atob(base64);\n return Uint8Array.from(raw, (char) => char.charCodeAt(0));\n}\n\nexport function pushSupported(): boolean {\n return (\n typeof navigator !== 'undefined' &&\n 'serviceWorker' in navigator &&\n typeof window !== 'undefined' &&\n 'PushManager' in window &&\n 'Notification' in window\n );\n}\n\n/** Whether this browser currently holds an active push subscription. */\nexport async function getExistingPushSubscription(): Promise<PushSubscription | null> {\n if (!pushSupported()) return null;\n const registration = await navigator.serviceWorker.getRegistration();\n if (!registration) return null;\n return registration.pushManager.getSubscription();\n}\n\n/** Register the SW and return this browser's (possibly new) subscription. */\nasync function obtainSubscription(\n swPath: string,\n vapidPublicKey: string,\n): Promise<PushSubscription> {\n const registration = await navigator.serviceWorker.register(swPath);\n await navigator.serviceWorker.ready;\n return (\n (await registration.pushManager.getSubscription()) ??\n registration.pushManager.subscribe({\n userVisibleOnly: true,\n applicationServerKey: base64UrlToUint8Array(vapidPublicKey) as BufferSource,\n })\n );\n}\n\n/**\n * Full enable flow: configured? → permission → SW registration → subscribe →\n * persist. Idempotent — an existing subscription is simply re-persisted (the\n * server upserts on the endpoint).\n */\n/** Persist the browser's subscription server-side (upsert on the endpoint). */\nasync function persist(\n api: NotificationsApiClient,\n subscription: PushSubscription,\n): Promise<PushSetupResult> {\n const json = subscription.toJSON();\n if (!json.endpoint || !json.keys?.p256dh || !json.keys.auth) {\n return { ok: false, reason: 'error' };\n }\n const saved = await api.savePushSubscription({\n endpoint: json.endpoint,\n keys: { p256dh: json.keys.p256dh, auth: json.keys.auth },\n });\n return saved.ok ? { ok: true } : { ok: false, reason: 'error' };\n}\n\nexport async function enableWebPush(\n api: NotificationsApiClient,\n swPath = '/sw.js',\n): Promise<PushSetupResult> {\n if (!pushSupported()) return { ok: false, reason: 'unsupported' };\n\n const registration = await api.getPushRegistration().catch(() => null);\n if (!registration?.vapidPublicKey) return { ok: false, reason: 'unconfigured' };\n\n const permission = await Notification.requestPermission();\n if (permission !== 'granted') return { ok: false, reason: 'permission-denied' };\n\n try {\n return await persist(\n api,\n await obtainSubscription(swPath, registration.vapidPublicKey),\n );\n } catch {\n return { ok: false, reason: 'error' };\n }\n}\n\n/** Disable flow: unsubscribe the browser and drop the server-side row. */\nexport async function disableWebPush(api: NotificationsApiClient): Promise<void> {\n const subscription = await getExistingPushSubscription();\n if (!subscription) return;\n const endpoint = subscription.endpoint;\n await subscription.unsubscribe().catch(() => false);\n await api.removePushSubscription(endpoint).catch(() => undefined);\n}\n"],"mappings":";;;;;AAqBA,SAAS,sBAAsB,WAA+B;AAC5D,QAAM,UAAU,IAAI,QAAQ,IAAK,UAAU,SAAS,KAAM,CAAC;AAC3D,QAAM,UAAU,YAAY,SAAS,WAAW,KAAK,GAAG,EAAE,WAAW,KAAK,GAAG;AAC7E,QAAM,MAAM,KAAK,MAAM;AACvB,SAAO,WAAW,KAAK,KAAK,CAAC,SAAS,KAAK,WAAW,CAAC,CAAC;AAC1D;AALS;AAOF,SAAS,gBAAyB;AACvC,SACE,OAAO,cAAc,eACrB,mBAAmB,aACnB,OAAO,WAAW,eAClB,iBAAiB,UACjB,kBAAkB;AAEtB;AARgB;AAWhB,eAAsB,8BAAgE;AACpF,MAAI,CAAC,cAAc,EAAG,QAAO;AAC7B,QAAM,eAAe,MAAM,UAAU,cAAc,gBAAgB;AACnE,MAAI,CAAC,aAAc,QAAO;AAC1B,SAAO,aAAa,YAAY,gBAAgB;AAClD;AALsB;AAQtB,eAAe,mBACb,QACA,gBAC2B;AAC3B,QAAM,eAAe,MAAM,UAAU,cAAc,SAAS,MAAM;AAClE,QAAM,UAAU,cAAc;AAC9B,SACG,MAAM,aAAa,YAAY,gBAAgB,KAChD,aAAa,YAAY,UAAU;AAAA,IACjC,iBAAiB;AAAA,IACjB,sBAAsB,sBAAsB,cAAc;AAAA,EAC5D,CAAC;AAEL;AAbe;AAqBf,eAAe,QACb,KACA,cAC0B;AAC1B,QAAM,OAAO,aAAa,OAAO;AACjC,MAAI,CAAC,KAAK,YAAY,CAAC,KAAK,MAAM,UAAU,CAAC,KAAK,KAAK,MAAM;AAC3D,WAAO,EAAE,IAAI,OAAO,QAAQ,QAAQ;AAAA,EACtC;AACA,QAAM,QAAQ,MAAM,IAAI,qBAAqB;AAAA,IAC3C,UAAU,KAAK;AAAA,IACf,MAAM,EAAE,QAAQ,KAAK,KAAK,QAAQ,MAAM,KAAK,KAAK,KAAK;AAAA,EACzD,CAAC;AACD,SAAO,MAAM,KAAK,EAAE,IAAI,KAAK,IAAI,EAAE,IAAI,OAAO,QAAQ,QAAQ;AAChE;AAbe;AAef,eAAsB,cACpB,KACA,SAAS,UACiB;AAC1B,MAAI,CAAC,cAAc,EAAG,QAAO,EAAE,IAAI,OAAO,QAAQ,cAAc;AAEhE,QAAM,eAAe,MAAM,IAAI,oBAAoB,EAAE,MAAM,MAAM,IAAI;AACrE,MAAI,CAAC,cAAc,eAAgB,QAAO,EAAE,IAAI,OAAO,QAAQ,eAAe;AAE9E,QAAM,aAAa,MAAM,aAAa,kBAAkB;AACxD,MAAI,eAAe,UAAW,QAAO,EAAE,IAAI,OAAO,QAAQ,oBAAoB;AAE9E,MAAI;AACF,WAAO,MAAM;AAAA,MACX;AAAA,MACA,MAAM,mBAAmB,QAAQ,aAAa,cAAc;AAAA,IAC9D;AAAA,EACF,QAAQ;AACN,WAAO,EAAE,IAAI,OAAO,QAAQ,QAAQ;AAAA,EACtC;AACF;AApBsB;AAuBtB,eAAsB,eAAe,KAA4C;AAC/E,QAAM,eAAe,MAAM,4BAA4B;AACvD,MAAI,CAAC,aAAc;AACnB,QAAM,WAAW,aAAa;AAC9B,QAAM,aAAa,YAAY,EAAE,MAAM,MAAM,KAAK;AAClD,QAAM,IAAI,uBAAuB,QAAQ,EAAE,MAAM,MAAM,MAAS;AAClE;AANsB;","names":[]}
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  UnknownNotificationRecipientError,
3
3
  UnknownNotificationTypeError
4
- } from "./chunk-HQU4R4SG.js";
4
+ } from "./chunk-HHMRCMQU.js";
5
5
  import {
6
6
  __name
7
7
  } from "./chunk-7QVYU63E.js";
@@ -91,4 +91,4 @@ export {
91
91
  NOTIFICATIONS_DRAIN_LEASE_MS,
92
92
  NOTIFICATIONS_JOBS
93
93
  };
94
- //# sourceMappingURL=chunk-QRAXX3GR.js.map
94
+ //# sourceMappingURL=chunk-CUZW62JS.js.map
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  NOTIFICATION_CHANNELS
3
- } from "./chunk-Y34FX24X.js";
3
+ } from "./chunk-XE7HZVMH.js";
4
4
  import {
5
5
  __name
6
6
  } from "./chunk-7QVYU63E.js";
@@ -131,4 +131,4 @@ export {
131
131
  normalizePhoneE164,
132
132
  inboxWire
133
133
  };
134
- //# sourceMappingURL=chunk-HQU4R4SG.js.map
134
+ //# sourceMappingURL=chunk-HHMRCMQU.js.map
@@ -0,0 +1,15 @@
1
+ import {
2
+ __name
3
+ } from "./chunk-7QVYU63E.js";
4
+
5
+ // src/messages.ts
6
+ function messagesOf(config, locale) {
7
+ const source = config.messages;
8
+ return typeof source === "function" ? source({ locale }) : source;
9
+ }
10
+ __name(messagesOf, "messagesOf");
11
+
12
+ export {
13
+ messagesOf
14
+ };
15
+ //# sourceMappingURL=chunk-M2TVBVH2.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/messages.ts"],"sourcesContent":["/**\n * Every sentence this package can say to a USER, stated by the HOST.\n *\n * The copy lives in ONE table rather than in each screen so the api half and\n * the react half can never disagree about a sentence — the 401 body the wire\n * returns and the error the panel renders come from the same key.\n *\n * THE pt-BR TABLE THAT USED TO BE THE DEFAULT IS GONE. Its own docstring said\n * what it was: \"the product copy the surface shipped with\", labelled in the\n * source as one named application's \"exact copy\". A description of one adopter,\n * shipped inside the package every other adopter installs, and reached by\n * saying nothing.\n *\n * `categoryLabels` is the sharpest of the forty. The categories themselves\n * became required config in the release before this one, precisely because\n * WHICH categories exist is product vocabulary — and their LABELS kept\n * defaulting, so a host that declared `['loans', 'fines']` got a labels map\n * describing somebody else's four. Required categories with defaulted labels\n * for a different host's categories is not a smaller version of the bug; it is\n * the same bug with a compile-time gesture in front of it.\n *\n * So `messages` is REQUIRED and whole. The interface is the checklist, and the\n * compiler names the sentences a host has not written yet.\n */\n/**\n * The sentences the SERVER half renders — and the whole of what a backend mount\n * has to state.\n *\n * Split out when `messages` became required. Requiring the full forty on a\n * server config would have made a backend-only adopter write three dozen\n * sentences for screens it does not serve, which is the kind of tax that gets a\n * required-config migration reverted rather than adopted. These four are the\n * ones the router and the route descriptors actually put on a wire.\n */\nexport interface NotificationWireMessages {\n unauthenticated: string;\n invalidBody: string;\n operationFailed: string;\n /** `POST /notifications/mark-read` with neither `ids` nor `all`. */\n markReadTargetRequired: string;\n}\n\n/** Every sentence, wire and screen — what the REACT half needs. */\nexport interface NotificationMessages extends NotificationWireMessages {\n // --- the inbox panel -----------------------------------------------------\n panelTitle: string;\n markAllRead: string;\n loading: string;\n loadMore: string;\n loadingMore: string;\n loadFailedTitle: string;\n loadFailedBody: string;\n retry: string;\n emptyTitle: string;\n emptyBody: string;\n openBell: string;\n /** `(count) => 'Abrir notificações (3 não lidas)'`. */\n openBellWithUnread: (count: number) => string;\n unreadSuffix: string;\n deleteOne: (title: string) => string;\n\n // --- relative timestamps -------------------------------------------------\n justNow: string;\n minutesAgo: (minutes: number) => string;\n hoursAgo: (hours: number) => string;\n daysAgo: (days: number) => string;\n /** Locale for the fallback absolute date on rows older than a week. */\n dateLocale: string;\n\n // --- the preferences screen ---------------------------------------------\n preferencesTitle: string;\n preferencesLead: string;\n channelLabels: Record<string, string>;\n channelUnavailableHints: Record<string, string>;\n categoryLabels: Record<string, { title: string; description: string }>;\n /** Fallback title for a category the host added but did not label. */\n categoryFallbackTitle: (category: string) => string;\n devicePushTitle: string;\n devicePushIdle: string;\n devicePushOn: string;\n devicePushDenied: string;\n devicePushFailed: string;\n devicePushEnable: string;\n devicePushEnabling: string;\n}\n\n/**\n * What a copy field takes once its words can follow a reader.\n *\n * Declared here rather than imported from `@12-apps/i18n`: this package must\n * stay liftable into a repo that has never heard of it, so the two agree\n * STRUCTURALLY and nothing forces the dependency. The context is deliberately\n * loose — a raw tag off the wire, unnarrowed — because matching it is the host\n * resolver's job, not this package's.\n */\nexport type NotificationsCopyResolver<T> = (context: {\n readonly locale?: string | null;\n}) => T;\nexport type NotificationsCopySource<T> = T | NotificationsCopyResolver<T>;\n\n/**\n * The messages in force, for ONE reader.\n *\n * A pass-through rather than a merge: there is nothing left to merge WITH, and\n * that is the point of the change. The old version spread the host's table over\n * the origin's, including PER KEY inside `channelLabels`,\n * `channelUnavailableHints` and `categoryLabels` — so a host that relabelled one\n * channel kept the origin's wording for the other three, and a host that\n * labelled its own two categories kept the origin's four sitting beside them in\n * the same screen.\n *\n * Kept as a function because all three mounts read it off a config object, and\n * because a later rule (a blank-string refusal, say) belongs in one place —\n * which is exactly what made it the right place to put the RESOLUTION when the\n * field learned to take a resolver.\n *\n * **Call it where the sentence is used.** `createApiNotifications` runs once\n * per process, and at least one host memoises its call behind an `if\n * (assembled) return assembled;`, so a value read there answers every later\n * request in the language the process started with — and a single-locale host\n * cannot tell the difference. The route handlers call it per request; the\n * parsers below them keep taking a plain pack, so one request resolves exactly\n * once and no helper can disagree with another about the language.\n *\n * The generic survives the widening: a host whose pack carries extra keys of\n * its own still gets them back, resolver or not.\n */\nexport function messagesOf<T extends NotificationWireMessages>(\n config: { messages: NotificationsCopySource<T> },\n locale?: string,\n): T {\n const source = config.messages;\n return typeof source === 'function'\n ? (source as NotificationsCopyResolver<T>)({ locale })\n : source;\n}\n"],"mappings":";;;;;AA+HO,SAAS,WACd,QACA,QACG;AACH,QAAM,SAAS,OAAO;AACtB,SAAO,OAAO,WAAW,aACpB,OAAwC,EAAE,OAAO,CAAC,IACnD;AACN;AARgB;","names":[]}
@@ -0,0 +1,263 @@
1
+ import {
2
+ BellIcon,
3
+ createInboxStore,
4
+ useUnreadCount
5
+ } from "./chunk-BW723CX2.js";
6
+ import {
7
+ messagesOf
8
+ } from "./chunk-M2TVBVH2.js";
9
+ import {
10
+ __name
11
+ } from "./chunk-7QVYU63E.js";
12
+
13
+ // src/react/api.ts
14
+ function createNotificationsApiClient(apiBase, transport) {
15
+ const base = apiBase.replace(/\/$/, "");
16
+ const url = /* @__PURE__ */ __name((path) => `${base}${path}`, "url");
17
+ return {
18
+ listNotifications({ cursor, limit, filter }) {
19
+ const params = new URLSearchParams();
20
+ if (limit !== void 0) params.set("limit", String(limit));
21
+ if (cursor) params.set("cursor", cursor);
22
+ if (filter) params.set("filter", filter);
23
+ const query = params.toString();
24
+ return transport.get(
25
+ url(`/notifications${query ? `?${query}` : ""}`)
26
+ );
27
+ },
28
+ async unreadCount() {
29
+ const { count } = await transport.get(
30
+ url("/notifications/unread-count")
31
+ );
32
+ return count;
33
+ },
34
+ markRead: /* @__PURE__ */ __name((ids) => transport.send(url("/notifications/mark-read"), "POST", { ids: [...ids] }), "markRead"),
35
+ markAllRead: /* @__PURE__ */ __name(() => transport.send(url("/notifications/mark-read"), "POST", { all: true }), "markAllRead"),
36
+ remove: /* @__PURE__ */ __name((ids) => transport.send(url("/notifications/delete"), "POST", { ids: [...ids] }), "remove"),
37
+ getPreferences: /* @__PURE__ */ __name(() => transport.get(url("/notification-preferences")), "getPreferences"),
38
+ savePreference: /* @__PURE__ */ __name((category, channel, enabled) => transport.send(url("/notification-preferences"), "PUT", {
39
+ [category]: { [channel]: enabled }
40
+ }), "savePreference"),
41
+ getPushRegistration: /* @__PURE__ */ __name(({ endpoint } = {}) => transport.get(
42
+ url(
43
+ endpoint ? `/push-subscriptions?endpoint=${encodeURIComponent(endpoint)}` : "/push-subscriptions"
44
+ )
45
+ ), "getPushRegistration"),
46
+ savePushSubscription: /* @__PURE__ */ __name((input) => transport.send(url("/push-subscriptions"), "POST", input), "savePushSubscription"),
47
+ removePushSubscription: /* @__PURE__ */ __name((endpoint) => transport.send(url("/push-subscriptions"), "DELETE", { endpoint }), "removePushSubscription")
48
+ };
49
+ }
50
+ __name(createNotificationsApiClient, "createNotificationsApiClient");
51
+
52
+ // src/react/transport.ts
53
+ var NotificationsHttpError = class _NotificationsHttpError extends Error {
54
+ static {
55
+ __name(this, "NotificationsHttpError");
56
+ }
57
+ status;
58
+ constructor(status, message) {
59
+ super(message);
60
+ this.name = "NotificationsHttpError";
61
+ this.status = status;
62
+ Object.setPrototypeOf(this, _NotificationsHttpError.prototype);
63
+ }
64
+ };
65
+ function httpNotificationsTransport(fallbackError) {
66
+ return {
67
+ async get(path) {
68
+ const response = await fetch(path, {
69
+ credentials: "same-origin",
70
+ headers: { Accept: "application/json" }
71
+ });
72
+ const payload = await response.json().catch(() => null);
73
+ if (!response.ok) {
74
+ throw new NotificationsHttpError(
75
+ response.status,
76
+ payload?.error ?? `HTTP ${response.status} for ${path}`
77
+ );
78
+ }
79
+ return payload?.data ?? payload;
80
+ },
81
+ async send(path, method, body) {
82
+ try {
83
+ const response = await fetch(path, {
84
+ method,
85
+ credentials: "same-origin",
86
+ headers: {
87
+ Accept: "application/json",
88
+ ...body === void 0 ? {} : { "Content-Type": "application/json" }
89
+ },
90
+ ...body === void 0 ? {} : { body: JSON.stringify(body) }
91
+ });
92
+ if (response.status === 204) return { ok: true, data: void 0 };
93
+ const payload = await response.json().catch(() => null);
94
+ if (!response.ok) return { ok: false, error: payload?.error ?? fallbackError };
95
+ return { ok: true, data: payload?.data ?? payload };
96
+ } catch {
97
+ return { ok: false, error: fallbackError };
98
+ }
99
+ }
100
+ };
101
+ }
102
+ __name(httpNotificationsTransport, "httpNotificationsTransport");
103
+
104
+ // src/react/create-web-notifications.tsx
105
+ import { useState as useState2 } from "react";
106
+
107
+ // src/react/bell-button.tsx
108
+ import { Badge } from "@12-apps/ui/data-display/Badge";
109
+ import { Box } from "@12-apps/ui/mui/Box";
110
+ import { jsx } from "react/jsx-runtime";
111
+ var triggerSx = {
112
+ display: "inline-flex",
113
+ alignItems: "center",
114
+ justifyContent: "center",
115
+ p: 0.5,
116
+ border: "none",
117
+ background: "none",
118
+ cursor: "pointer",
119
+ color: "text.primary",
120
+ lineHeight: 0,
121
+ "& *": { cursor: "pointer" },
122
+ "&:hover": { color: "primary.main" },
123
+ "&:focus-visible": {
124
+ outline: "2px solid",
125
+ outlineColor: "primary.main",
126
+ outlineOffset: "2px",
127
+ borderRadius: "50%"
128
+ }
129
+ };
130
+ function BellButton({
131
+ onClick,
132
+ enabled = true,
133
+ store,
134
+ messages,
135
+ subscribe,
136
+ useSignal
137
+ }) {
138
+ const count = useUnreadCount(store, {
139
+ enabled,
140
+ ...subscribe ? { subscribe } : {},
141
+ ...useSignal ? { useSignal } : {}
142
+ });
143
+ return /* @__PURE__ */ jsx(
144
+ Box,
145
+ {
146
+ component: "button",
147
+ type: "button",
148
+ onClick,
149
+ "aria-label": count > 0 ? messages.openBellWithUnread(count) : messages.openBell,
150
+ "data-testid": "notifications-bell",
151
+ sx: triggerSx,
152
+ children: /* @__PURE__ */ jsx(
153
+ Badge,
154
+ {
155
+ content: count > 0 ? count : void 0,
156
+ color: "primary",
157
+ variant: "count",
158
+ max: 99,
159
+ "data-testid": "notifications-badge",
160
+ children: /* @__PURE__ */ jsx(BellIcon, { size: 28 })
161
+ }
162
+ )
163
+ }
164
+ );
165
+ }
166
+ __name(BellButton, "BellButton");
167
+
168
+ // src/react/panel-lazy.tsx
169
+ import { Suspense, lazy, useEffect, useState } from "react";
170
+ import { jsx as jsx2 } from "react/jsx-runtime";
171
+ function lazyNotificationsPanel(parts) {
172
+ const Bound = lazy(async () => {
173
+ const { NotificationsPanel } = await import("./panel-UFXNO4AF.js");
174
+ return {
175
+ default: /* @__PURE__ */ __name((props) => /* @__PURE__ */ jsx2(NotificationsPanel, { ...props, ...parts }), "default")
176
+ };
177
+ });
178
+ return /* @__PURE__ */ __name(function NotificationsPanelSlot(props) {
179
+ const [everOpened, setEverOpened] = useState(props.open);
180
+ useEffect(() => {
181
+ if (props.open) setEverOpened(true);
182
+ }, [props.open]);
183
+ if (!everOpened) return null;
184
+ return /* @__PURE__ */ jsx2(Suspense, { fallback: null, children: /* @__PURE__ */ jsx2(Bound, { ...props }) });
185
+ }, "NotificationsPanelSlot");
186
+ }
187
+ __name(lazyNotificationsPanel, "lazyNotificationsPanel");
188
+
189
+ // src/react/page-lazy.tsx
190
+ import { Suspense as Suspense2, lazy as lazy2 } from "react";
191
+ import { jsx as jsx3 } from "react/jsx-runtime";
192
+ function lazyPreferencesPage(parts) {
193
+ const Bound = lazy2(async () => {
194
+ const { PreferencesScreen } = await import("./preferences-screen-IOW6Y2H2.js");
195
+ return {
196
+ default: /* @__PURE__ */ __name((props) => /* @__PURE__ */ jsx3(PreferencesScreen, { ...props, ...parts }), "default")
197
+ };
198
+ });
199
+ return /* @__PURE__ */ __name(function NotificationsPreferencesPage(props) {
200
+ return /* @__PURE__ */ jsx3(Suspense2, { fallback: null, children: /* @__PURE__ */ jsx3(Bound, { ...props }) });
201
+ }, "NotificationsPreferencesPage");
202
+ }
203
+ __name(lazyPreferencesPage, "lazyPreferencesPage");
204
+
205
+ // src/react/create-web-notifications.tsx
206
+ import { Fragment, jsx as jsx4, jsxs } from "react/jsx-runtime";
207
+ function createWebNotifications(config) {
208
+ const messages = messagesOf(config);
209
+ const api = createNotificationsApiClient(
210
+ config.apiBase,
211
+ config.transport ?? httpNotificationsTransport(messages.operationFailed)
212
+ );
213
+ const store = createInboxStore(api);
214
+ const webPush = config.webPush ?? {};
215
+ const subscribe = config.subscribe;
216
+ const subscribeOption = {
217
+ ...subscribe ? { subscribe } : {},
218
+ ...config.useSignal ? { useSignal: config.useSignal } : {}
219
+ };
220
+ const Bell = /* @__PURE__ */ __name((props) => /* @__PURE__ */ jsx4(BellButton, { ...props, store, messages, ...subscribeOption }), "Bell");
221
+ const Panel = lazyNotificationsPanel({ store, messages });
222
+ function useBoundUnreadCount(options = {}) {
223
+ return useUnreadCount(store, { ...options, ...subscribeOption });
224
+ }
225
+ __name(useBoundUnreadCount, "useBoundUnreadCount");
226
+ function BellWithPanel({
227
+ enabled = true,
228
+ onNavigate
229
+ }) {
230
+ const [open, setOpen] = useState2(false);
231
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
232
+ /* @__PURE__ */ jsx4(Bell, { enabled, onClick: () => setOpen(true) }),
233
+ /* @__PURE__ */ jsx4(
234
+ Panel,
235
+ {
236
+ open,
237
+ onClose: () => setOpen(false),
238
+ ...onNavigate ? { onNavigate } : {}
239
+ }
240
+ )
241
+ ] });
242
+ }
243
+ __name(BellWithPanel, "BellWithPanel");
244
+ return {
245
+ page: lazyPreferencesPage({ api, messages, webPush }),
246
+ BellButton: Bell,
247
+ Panel,
248
+ BellWithPanel,
249
+ useUnreadCount: useBoundUnreadCount,
250
+ store,
251
+ api,
252
+ messages
253
+ };
254
+ }
255
+ __name(createWebNotifications, "createWebNotifications");
256
+
257
+ export {
258
+ createNotificationsApiClient,
259
+ NotificationsHttpError,
260
+ httpNotificationsTransport,
261
+ createWebNotifications
262
+ };
263
+ //# sourceMappingURL=chunk-MMLV4EZT.js.map