@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
@@ -1,1022 +0,0 @@
1
- import {
2
- NOTIFICATION_CHANNELS,
3
- messagesOf
4
- } from "./chunk-Y34FX24X.js";
5
- import {
6
- __name
7
- } from "./chunk-7QVYU63E.js";
8
-
9
- // src/react/api.ts
10
- function createNotificationsApiClient(apiBase, transport) {
11
- const base = apiBase.replace(/\/$/, "");
12
- const url = /* @__PURE__ */ __name((path) => `${base}${path}`, "url");
13
- return {
14
- listNotifications({ cursor, limit, filter }) {
15
- const params = new URLSearchParams();
16
- if (limit !== void 0) params.set("limit", String(limit));
17
- if (cursor) params.set("cursor", cursor);
18
- if (filter) params.set("filter", filter);
19
- const query = params.toString();
20
- return transport.get(
21
- url(`/notifications${query ? `?${query}` : ""}`)
22
- );
23
- },
24
- async unreadCount() {
25
- const { count } = await transport.get(
26
- url("/notifications/unread-count")
27
- );
28
- return count;
29
- },
30
- markRead: /* @__PURE__ */ __name((ids) => transport.send(url("/notifications/mark-read"), "POST", { ids: [...ids] }), "markRead"),
31
- markAllRead: /* @__PURE__ */ __name(() => transport.send(url("/notifications/mark-read"), "POST", { all: true }), "markAllRead"),
32
- remove: /* @__PURE__ */ __name((ids) => transport.send(url("/notifications/delete"), "POST", { ids: [...ids] }), "remove"),
33
- getPreferences: /* @__PURE__ */ __name(() => transport.get(url("/notification-preferences")), "getPreferences"),
34
- savePreference: /* @__PURE__ */ __name((category, channel, enabled) => transport.send(url("/notification-preferences"), "PUT", {
35
- [category]: { [channel]: enabled }
36
- }), "savePreference"),
37
- getPushRegistration: /* @__PURE__ */ __name(({ endpoint } = {}) => transport.get(
38
- url(
39
- endpoint ? `/push-subscriptions?endpoint=${encodeURIComponent(endpoint)}` : "/push-subscriptions"
40
- )
41
- ), "getPushRegistration"),
42
- savePushSubscription: /* @__PURE__ */ __name((input) => transport.send(url("/push-subscriptions"), "POST", input), "savePushSubscription"),
43
- removePushSubscription: /* @__PURE__ */ __name((endpoint) => transport.send(url("/push-subscriptions"), "DELETE", { endpoint }), "removePushSubscription")
44
- };
45
- }
46
- __name(createNotificationsApiClient, "createNotificationsApiClient");
47
-
48
- // src/react/bell-icon.tsx
49
- import { Box } from "@12-apps/ui/mui/Box";
50
- import { jsx, jsxs } from "react/jsx-runtime";
51
- function BellIcon({
52
- size = 28,
53
- dim = false
54
- }) {
55
- return /* @__PURE__ */ jsxs(
56
- Box,
57
- {
58
- component: "svg",
59
- viewBox: "0 0 24 24",
60
- "aria-hidden": true,
61
- sx: {
62
- width: size,
63
- height: size,
64
- fill: "none",
65
- stroke: "currentColor",
66
- opacity: dim ? 0.4 : 1
67
- },
68
- strokeWidth: 1.8,
69
- strokeLinecap: "round",
70
- strokeLinejoin: "round",
71
- children: [
72
- /* @__PURE__ */ jsx("path", { d: "M6 9a6 6 0 0 1 12 0c0 5 2 6 2 6H4s2-1 2-6" }),
73
- /* @__PURE__ */ jsx("path", { d: "M10 20a2 2 0 0 0 4 0" })
74
- ]
75
- }
76
- );
77
- }
78
- __name(BellIcon, "BellIcon");
79
-
80
- // src/react/inbox-state.ts
81
- var PAGE_SIZE = 20;
82
- var BADGE_POLL_MS = 6e4;
83
- var BADGE_RECONCILE_MS = 3e5;
84
- var EMPTY = {
85
- unread: 0,
86
- items: [],
87
- status: "idle",
88
- nextCursor: null,
89
- loadingMore: false
90
- };
91
- function patch(cell, next) {
92
- cell.state = { ...cell.state, ...next };
93
- for (const listener of cell.listeners) listener();
94
- }
95
- __name(patch, "patch");
96
- function refreshBadge(cell, api) {
97
- void api.unreadCount().then((unread) => patch(cell, { unread })).catch(() => void 0);
98
- }
99
- __name(refreshBadge, "refreshBadge");
100
- function reloadList(cell, api) {
101
- const token = cell.request += 1;
102
- patch(cell, { status: cell.state.items.length > 0 ? cell.state.status : "pending" });
103
- void api.listNotifications({ limit: PAGE_SIZE }).then((page) => {
104
- if (token !== cell.request) return;
105
- patch(cell, { items: page.items, nextCursor: page.nextCursor, status: "ready" });
106
- }).catch(() => {
107
- if (token !== cell.request) return;
108
- patch(cell, { status: "error" });
109
- });
110
- }
111
- __name(reloadList, "reloadList");
112
- function invalidate(cell, api) {
113
- refreshBadge(cell, api);
114
- if (cell.state.status !== "idle") reloadList(cell, api);
115
- }
116
- __name(invalidate, "invalidate");
117
- function write(cell, api, apply, send) {
118
- apply();
119
- void send().then((result) => {
120
- if (!result.ok) invalidate(cell, api);
121
- }).catch(() => invalidate(cell, api));
122
- }
123
- __name(write, "write");
124
- function bumpUnread(cell, delta) {
125
- patch(cell, { unread: Math.max(0, cell.state.unread + delta) });
126
- }
127
- __name(bumpUnread, "bumpUnread");
128
- function loadMore(cell, api) {
129
- const cursor = cell.state.nextCursor;
130
- if (!cursor || cell.state.loadingMore) return;
131
- patch(cell, { loadingMore: true });
132
- void api.listNotifications({ cursor, limit: PAGE_SIZE }).then((page) => {
133
- patch(cell, {
134
- items: [...cell.state.items, ...page.items],
135
- nextCursor: page.nextCursor,
136
- loadingMore: false
137
- });
138
- }).catch(() => patch(cell, { loadingMore: false }));
139
- }
140
- __name(loadMore, "loadMore");
141
- function markRead(cell, api, ids) {
142
- const readAt = (/* @__PURE__ */ new Date()).toISOString();
143
- let flipped = 0;
144
- const items = cell.state.items.map((item) => {
145
- if (!ids.includes(item.id) || item.readAt !== null) return item;
146
- flipped += 1;
147
- return { ...item, readAt };
148
- });
149
- if (flipped === 0) return;
150
- write(
151
- cell,
152
- api,
153
- () => {
154
- patch(cell, { items });
155
- bumpUnread(cell, -flipped);
156
- },
157
- () => api.markRead(ids)
158
- );
159
- }
160
- __name(markRead, "markRead");
161
- function remove(cell, api, id) {
162
- const target = cell.state.items.find((item) => item.id === id);
163
- if (!target) return;
164
- const items = cell.state.items.filter((item) => item.id !== id);
165
- write(
166
- cell,
167
- api,
168
- () => {
169
- patch(cell, { items });
170
- if (target.readAt === null) bumpUnread(cell, -1);
171
- },
172
- () => api.remove([id])
173
- );
174
- }
175
- __name(remove, "remove");
176
- function createInboxStore(api) {
177
- const cell = { state: EMPTY, listeners: /* @__PURE__ */ new Set(), request: 0 };
178
- return {
179
- getState: /* @__PURE__ */ __name(() => cell.state, "getState"),
180
- subscribe(listener) {
181
- cell.listeners.add(listener);
182
- return () => cell.listeners.delete(listener);
183
- },
184
- open() {
185
- if (cell.state.status === "idle") reloadList(cell, api);
186
- },
187
- refreshBadge: /* @__PURE__ */ __name(() => refreshBadge(cell, api), "refreshBadge"),
188
- invalidate: /* @__PURE__ */ __name(() => invalidate(cell, api), "invalidate"),
189
- loadMore: /* @__PURE__ */ __name(() => loadMore(cell, api), "loadMore"),
190
- markRead: /* @__PURE__ */ __name((ids) => markRead(cell, api, ids), "markRead"),
191
- markAllRead() {
192
- const readAt = (/* @__PURE__ */ new Date()).toISOString();
193
- const items = cell.state.items.map((item) => ({ ...item, readAt: item.readAt ?? readAt }));
194
- write(
195
- cell,
196
- api,
197
- () => patch(cell, { items, unread: 0 }),
198
- () => api.markAllRead()
199
- );
200
- },
201
- remove: /* @__PURE__ */ __name((id) => remove(cell, api, id), "remove")
202
- };
203
- }
204
- __name(createInboxStore, "createInboxStore");
205
-
206
- // src/react/hooks.ts
207
- import { useEffect, useSyncExternalStore } from "react";
208
- function useInboxState(store) {
209
- return useSyncExternalStore(store.subscribe, store.getState, store.getState);
210
- }
211
- __name(useInboxState, "useInboxState");
212
- function useUnreadCount(store, options = {}) {
213
- const enabled = options.enabled ?? true;
214
- const subscribe = options.subscribe;
215
- const { unread } = useInboxState(store);
216
- options.useSignal?.(() => {
217
- if (enabled) store.invalidate();
218
- });
219
- useEffect(() => {
220
- if (!enabled) return;
221
- store.refreshBadge();
222
- const unsubscribe = subscribe?.(() => store.invalidate());
223
- const interval = setInterval(
224
- () => store.refreshBadge(),
225
- subscribe ? BADGE_RECONCILE_MS : BADGE_POLL_MS
226
- );
227
- const onFocus = /* @__PURE__ */ __name(() => store.refreshBadge(), "onFocus");
228
- globalThis.addEventListener?.("focus", onFocus);
229
- return () => {
230
- clearInterval(interval);
231
- globalThis.removeEventListener?.("focus", onFocus);
232
- unsubscribe?.();
233
- };
234
- }, [store, enabled, subscribe]);
235
- return enabled ? unread : 0;
236
- }
237
- __name(useUnreadCount, "useUnreadCount");
238
- function useInboxList(store, open) {
239
- const state = useInboxState(store);
240
- useEffect(() => {
241
- if (open) store.open();
242
- }, [store, open]);
243
- return state;
244
- }
245
- __name(useInboxList, "useInboxList");
246
-
247
- // src/react/relative-time.ts
248
- function relativeTime(iso, messages) {
249
- const elapsedMs = Date.now() - new Date(iso).getTime();
250
- const minutes = Math.round(elapsedMs / 6e4);
251
- if (minutes < 1) return messages.justNow;
252
- if (minutes < 60) return messages.minutesAgo(minutes);
253
- const hours = Math.round(minutes / 60);
254
- if (hours < 24) return messages.hoursAgo(hours);
255
- const days = Math.round(hours / 24);
256
- if (days < 7) return messages.daysAgo(days);
257
- return new Date(iso).toLocaleDateString(messages.dateLocale);
258
- }
259
- __name(relativeTime, "relativeTime");
260
-
261
- // src/react/web-push-client.ts
262
- function base64UrlToUint8Array(base64Url) {
263
- const padding = "=".repeat((4 - base64Url.length % 4) % 4);
264
- const base64 = (base64Url + padding).replaceAll("-", "+").replaceAll("_", "/");
265
- const raw = atob(base64);
266
- return Uint8Array.from(raw, (char) => char.charCodeAt(0));
267
- }
268
- __name(base64UrlToUint8Array, "base64UrlToUint8Array");
269
- function pushSupported() {
270
- return typeof navigator !== "undefined" && "serviceWorker" in navigator && typeof window !== "undefined" && "PushManager" in window && "Notification" in window;
271
- }
272
- __name(pushSupported, "pushSupported");
273
- async function getExistingPushSubscription() {
274
- if (!pushSupported()) return null;
275
- const registration = await navigator.serviceWorker.getRegistration();
276
- if (!registration) return null;
277
- return registration.pushManager.getSubscription();
278
- }
279
- __name(getExistingPushSubscription, "getExistingPushSubscription");
280
- async function obtainSubscription(swPath, vapidPublicKey) {
281
- const registration = await navigator.serviceWorker.register(swPath);
282
- await navigator.serviceWorker.ready;
283
- return await registration.pushManager.getSubscription() ?? registration.pushManager.subscribe({
284
- userVisibleOnly: true,
285
- applicationServerKey: base64UrlToUint8Array(vapidPublicKey)
286
- });
287
- }
288
- __name(obtainSubscription, "obtainSubscription");
289
- async function persist(api, subscription) {
290
- const json = subscription.toJSON();
291
- if (!json.endpoint || !json.keys?.p256dh || !json.keys.auth) {
292
- return { ok: false, reason: "error" };
293
- }
294
- const saved = await api.savePushSubscription({
295
- endpoint: json.endpoint,
296
- keys: { p256dh: json.keys.p256dh, auth: json.keys.auth }
297
- });
298
- return saved.ok ? { ok: true } : { ok: false, reason: "error" };
299
- }
300
- __name(persist, "persist");
301
- async function enableWebPush(api, swPath = "/sw.js") {
302
- if (!pushSupported()) return { ok: false, reason: "unsupported" };
303
- const registration = await api.getPushRegistration().catch(() => null);
304
- if (!registration?.vapidPublicKey) return { ok: false, reason: "unconfigured" };
305
- const permission = await Notification.requestPermission();
306
- if (permission !== "granted") return { ok: false, reason: "permission-denied" };
307
- try {
308
- return await persist(
309
- api,
310
- await obtainSubscription(swPath, registration.vapidPublicKey)
311
- );
312
- } catch {
313
- return { ok: false, reason: "error" };
314
- }
315
- }
316
- __name(enableWebPush, "enableWebPush");
317
- async function disableWebPush(api) {
318
- const subscription = await getExistingPushSubscription();
319
- if (!subscription) return;
320
- const endpoint = subscription.endpoint;
321
- await subscription.unsubscribe().catch(() => false);
322
- await api.removePushSubscription(endpoint).catch(() => void 0);
323
- }
324
- __name(disableWebPush, "disableWebPush");
325
-
326
- // src/react/transport.ts
327
- var NotificationsHttpError = class _NotificationsHttpError extends Error {
328
- static {
329
- __name(this, "NotificationsHttpError");
330
- }
331
- status;
332
- constructor(status, message) {
333
- super(message);
334
- this.name = "NotificationsHttpError";
335
- this.status = status;
336
- Object.setPrototypeOf(this, _NotificationsHttpError.prototype);
337
- }
338
- };
339
- function httpNotificationsTransport(fallbackError) {
340
- return {
341
- async get(path) {
342
- const response = await fetch(path, {
343
- credentials: "same-origin",
344
- headers: { Accept: "application/json" }
345
- });
346
- const payload = await response.json().catch(() => null);
347
- if (!response.ok) {
348
- throw new NotificationsHttpError(
349
- response.status,
350
- payload?.error ?? `HTTP ${response.status} for ${path}`
351
- );
352
- }
353
- return payload?.data ?? payload;
354
- },
355
- async send(path, method, body) {
356
- try {
357
- const response = await fetch(path, {
358
- method,
359
- credentials: "same-origin",
360
- headers: {
361
- Accept: "application/json",
362
- ...body === void 0 ? {} : { "Content-Type": "application/json" }
363
- },
364
- ...body === void 0 ? {} : { body: JSON.stringify(body) }
365
- });
366
- if (response.status === 204) return { ok: true, data: void 0 };
367
- const payload = await response.json().catch(() => null);
368
- if (!response.ok) return { ok: false, error: payload?.error ?? fallbackError };
369
- return { ok: true, data: payload?.data ?? payload };
370
- } catch {
371
- return { ok: false, error: fallbackError };
372
- }
373
- }
374
- };
375
- }
376
- __name(httpNotificationsTransport, "httpNotificationsTransport");
377
-
378
- // src/react/create-web-notifications.tsx
379
- import { useState as useState3 } from "react";
380
-
381
- // src/react/bell-button.tsx
382
- import { Badge } from "@12-apps/ui/data-display/Badge";
383
- import { Box as Box2 } from "@12-apps/ui/mui/Box";
384
- import { jsx as jsx2 } from "react/jsx-runtime";
385
- var triggerSx = {
386
- display: "inline-flex",
387
- alignItems: "center",
388
- justifyContent: "center",
389
- p: 0.5,
390
- border: "none",
391
- background: "none",
392
- cursor: "pointer",
393
- color: "text.primary",
394
- lineHeight: 0,
395
- "& *": { cursor: "pointer" },
396
- "&:hover": { color: "primary.main" },
397
- "&:focus-visible": {
398
- outline: "2px solid",
399
- outlineColor: "primary.main",
400
- outlineOffset: "2px",
401
- borderRadius: "50%"
402
- }
403
- };
404
- function BellButton({
405
- onClick,
406
- enabled = true,
407
- store,
408
- messages,
409
- subscribe,
410
- useSignal
411
- }) {
412
- const count = useUnreadCount(store, {
413
- enabled,
414
- ...subscribe ? { subscribe } : {},
415
- ...useSignal ? { useSignal } : {}
416
- });
417
- return /* @__PURE__ */ jsx2(
418
- Box2,
419
- {
420
- component: "button",
421
- type: "button",
422
- onClick,
423
- "aria-label": count > 0 ? messages.openBellWithUnread(count) : messages.openBell,
424
- "data-testid": "notifications-bell",
425
- sx: triggerSx,
426
- children: /* @__PURE__ */ jsx2(
427
- Badge,
428
- {
429
- content: count > 0 ? count : void 0,
430
- color: "primary",
431
- variant: "count",
432
- max: 99,
433
- "data-testid": "notifications-badge",
434
- children: /* @__PURE__ */ jsx2(BellIcon, { size: 28 })
435
- }
436
- )
437
- }
438
- );
439
- }
440
- __name(BellButton, "BellButton");
441
-
442
- // src/react/panel.tsx
443
- import { useCallback } from "react";
444
- import { EmptyState } from "@12-apps/ui/data-display/EmptyState";
445
- import { LoadingState } from "@12-apps/ui/data-display/LoadingState";
446
- import { Button as Button2 } from "@12-apps/ui/form/Button";
447
- import { Drawer, DrawerContent, DrawerHeader } from "@12-apps/ui/layout/Drawer";
448
- import { Box as Box4 } from "@12-apps/ui/mui/Box";
449
- import { useMediaQuery } from "@12-apps/ui/mui/useMediaQuery";
450
- import { useTheme } from "@12-apps/ui/mui/styles";
451
-
452
- // src/react/row.tsx
453
- import { Button } from "@12-apps/ui/form/Button";
454
- import { Box as Box3 } from "@12-apps/ui/mui/Box";
455
- import { alpha } from "@12-apps/ui/mui/styles";
456
- import { Text } from "@12-apps/ui/typography/Text";
457
- import { jsx as jsx3, jsxs as jsxs2 } from "react/jsx-runtime";
458
- var contentButtonSx = {
459
- flex: 1,
460
- minWidth: 0,
461
- display: "flex",
462
- flexDirection: "column",
463
- gap: 0.25,
464
- textAlign: "left",
465
- border: "none",
466
- background: "none",
467
- p: 0,
468
- cursor: "pointer",
469
- color: "text.primary",
470
- fontFamily: "inherit"
471
- };
472
- var unreadDotSx = {
473
- width: 8,
474
- height: 8,
475
- borderRadius: "50%",
476
- bgcolor: "primary.main",
477
- flex: "0 0 auto"
478
- };
479
- function NotificationRow({
480
- notification,
481
- messages,
482
- onOpen,
483
- onDelete
484
- }) {
485
- const unread = notification.readAt === null;
486
- return /* @__PURE__ */ jsxs2(
487
- Box3,
488
- {
489
- "data-testid": `notification-${notification.id}`,
490
- sx: {
491
- display: "flex",
492
- alignItems: "flex-start",
493
- gap: 1,
494
- py: 1.5,
495
- px: 1,
496
- borderBottom: "1px solid",
497
- borderColor: "divider",
498
- bgcolor: unread ? (t) => alpha(t.palette.primary.main, 0.06) : "transparent"
499
- },
500
- children: [
501
- /* @__PURE__ */ jsxs2(
502
- Box3,
503
- {
504
- component: "button",
505
- type: "button",
506
- onClick: () => onOpen(notification),
507
- "aria-label": unread ? `${notification.title} (${messages.unreadSuffix})` : notification.title,
508
- sx: contentButtonSx,
509
- children: [
510
- /* @__PURE__ */ jsxs2(Box3, { sx: { display: "flex", alignItems: "center", gap: 0.75 }, children: [
511
- unread ? /* @__PURE__ */ jsx3(Box3, { "aria-hidden": true, sx: unreadDotSx }) : null,
512
- /* @__PURE__ */ jsx3(Text, { variant: "body", size: "sm", weight: unread ? "bold" : "medium", as: "span", children: notification.title })
513
- ] }),
514
- /* @__PURE__ */ jsx3(Text, { variant: "caption", size: "xs", color: "secondary", as: "span", children: notification.body }),
515
- /* @__PURE__ */ jsx3(Text, { variant: "caption", size: "xs", color: "secondary", as: "span", italic: true, children: relativeTime(notification.createdAt, messages) })
516
- ]
517
- }
518
- ),
519
- /* @__PURE__ */ jsx3(
520
- Button,
521
- {
522
- variant: "ghost",
523
- color: "neutral",
524
- size: "xs",
525
- "aria-label": messages.deleteOne(notification.title),
526
- onClick: () => onDelete(notification.id),
527
- dataTestId: `notification-delete-${notification.id}`,
528
- children: "\u2715"
529
- }
530
- )
531
- ]
532
- }
533
- );
534
- }
535
- __name(NotificationRow, "NotificationRow");
536
-
537
- // src/react/panel.tsx
538
- import { jsx as jsx4, jsxs as jsxs3 } from "react/jsx-runtime";
539
- function PanelBody({
540
- state,
541
- messages,
542
- onRetry,
543
- onLoadMore,
544
- onOpen,
545
- onDelete
546
- }) {
547
- if (state.status === "pending" || state.status === "idle") {
548
- return /* @__PURE__ */ jsx4(
549
- LoadingState,
550
- {
551
- variant: "spinner",
552
- message: messages.loading,
553
- size: "md",
554
- dataTestId: "notifications-loading"
555
- }
556
- );
557
- }
558
- if (state.status === "error") {
559
- return /* @__PURE__ */ jsx4(
560
- EmptyState,
561
- {
562
- variant: "minimal",
563
- title: messages.loadFailedTitle,
564
- description: messages.loadFailedBody,
565
- onRefresh: onRetry,
566
- refreshLabel: messages.retry,
567
- dataTestId: "notifications-error"
568
- }
569
- );
570
- }
571
- if (state.items.length === 0) {
572
- return /* @__PURE__ */ jsx4(
573
- EmptyState,
574
- {
575
- variant: "illustrated",
576
- illustration: /* @__PURE__ */ jsx4(BellIcon, { size: 44, dim: true }),
577
- title: messages.emptyTitle,
578
- description: messages.emptyBody,
579
- dataTestId: "notifications-empty"
580
- }
581
- );
582
- }
583
- return /* @__PURE__ */ jsxs3(Box4, { children: [
584
- state.items.map((notification) => /* @__PURE__ */ jsx4(
585
- NotificationRow,
586
- {
587
- notification,
588
- messages,
589
- onOpen,
590
- onDelete
591
- },
592
- notification.id
593
- )),
594
- state.nextCursor ? /* @__PURE__ */ jsx4(Box4, { sx: { display: "flex", justifyContent: "center", py: 1.5 }, children: /* @__PURE__ */ jsx4(
595
- Button2,
596
- {
597
- variant: "outline",
598
- color: "neutral",
599
- size: "sm",
600
- disabled: state.loadingMore,
601
- onClick: onLoadMore,
602
- dataTestId: "notifications-load-more",
603
- children: state.loadingMore ? messages.loadingMore : messages.loadMore
604
- }
605
- ) }) : null
606
- ] });
607
- }
608
- __name(PanelBody, "PanelBody");
609
- function NotificationsPanel({
610
- open,
611
- onClose,
612
- onNavigate,
613
- store,
614
- messages
615
- }) {
616
- const theme = useTheme();
617
- const isMobile = useMediaQuery(theme.breakpoints.down("sm"));
618
- const state = useInboxList(store, open);
619
- const openNotification = useCallback(
620
- (notification) => {
621
- if (notification.readAt === null) store.markRead([notification.id]);
622
- if (notification.link && onNavigate) {
623
- onClose();
624
- onNavigate(notification.link);
625
- }
626
- },
627
- [store, onClose, onNavigate]
628
- );
629
- const hasUnread = state.items.some((item) => item.readAt === null);
630
- return /* @__PURE__ */ jsxs3(
631
- Drawer,
632
- {
633
- open,
634
- onClose,
635
- anchor: "right",
636
- variant: "right",
637
- width: isMobile ? "100vw" : 400,
638
- dataTestId: "notifications-panel",
639
- children: [
640
- /* @__PURE__ */ jsx4(DrawerHeader, { onClose, children: messages.panelTitle }),
641
- /* @__PURE__ */ jsxs3(DrawerContent, { children: [
642
- hasUnread ? /* @__PURE__ */ jsx4(Box4, { sx: { display: "flex", justifyContent: "flex-end", pb: 1 }, children: /* @__PURE__ */ jsx4(
643
- Button2,
644
- {
645
- variant: "ghost",
646
- color: "primary",
647
- size: "xs",
648
- onClick: () => store.markAllRead(),
649
- dataTestId: "notifications-mark-all-read",
650
- children: messages.markAllRead
651
- }
652
- ) }) : null,
653
- /* @__PURE__ */ jsx4(
654
- PanelBody,
655
- {
656
- state,
657
- messages,
658
- onRetry: () => store.invalidate(),
659
- onLoadMore: () => store.loadMore(),
660
- onOpen: openNotification,
661
- onDelete: (id) => store.remove(id)
662
- }
663
- )
664
- ] })
665
- ]
666
- }
667
- );
668
- }
669
- __name(NotificationsPanel, "NotificationsPanel");
670
-
671
- // src/react/preferences-screen.tsx
672
- import { useCallback as useCallback2, useEffect as useEffect3, useState as useState2 } from "react";
673
- import { LoadingState as LoadingState2 } from "@12-apps/ui/data-display/LoadingState";
674
- import { Switch } from "@12-apps/ui/form/Switch";
675
- import { Box as Box6 } from "@12-apps/ui/mui/Box";
676
- import { Text as Text3 } from "@12-apps/ui/typography/Text";
677
-
678
- // src/react/web-push-setup.tsx
679
- import { useEffect as useEffect2, useState } from "react";
680
- import { Button as Button3 } from "@12-apps/ui/form/Button";
681
- import { Box as Box5 } from "@12-apps/ui/mui/Box";
682
- import { Text as Text2 } from "@12-apps/ui/typography/Text";
683
- import { jsx as jsx5, jsxs as jsxs4 } from "react/jsx-runtime";
684
- var cardSx = {
685
- p: 2,
686
- border: "1px solid",
687
- borderColor: "divider",
688
- borderRadius: 2
689
- };
690
- function InstallFirstHint({ hint }) {
691
- return /* @__PURE__ */ jsxs4(Box5, { sx: cardSx, "data-testid": "web-push-install-hint", children: [
692
- /* @__PURE__ */ jsx5(Text2, { variant: "body", size: "sm", weight: "semibold", as: "p", children: hint.title }),
693
- /* @__PURE__ */ jsx5(Text2, { variant: "caption", size: "xs", color: "secondary", as: "p", children: hint.body })
694
- ] });
695
- }
696
- __name(InstallFirstHint, "InstallFirstHint");
697
- async function resolveState(api) {
698
- const subscription = await getExistingPushSubscription();
699
- if (!subscription) return "idle";
700
- const registration = await api.getPushRegistration({ endpoint: subscription.endpoint }).catch(() => null);
701
- return registration?.registered ? "on" : "idle";
702
- }
703
- __name(resolveState, "resolveState");
704
- function statusText(state, messages) {
705
- if (state === "on") return messages.devicePushOn;
706
- if (state === "denied") return messages.devicePushDenied;
707
- if (state === "failed") return messages.devicePushFailed;
708
- return messages.devicePushIdle;
709
- }
710
- __name(statusText, "statusText");
711
- function WebPushDeviceSetup({
712
- available,
713
- api,
714
- messages,
715
- config
716
- }) {
717
- const [state, setState] = useState("checking");
718
- const [needsInstall] = useState(() => config.needsInstallFirst?.() ?? false);
719
- useEffect2(() => {
720
- let cancelled = false;
721
- void resolveState(api).then((next) => {
722
- if (!cancelled) setState(next);
723
- });
724
- return () => {
725
- cancelled = true;
726
- };
727
- }, [api]);
728
- if (!available) return null;
729
- if (needsInstall && config.installHint) return /* @__PURE__ */ jsx5(InstallFirstHint, { hint: config.installHint });
730
- const enable = /* @__PURE__ */ __name(async () => {
731
- setState("busy");
732
- const result = await enableWebPush(api, config.swPath);
733
- if (result.ok) setState("on");
734
- else setState(result.reason === "permission-denied" ? "denied" : "failed");
735
- }, "enable");
736
- return /* @__PURE__ */ jsxs4(
737
- Box5,
738
- {
739
- sx: {
740
- ...cardSx,
741
- display: "flex",
742
- alignItems: "center",
743
- justifyContent: "space-between",
744
- gap: 2
745
- },
746
- "data-testid": "web-push-device-setup",
747
- children: [
748
- /* @__PURE__ */ jsxs4(Box5, { sx: { minWidth: 0 }, children: [
749
- /* @__PURE__ */ jsx5(Text2, { variant: "body", size: "sm", weight: "semibold", as: "p", children: messages.devicePushTitle }),
750
- /* @__PURE__ */ jsx5(Text2, { variant: "caption", size: "xs", color: "secondary", as: "p", children: statusText(state, messages) })
751
- ] }),
752
- state !== "on" ? /* @__PURE__ */ jsx5(
753
- Button3,
754
- {
755
- variant: "outline",
756
- color: "primary",
757
- size: "sm",
758
- disabled: state === "busy" || state === "checking",
759
- onClick: () => void enable(),
760
- dataTestId: "web-push-enable",
761
- children: state === "busy" ? messages.devicePushEnabling : messages.devicePushEnable
762
- }
763
- ) : null
764
- ]
765
- }
766
- );
767
- }
768
- __name(WebPushDeviceSetup, "WebPushDeviceSetup");
769
-
770
- // src/react/preferences-screen.tsx
771
- import { jsx as jsx6, jsxs as jsxs5 } from "react/jsx-runtime";
772
- function CategoryCard({
773
- category,
774
- channels,
775
- availability,
776
- messages,
777
- onToggle
778
- }) {
779
- const labels = messages.categoryLabels[category];
780
- return /* @__PURE__ */ jsxs5(
781
- Box6,
782
- {
783
- sx: { p: 2, border: "1px solid", borderColor: "divider", borderRadius: 2 },
784
- "data-testid": `prefs-${category}`,
785
- children: [
786
- /* @__PURE__ */ jsx6(Text3, { variant: "body", size: "sm", weight: "semibold", as: "p", children: labels?.title ?? messages.categoryFallbackTitle(category) }),
787
- labels?.description ? /* @__PURE__ */ jsx6(Text3, { variant: "caption", size: "xs", color: "secondary", as: "p", children: labels.description }) : null,
788
- /* @__PURE__ */ jsx6(
789
- Box6,
790
- {
791
- sx: {
792
- mt: 1.5,
793
- display: "grid",
794
- gridTemplateColumns: { xs: "1fr 1fr", sm: "repeat(4, 1fr)" },
795
- gap: 1
796
- },
797
- children: NOTIFICATION_CHANNELS.map((channel) => /* @__PURE__ */ jsx6(
798
- Switch,
799
- {
800
- size: "sm",
801
- color: "primary",
802
- label: messages.channelLabels[channel] ?? channel,
803
- checked: channels[channel] && availability[channel],
804
- disabled: !availability[channel],
805
- onChange: (_, checked) => onToggle(channel, checked),
806
- dataTestId: `prefs-${category}-${channel}`
807
- },
808
- channel
809
- ))
810
- }
811
- )
812
- ]
813
- }
814
- );
815
- }
816
- __name(CategoryCard, "CategoryCard");
817
- function UnavailableHints({
818
- availability,
819
- messages
820
- }) {
821
- const unavailable = NOTIFICATION_CHANNELS.filter((channel) => !availability[channel]);
822
- if (unavailable.length === 0) return null;
823
- return /* @__PURE__ */ jsx6(Box6, { sx: { display: "flex", flexDirection: "column", gap: 0.5 }, children: unavailable.map((channel) => /* @__PURE__ */ jsxs5(
824
- Text3,
825
- {
826
- variant: "caption",
827
- size: "xs",
828
- color: "secondary",
829
- as: "p",
830
- "data-testid": `prefs-hint-${channel}`,
831
- children: [
832
- messages.channelLabels[channel] ?? channel,
833
- ":",
834
- " ",
835
- messages.channelUnavailableHints[channel] ?? ""
836
- ]
837
- },
838
- channel
839
- )) });
840
- }
841
- __name(UnavailableHints, "UnavailableHints");
842
- function usePreferences(api) {
843
- const [payload, setPayload] = useState2(null);
844
- useEffect3(() => {
845
- let cancelled = false;
846
- void api.getPreferences().then((next) => {
847
- if (!cancelled) setPayload(next);
848
- }).catch(() => void 0);
849
- return () => {
850
- cancelled = true;
851
- };
852
- }, [api]);
853
- const toggle = useCallback2(
854
- (category, channel, enabled) => {
855
- setPayload((current) => {
856
- const row = current?.preferences[category];
857
- if (!current || !row) return current;
858
- return {
859
- ...current,
860
- preferences: { ...current.preferences, [category]: { ...row, [channel]: enabled } }
861
- };
862
- });
863
- const reconcile = /* @__PURE__ */ __name(() => {
864
- void api.getPreferences().then(setPayload).catch(() => void 0);
865
- }, "reconcile");
866
- void api.savePreference(category, channel, enabled).then((result) => {
867
- if (result.ok) setPayload(result.data);
868
- else reconcile();
869
- }).catch(reconcile);
870
- },
871
- [api]
872
- );
873
- return { payload, toggle };
874
- }
875
- __name(usePreferences, "usePreferences");
876
- function PreferencesScreen({
877
- footer,
878
- api,
879
- messages,
880
- webPush
881
- }) {
882
- const { payload, toggle } = usePreferences(api);
883
- if (!payload) {
884
- return /* @__PURE__ */ jsx6(
885
- LoadingState2,
886
- {
887
- variant: "spinner",
888
- message: messages.loadingMore,
889
- size: "md",
890
- dataTestId: "notification-prefs-loading"
891
- }
892
- );
893
- }
894
- const { preferences, availability, categories } = payload;
895
- return /* @__PURE__ */ jsxs5(
896
- Box6,
897
- {
898
- component: "section",
899
- "data-testid": "notification-prefs-view",
900
- sx: {
901
- maxWidth: 640,
902
- mx: "auto",
903
- width: "100%",
904
- px: 2,
905
- py: 4,
906
- display: "flex",
907
- flexDirection: "column",
908
- gap: 2
909
- },
910
- children: [
911
- /* @__PURE__ */ jsxs5(Box6, { children: [
912
- /* @__PURE__ */ jsx6(Text3, { variant: "heading", size: "lg", as: "h1", children: messages.preferencesTitle }),
913
- /* @__PURE__ */ jsxs5(Text3, { variant: "caption", size: "sm", color: "secondary", as: "p", children: [
914
- messages.preferencesLead,
915
- " ",
916
- footer
917
- ] })
918
- ] }),
919
- /* @__PURE__ */ jsx6(
920
- WebPushDeviceSetup,
921
- {
922
- available: availability.WEB_PUSH,
923
- api,
924
- messages,
925
- config: webPush
926
- }
927
- ),
928
- categories.map((category) => /* @__PURE__ */ jsx6(
929
- CategoryCard,
930
- {
931
- category,
932
- channels: preferences[category] ?? {
933
- EMAIL: false,
934
- SMS: false,
935
- WHATSAPP: false,
936
- WEB_PUSH: false
937
- },
938
- availability,
939
- messages,
940
- onToggle: (channel, enabled) => toggle(category, channel, enabled)
941
- },
942
- category
943
- )),
944
- /* @__PURE__ */ jsx6(UnavailableHints, { availability, messages })
945
- ]
946
- }
947
- );
948
- }
949
- __name(PreferencesScreen, "PreferencesScreen");
950
-
951
- // src/react/create-web-notifications.tsx
952
- import { Fragment, jsx as jsx7, jsxs as jsxs6 } from "react/jsx-runtime";
953
- function createWebNotifications(config) {
954
- const messages = messagesOf(config);
955
- const api = createNotificationsApiClient(
956
- config.apiBase,
957
- config.transport ?? httpNotificationsTransport(messages.operationFailed)
958
- );
959
- const store = createInboxStore(api);
960
- const webPush = config.webPush ?? {};
961
- const subscribe = config.subscribe;
962
- const subscribeOption = {
963
- ...subscribe ? { subscribe } : {},
964
- ...config.useSignal ? { useSignal: config.useSignal } : {}
965
- };
966
- const Bell = /* @__PURE__ */ __name((props) => /* @__PURE__ */ jsx7(BellButton, { ...props, store, messages, ...subscribeOption }), "Bell");
967
- const Panel = /* @__PURE__ */ __name((props) => /* @__PURE__ */ jsx7(NotificationsPanel, { ...props, store, messages }), "Panel");
968
- function useBoundUnreadCount(options = {}) {
969
- return useUnreadCount(store, { ...options, ...subscribeOption });
970
- }
971
- __name(useBoundUnreadCount, "useBoundUnreadCount");
972
- function BellWithPanel({
973
- enabled = true,
974
- onNavigate
975
- }) {
976
- const [open, setOpen] = useState3(false);
977
- return /* @__PURE__ */ jsxs6(Fragment, { children: [
978
- /* @__PURE__ */ jsx7(Bell, { enabled, onClick: () => setOpen(true) }),
979
- /* @__PURE__ */ jsx7(
980
- Panel,
981
- {
982
- open,
983
- onClose: () => setOpen(false),
984
- ...onNavigate ? { onNavigate } : {}
985
- }
986
- )
987
- ] });
988
- }
989
- __name(BellWithPanel, "BellWithPanel");
990
- return {
991
- page: /* @__PURE__ */ __name((props) => /* @__PURE__ */ jsx7(PreferencesScreen, { ...props, api, messages, webPush }), "page"),
992
- BellButton: Bell,
993
- Panel,
994
- BellWithPanel,
995
- useUnreadCount: useBoundUnreadCount,
996
- store,
997
- api,
998
- messages
999
- };
1000
- }
1001
- __name(createWebNotifications, "createWebNotifications");
1002
-
1003
- export {
1004
- createNotificationsApiClient,
1005
- BellIcon,
1006
- PAGE_SIZE,
1007
- BADGE_POLL_MS,
1008
- BADGE_RECONCILE_MS,
1009
- createInboxStore,
1010
- useInboxState,
1011
- useUnreadCount,
1012
- useInboxList,
1013
- relativeTime,
1014
- pushSupported,
1015
- getExistingPushSubscription,
1016
- enableWebPush,
1017
- disableWebPush,
1018
- NotificationsHttpError,
1019
- httpNotificationsTransport,
1020
- createWebNotifications
1021
- };
1022
- //# sourceMappingURL=chunk-6HLHQDKS.js.map