@12-apps/notifications 4.6.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.
- package/dist/chunk-BW723CX2.js +214 -0
- package/dist/chunk-BW723CX2.js.map +1 -0
- package/dist/chunk-CQZMTFPY.js +76 -0
- package/dist/chunk-CQZMTFPY.js.map +1 -0
- package/dist/{chunk-WU6QJLSZ.js → chunk-CUZW62JS.js} +2 -2
- package/dist/{chunk-AHNRSA6U.js → chunk-HHMRCMQU.js} +2 -2
- package/dist/chunk-M2TVBVH2.js +15 -0
- package/dist/chunk-M2TVBVH2.js.map +1 -0
- package/dist/chunk-MMLV4EZT.js +263 -0
- package/dist/chunk-MMLV4EZT.js.map +1 -0
- package/dist/chunk-O5BVUXPO.js +22 -0
- package/dist/chunk-O5BVUXPO.js.map +1 -0
- package/dist/{chunk-ORXJH3VM.js → chunk-WHBMPHQE.js} +28 -18
- package/dist/chunk-WHBMPHQE.js.map +1 -0
- package/dist/{chunk-4TTYQVPK.js → chunk-XE7HZVMH.js} +2 -9
- package/dist/chunk-XE7HZVMH.js.map +1 -0
- package/dist/{create-api-notifications-CgBdjfyF.d.ts → create-api-notifications-B3u6Kx3x.d.ts} +28 -5
- package/dist/{create-web-notifications-Du3hTs7P.d.ts → create-web-notifications-BHCzaU2y.d.ts} +14 -3
- package/dist/hono/index.d.ts +2 -2
- package/dist/hono/index.js +4 -3
- package/dist/hono/index.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +5 -3
- package/dist/{jobs-DhDjrAX5.d.ts → jobs-BDZ7aGHV.d.ts} +1 -1
- package/dist/manifest/server.d.ts +3 -3
- package/dist/manifest/server.js +5 -4
- package/dist/manifest/server.js.map +1 -1
- package/dist/manifest/web.d.ts +2 -2
- package/dist/manifest/web.js +3 -2
- package/dist/manifest/web.js.map +1 -1
- package/dist/panel-UFXNO4AF.js +243 -0
- package/dist/panel-UFXNO4AF.js.map +1 -0
- package/dist/preferences-screen-IOW6Y2H2.js +294 -0
- package/dist/preferences-screen-IOW6Y2H2.js.map +1 -0
- package/dist/react/index.d.ts +3 -3
- package/dist/react/index.js +17 -11
- package/dist/server/index.d.ts +4 -4
- package/dist/server/index.js +5 -4
- package/dist/{wire-5IRin4zH.d.ts → wire-6dzyfDE7.d.ts} +31 -5
- package/package.json +2 -2
- package/src/messages.ts +37 -4
- package/src/react/create-web-notifications.tsx +19 -10
- package/src/react/page-lazy.tsx +73 -0
- package/src/react/panel-lazy.tsx +74 -0
- package/src/server/context.ts +10 -0
- package/src/server/create-api-notifications.ts +30 -6
- package/src/server/routes.ts +32 -16
- package/dist/chunk-4TTYQVPK.js.map +0 -1
- package/dist/chunk-ORXJH3VM.js.map +0 -1
- package/dist/chunk-YE24MDS6.js +0 -1022
- package/dist/chunk-YE24MDS6.js.map +0 -1
- /package/dist/{chunk-WU6QJLSZ.js.map → chunk-CUZW62JS.js.map} +0 -0
- /package/dist/{chunk-AHNRSA6U.js.map → chunk-HHMRCMQU.js.map} +0 -0
package/dist/{create-api-notifications-CgBdjfyF.d.ts → create-api-notifications-B3u6Kx3x.d.ts}
RENAMED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { N as NotificationGeneratorRegistry } from './generators-B9xt3sRh.js';
|
|
2
|
-
import { b as NotificationRow, C as ChannelMatrix, a as ChannelRow, L as ListNotificationsResult, c as NotificationWireMessages } from './wire-
|
|
2
|
+
import { b as NotificationRow, C as ChannelMatrix, a as ChannelRow, L as ListNotificationsResult, h as NotificationsCopySource, c as NotificationWireMessages } from './wire-6dzyfDE7.js';
|
|
3
3
|
import { b as NotificationChannel, D as DeliveryStatus, f as NotificationLogger, i as NotificationTransport, c as NotificationContent, a as NotificationCategory, d as NotificationEvent, e as NotificationGenerator } from './types-CXLAG3UU.js';
|
|
4
4
|
import { e as WebPushSubscriptionSource, D as DriverDeclarationBase, a as WebPushDriverDeclaration, c as WebPushSender } from './web-push-Cs14Wp9u.js';
|
|
5
5
|
|
|
@@ -753,6 +753,16 @@ interface NotificationsRequest {
|
|
|
753
753
|
body?: unknown;
|
|
754
754
|
/** Headers the surface reads (`user-agent`, for the device hint). */
|
|
755
755
|
headers?: Record<string, string | undefined>;
|
|
756
|
+
/**
|
|
757
|
+
* The language to answer this caller in, as a language tag (`pt-BR`,
|
|
758
|
+
* `en-US`) — the same field `@12-apps/wiring`'s `WireRequest` carries.
|
|
759
|
+
*
|
|
760
|
+
* Populated by the host's adapter, which is the only layer that can
|
|
761
|
+
* negotiate one. Absent is meaningful and not an error: a host with one
|
|
762
|
+
* audience never sets it, and this surface must then answer with the words
|
|
763
|
+
* it was configured with rather than invent a language.
|
|
764
|
+
*/
|
|
765
|
+
locale?: string;
|
|
756
766
|
}
|
|
757
767
|
/** What a handler answers with; the adapter maps it onto its response type. */
|
|
758
768
|
interface NotificationsResponse {
|
|
@@ -874,8 +884,15 @@ interface NotificationsServerConfig {
|
|
|
874
884
|
onInboxChanged?: (userId: string) => void;
|
|
875
885
|
/** The host's authorization engine, for `notifyByPermission`. */
|
|
876
886
|
audience?: NotificationAudienceDirectory;
|
|
877
|
-
/**
|
|
878
|
-
|
|
887
|
+
/**
|
|
888
|
+
* Every user-facing sentence this surface can produce — REQUIRED host config.
|
|
889
|
+
*
|
|
890
|
+
* A pack, or a RESOLVER for a host whose callers do not share a language.
|
|
891
|
+
* Passed to the routes UNRESOLVED: this factory runs once per process (and
|
|
892
|
+
* at least one host memoises its call), so resolving here would answer every
|
|
893
|
+
* later request in the language the process started with.
|
|
894
|
+
*/
|
|
895
|
+
messages: NotificationsCopySource<NotificationWireMessages>;
|
|
879
896
|
/** The host's logger. Defaults to the console. */
|
|
880
897
|
logger?: NotificationLogger;
|
|
881
898
|
}
|
|
@@ -901,8 +918,14 @@ interface ApiNotifications {
|
|
|
901
918
|
registerGenerator: NotificationGeneratorRegistry['register'];
|
|
902
919
|
/** The declared transports, for diagnostics and availability probes. */
|
|
903
920
|
transports: TransportRegistry;
|
|
904
|
-
/**
|
|
905
|
-
|
|
921
|
+
/**
|
|
922
|
+
* The copy source in force, so a host's own screens can reuse a sentence.
|
|
923
|
+
*
|
|
924
|
+
* The SOURCE rather than a resolved pack, for the same reason the routes get
|
|
925
|
+
* one: a host screen serving two readers must be able to ask per reader.
|
|
926
|
+
* Read it with `messagesOf({ messages }, locale)`.
|
|
927
|
+
*/
|
|
928
|
+
messages: NotificationsCopySource<NotificationWireMessages>;
|
|
906
929
|
}
|
|
907
930
|
declare function createApiNotifications(config: NotificationsServerConfig): ApiNotifications;
|
|
908
931
|
|
package/dist/{create-web-notifications-Du3hTs7P.d.ts → create-web-notifications-BHCzaU2y.d.ts}
RENAMED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { JSX, ComponentType } from 'react';
|
|
2
|
-
import { L as ListNotificationsResult, a as ChannelRow, I as InboxNotification, N as NotificationMessages } from './wire-
|
|
2
|
+
import { L as ListNotificationsResult, a as ChannelRow, I as InboxNotification, N as NotificationMessages } from './wire-6dzyfDE7.js';
|
|
3
3
|
import { b as NotificationChannel } from './types-CXLAG3UU.js';
|
|
4
4
|
|
|
5
5
|
/**
|
|
@@ -324,11 +324,22 @@ interface NotificationsWebConfig {
|
|
|
324
324
|
webPush?: WebPushSetupConfig;
|
|
325
325
|
}
|
|
326
326
|
interface WebNotifications {
|
|
327
|
-
/**
|
|
327
|
+
/**
|
|
328
|
+
* The routed surface: the preferences screen.
|
|
329
|
+
*
|
|
330
|
+
* Loaded on demand — see `page-lazy.tsx`. A host that mounts only the bell and
|
|
331
|
+
* the panel never downloads it, and a host that routes to it fetches it while
|
|
332
|
+
* entering that route.
|
|
333
|
+
*/
|
|
328
334
|
page: ComponentType<PreferencesScreenProps>;
|
|
329
335
|
/** The bell, already bound to the shared store. */
|
|
330
336
|
BellButton: ComponentType<BellButtonProps>;
|
|
331
|
-
/**
|
|
337
|
+
/**
|
|
338
|
+
* The inbox slide-over, sharing that store.
|
|
339
|
+
*
|
|
340
|
+
* Loaded the first time it is opened — see `panel-lazy.tsx`. Until then a
|
|
341
|
+
* host's chrome carries the bell and nothing else.
|
|
342
|
+
*/
|
|
332
343
|
Panel: ComponentType<NotificationsPanelProps>;
|
|
333
344
|
/**
|
|
334
345
|
* Bell + panel as ONE element, for a host that just wants the feature in its
|
package/dist/hono/index.d.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { Hono, Context } from 'hono';
|
|
2
|
-
import { A as ApiNotifications, N as NotificationsServerConfig, a as NotificationsActor } from '../create-api-notifications-
|
|
2
|
+
import { A as ApiNotifications, N as NotificationsServerConfig, a as NotificationsActor } from '../create-api-notifications-B3u6Kx3x.js';
|
|
3
3
|
import '../generators-B9xt3sRh.js';
|
|
4
4
|
import '../types-CXLAG3UU.js';
|
|
5
|
-
import '../wire-
|
|
5
|
+
import '../wire-6dzyfDE7.js';
|
|
6
6
|
import '../web-push-Cs14Wp9u.js';
|
|
7
7
|
|
|
8
8
|
/**
|
package/dist/hono/index.js
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import {
|
|
2
2
|
createApiNotifications
|
|
3
|
-
} from "../chunk-
|
|
4
|
-
import "../chunk-
|
|
3
|
+
} from "../chunk-WHBMPHQE.js";
|
|
4
|
+
import "../chunk-HHMRCMQU.js";
|
|
5
|
+
import "../chunk-XE7HZVMH.js";
|
|
5
6
|
import {
|
|
6
7
|
messagesOf
|
|
7
|
-
} from "../chunk-
|
|
8
|
+
} from "../chunk-M2TVBVH2.js";
|
|
8
9
|
import {
|
|
9
10
|
__name
|
|
10
11
|
} from "../chunk-7QVYU63E.js";
|
package/dist/hono/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/hono/index.ts"],"sourcesContent":["import { Hono } from 'hono';\nimport type { Context } from 'hono';\n\nimport { messagesOf } from '../messages';\n\nimport {\n createApiNotifications,\n type ApiNotifications,\n type NotificationsServerConfig,\n} from '../server/create-api-notifications';\nimport type { NotificationsActor } from '../server/context';\n\n/**\n * `@12-apps/notifications/hono` — the account notification endpoints as a\n * mountable router.\n *\n * The framework-neutral descriptors in `/server` are the contract; this is the\n * adapter for the framework we happen to use, behind its own subpath with\n * `hono` as an OPTIONAL peer (the report-builder precedent — a host on Express,\n * or one that only wants the React surface, never resolves Hono).\n *\n * A host writes:\n *\n * const notifications = notificationsRouter({ …config, resolveActor });\n * app.route('/api/account', notifications.router);\n *\n * and keeps what is genuinely its own: who the caller is. Everything after\n * that — parsing, status codes, the envelope, the pt-BR copy — is the\n * package's.\n */\n\n/**\n * Resolve the caller. Returning `null` means unauthenticated, which answers 401\n * before any handler runs.\n *\n * Note the 401 is self-guarded HERE rather than assumed from middleware: these\n * paths sit under an API prefix that a host's page middleware typically does not\n * match, and an unauthenticated inbox read that fell through would answer\n * somebody else's rows or none at all — both worse than a 401.\n */\nexport type ResolveNotificationsActor = (\n c: Context,\n) => Promise<NotificationsActor | null> | NotificationsActor | null;\n\nexport interface NotificationsHonoConfig extends NotificationsServerConfig {\n resolveActor: ResolveNotificationsActor;\n}\n\nexport interface NotificationsHono extends ApiNotifications {\n router: Hono;\n}\n\n/**\n * Reads the JSON body, tolerating an absent or malformed one — and only when\n * the caller SAID it was JSON.\n *\n * The content-type check is a CSRF speed bump, not a defence (see ADOPTING rule\n * 13, which names the actual one). `text/plain`, `multipart/form-data` and\n * `application/x-www-form-urlencoded` are the three types a cross-site `fetch`\n * or a plain `<form>` can send with NO preflight, so parsing a body regardless\n * of its type is what lets such a request reach these handlers at all. Refusing\n * them means a cross-site write has to earn a preflight first, which the browser\n * will then refuse on its own. The price is nil: every client of this surface,\n * the packaged one included, sends `application/json`.\n */\nfunction saysJson(c: Context): boolean {\n const type = c.req.header('content-type');\n if (!type) return false;\n const mime = (type.split(';')[0] ?? '').trim().toLowerCase();\n return mime === 'application/json' || mime.endsWith('+json');\n}\n\nasync function readBody(c: Context): Promise<unknown> {\n if (c.req.method === 'GET') return undefined;\n if (!saysJson(c)) return undefined;\n try {\n return await c.req.json();\n } catch {\n // A malformed body is the caller's error; the handler's own validation\n // reports it far better than a parse failure would.\n return undefined;\n }\n}\n\nexport function notificationsRouter(config: NotificationsHonoConfig): NotificationsHono {\n const api = createApiNotifications(config);\n const messages = messagesOf(config);\n const router = new Hono();\n\n // Mounted IN DESCRIPTOR ORDER, which any adapter must preserve. Hono resolves\n // by registration order, so a host route shaped `/notifications/:id` under the\n // same prefix must be registered AFTER this router or it captures\n // `/notifications/unread-count`.\n for (const route of api.routes) {\n const handler = async (c: Context): Promise<Response> => {\n const actor = await config.resolveActor(c);\n if (!actor) return c.json({ error: messages.unauthenticated }, 401);\n\n const response = await route.handle({\n actor,\n params: c.req.param() as Record<string, string | undefined>,\n query: c.req.query() as Record<string, string | undefined>,\n body: await readBody(c),\n headers: { 'user-agent': c.req.header('user-agent') },\n });\n\n // A handler that chose NO body means exactly that (204).\n if (response.body === undefined) return c.body(null, response.status as 204);\n // The status travels with the body the handler chose; the adapter never\n // reinterprets either.\n return c.json(response.body as Record<string, unknown>, response.status as 200);\n };\n\n if (route.method === 'GET') router.get(route.path, handler);\n else if (route.method === 'POST') router.post(route.path, handler);\n else if (route.method === 'PUT') router.put(route.path, handler);\n else router.delete(route.path, handler);\n }\n\n return { ...api, router };\n}\n"],"mappings":"
|
|
1
|
+
{"version":3,"sources":["../../src/hono/index.ts"],"sourcesContent":["import { Hono } from 'hono';\nimport type { Context } from 'hono';\n\nimport { messagesOf } from '../messages';\n\nimport {\n createApiNotifications,\n type ApiNotifications,\n type NotificationsServerConfig,\n} from '../server/create-api-notifications';\nimport type { NotificationsActor } from '../server/context';\n\n/**\n * `@12-apps/notifications/hono` — the account notification endpoints as a\n * mountable router.\n *\n * The framework-neutral descriptors in `/server` are the contract; this is the\n * adapter for the framework we happen to use, behind its own subpath with\n * `hono` as an OPTIONAL peer (the report-builder precedent — a host on Express,\n * or one that only wants the React surface, never resolves Hono).\n *\n * A host writes:\n *\n * const notifications = notificationsRouter({ …config, resolveActor });\n * app.route('/api/account', notifications.router);\n *\n * and keeps what is genuinely its own: who the caller is. Everything after\n * that — parsing, status codes, the envelope, the pt-BR copy — is the\n * package's.\n */\n\n/**\n * Resolve the caller. Returning `null` means unauthenticated, which answers 401\n * before any handler runs.\n *\n * Note the 401 is self-guarded HERE rather than assumed from middleware: these\n * paths sit under an API prefix that a host's page middleware typically does not\n * match, and an unauthenticated inbox read that fell through would answer\n * somebody else's rows or none at all — both worse than a 401.\n */\nexport type ResolveNotificationsActor = (\n c: Context,\n) => Promise<NotificationsActor | null> | NotificationsActor | null;\n\nexport interface NotificationsHonoConfig extends NotificationsServerConfig {\n resolveActor: ResolveNotificationsActor;\n}\n\nexport interface NotificationsHono extends ApiNotifications {\n router: Hono;\n}\n\n/**\n * Reads the JSON body, tolerating an absent or malformed one — and only when\n * the caller SAID it was JSON.\n *\n * The content-type check is a CSRF speed bump, not a defence (see ADOPTING rule\n * 13, which names the actual one). `text/plain`, `multipart/form-data` and\n * `application/x-www-form-urlencoded` are the three types a cross-site `fetch`\n * or a plain `<form>` can send with NO preflight, so parsing a body regardless\n * of its type is what lets such a request reach these handlers at all. Refusing\n * them means a cross-site write has to earn a preflight first, which the browser\n * will then refuse on its own. The price is nil: every client of this surface,\n * the packaged one included, sends `application/json`.\n */\nfunction saysJson(c: Context): boolean {\n const type = c.req.header('content-type');\n if (!type) return false;\n const mime = (type.split(';')[0] ?? '').trim().toLowerCase();\n return mime === 'application/json' || mime.endsWith('+json');\n}\n\nasync function readBody(c: Context): Promise<unknown> {\n if (c.req.method === 'GET') return undefined;\n if (!saysJson(c)) return undefined;\n try {\n return await c.req.json();\n } catch {\n // A malformed body is the caller's error; the handler's own validation\n // reports it far better than a parse failure would.\n return undefined;\n }\n}\n\nexport function notificationsRouter(config: NotificationsHonoConfig): NotificationsHono {\n const api = createApiNotifications(config);\n const messages = messagesOf(config);\n const router = new Hono();\n\n // Mounted IN DESCRIPTOR ORDER, which any adapter must preserve. Hono resolves\n // by registration order, so a host route shaped `/notifications/:id` under the\n // same prefix must be registered AFTER this router or it captures\n // `/notifications/unread-count`.\n for (const route of api.routes) {\n const handler = async (c: Context): Promise<Response> => {\n const actor = await config.resolveActor(c);\n if (!actor) return c.json({ error: messages.unauthenticated }, 401);\n\n const response = await route.handle({\n actor,\n params: c.req.param() as Record<string, string | undefined>,\n query: c.req.query() as Record<string, string | undefined>,\n body: await readBody(c),\n headers: { 'user-agent': c.req.header('user-agent') },\n });\n\n // A handler that chose NO body means exactly that (204).\n if (response.body === undefined) return c.body(null, response.status as 204);\n // The status travels with the body the handler chose; the adapter never\n // reinterprets either.\n return c.json(response.body as Record<string, unknown>, response.status as 200);\n };\n\n if (route.method === 'GET') router.get(route.path, handler);\n else if (route.method === 'POST') router.post(route.path, handler);\n else if (route.method === 'PUT') router.put(route.path, handler);\n else router.delete(route.path, handler);\n }\n\n return { ...api, router };\n}\n"],"mappings":";;;;;;;;;;;;;AAAA,SAAS,YAAY;AAiErB,SAAS,SAAS,GAAqB;AACrC,QAAM,OAAO,EAAE,IAAI,OAAO,cAAc;AACxC,MAAI,CAAC,KAAM,QAAO;AAClB,QAAM,QAAQ,KAAK,MAAM,GAAG,EAAE,CAAC,KAAK,IAAI,KAAK,EAAE,YAAY;AAC3D,SAAO,SAAS,sBAAsB,KAAK,SAAS,OAAO;AAC7D;AALS;AAOT,eAAe,SAAS,GAA8B;AACpD,MAAI,EAAE,IAAI,WAAW,MAAO,QAAO;AACnC,MAAI,CAAC,SAAS,CAAC,EAAG,QAAO;AACzB,MAAI;AACF,WAAO,MAAM,EAAE,IAAI,KAAK;AAAA,EAC1B,QAAQ;AAGN,WAAO;AAAA,EACT;AACF;AAVe;AAYR,SAAS,oBAAoB,QAAoD;AACtF,QAAM,MAAM,uBAAuB,MAAM;AACzC,QAAM,WAAW,WAAW,MAAM;AAClC,QAAM,SAAS,IAAI,KAAK;AAMxB,aAAW,SAAS,IAAI,QAAQ;AAC9B,UAAM,UAAU,8BAAO,MAAkC;AACvD,YAAM,QAAQ,MAAM,OAAO,aAAa,CAAC;AACzC,UAAI,CAAC,MAAO,QAAO,EAAE,KAAK,EAAE,OAAO,SAAS,gBAAgB,GAAG,GAAG;AAElE,YAAM,WAAW,MAAM,MAAM,OAAO;AAAA,QAClC;AAAA,QACA,QAAQ,EAAE,IAAI,MAAM;AAAA,QACpB,OAAO,EAAE,IAAI,MAAM;AAAA,QACnB,MAAM,MAAM,SAAS,CAAC;AAAA,QACtB,SAAS,EAAE,cAAc,EAAE,IAAI,OAAO,YAAY,EAAE;AAAA,MACtD,CAAC;AAGD,UAAI,SAAS,SAAS,OAAW,QAAO,EAAE,KAAK,MAAM,SAAS,MAAa;AAG3E,aAAO,EAAE,KAAK,SAAS,MAAiC,SAAS,MAAa;AAAA,IAChF,GAjBgB;AAmBhB,QAAI,MAAM,WAAW,MAAO,QAAO,IAAI,MAAM,MAAM,OAAO;AAAA,aACjD,MAAM,WAAW,OAAQ,QAAO,KAAK,MAAM,MAAM,OAAO;AAAA,aACxD,MAAM,WAAW,MAAO,QAAO,IAAI,MAAM,MAAM,OAAO;AAAA,QAC1D,QAAO,OAAO,MAAM,MAAM,OAAO;AAAA,EACxC;AAEA,SAAO,EAAE,GAAG,KAAK,OAAO;AAC1B;AApCgB;","names":[]}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
export { D as DeliveryStatus, N as NOTIFICATION_CHANNELS, a as NotificationCategory, b as NotificationChannel, c as NotificationContent, d as NotificationEvent, e as NotificationGenerator, f as NotificationLogger, g as NotificationRecipient, h as NotificationTaxonomy, i as NotificationTransport, T as TransportRecipient, t as taxonomyOf } from './types-CXLAG3UU.js';
|
|
2
2
|
export { N as NotificationGeneratorRegistry, c as createGeneratorRegistry } from './generators-B9xt3sRh.js';
|
|
3
|
-
export { C as ChannelMatrix, a as ChannelRow, D as DEFAULT_CHANNEL_ROW, I as InboxNotification, L as ListNotificationsResult, N as NotificationMessages, b as NotificationRow, c as NotificationWireMessages, d as defaultChannelMatrix, e as enabledChannelsOf, i as inboxWire, m as mergeChoices, f as mergeStoredRow, g as messagesOf } from './wire-
|
|
3
|
+
export { C as ChannelMatrix, a as ChannelRow, D as DEFAULT_CHANNEL_ROW, I as InboxNotification, L as ListNotificationsResult, N as NotificationMessages, b as NotificationRow, c as NotificationWireMessages, d as defaultChannelMatrix, e as enabledChannelsOf, i as inboxWire, m as mergeChoices, f as mergeStoredRow, g as messagesOf } from './wire-6dzyfDE7.js';
|
|
4
4
|
|
|
5
5
|
/** Thrown by `notify` when no generator is registered for the event type. */
|
|
6
6
|
declare class UnknownNotificationTypeError extends Error {
|
package/dist/index.js
CHANGED
|
@@ -9,12 +9,14 @@ import {
|
|
|
9
9
|
mergeChoices,
|
|
10
10
|
mergeStoredRow,
|
|
11
11
|
normalizePhoneE164
|
|
12
|
-
} from "./chunk-
|
|
12
|
+
} from "./chunk-HHMRCMQU.js";
|
|
13
13
|
import {
|
|
14
14
|
NOTIFICATION_CHANNELS,
|
|
15
|
-
messagesOf,
|
|
16
15
|
taxonomyOf
|
|
17
|
-
} from "./chunk-
|
|
16
|
+
} from "./chunk-XE7HZVMH.js";
|
|
17
|
+
import {
|
|
18
|
+
messagesOf
|
|
19
|
+
} from "./chunk-M2TVBVH2.js";
|
|
18
20
|
import "./chunk-7QVYU63E.js";
|
|
19
21
|
export {
|
|
20
22
|
DEFAULT_CHANNEL_ROW,
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { WireJobBlueprint } from '@12-apps/wiring';
|
|
2
|
-
import { A as ApiNotifications } from './create-api-notifications-
|
|
2
|
+
import { A as ApiNotifications } from './create-api-notifications-B3u6Kx3x.js';
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* The two background jobs getting a message out actually needs.
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import * as _12_apps_wiring from '@12-apps/wiring';
|
|
2
2
|
import { WireRequest } from '@12-apps/wiring';
|
|
3
|
-
import { N as NotificationsServerConfig, A as ApiNotifications, b as NotificationsRoute } from '../create-api-notifications-
|
|
4
|
-
import { N as NotificationsJobDeps } from '../jobs-
|
|
3
|
+
import { N as NotificationsServerConfig, A as ApiNotifications, b as NotificationsRoute } from '../create-api-notifications-B3u6Kx3x.js';
|
|
4
|
+
import { N as NotificationsJobDeps } from '../jobs-BDZ7aGHV.js';
|
|
5
5
|
import '../generators-B9xt3sRh.js';
|
|
6
6
|
import '../types-CXLAG3UU.js';
|
|
7
|
-
import '../wire-
|
|
7
|
+
import '../wire-6dzyfDE7.js';
|
|
8
8
|
import '../web-push-Cs14Wp9u.js';
|
|
9
9
|
|
|
10
10
|
/** One `NotificationsRoute` as the wiring contract reads it. */
|
package/dist/manifest/server.js
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import {
|
|
2
2
|
NOTIFICATIONS_JOBS
|
|
3
|
-
} from "../chunk-
|
|
3
|
+
} from "../chunk-CUZW62JS.js";
|
|
4
4
|
import {
|
|
5
5
|
createApiNotifications
|
|
6
|
-
} from "../chunk-
|
|
7
|
-
import "../chunk-
|
|
8
|
-
import "../chunk-
|
|
6
|
+
} from "../chunk-WHBMPHQE.js";
|
|
7
|
+
import "../chunk-HHMRCMQU.js";
|
|
8
|
+
import "../chunk-XE7HZVMH.js";
|
|
9
|
+
import "../chunk-M2TVBVH2.js";
|
|
9
10
|
import {
|
|
10
11
|
__name
|
|
11
12
|
} from "../chunk-7QVYU63E.js";
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/manifest/server.ts"],"sourcesContent":["/**\n * `@12-apps/notifications/manifest/server` — the server capabilities.\n *\n * `http.create` wraps `createApiNotifications` in a WIRE VIEW, and the reason\n * is one field. `NotificationsRequest` carries `headers` — the contract's\n * `WireRequest` does not, because headers are the adapter's business\n * everywhere else — and exactly one descriptor reads it: push-subscribe takes\n * `user-agent` as the DEVICE HINT it labels a subscription with. Without the\n * view the field would simply be absent at runtime while still type-checking,\n * and every saved device would come back unnamed: a silent quality loss, the\n * failure mode the wiring contract exists to convert into a loud one.\n *\n * So the view derives `headers` from the raw request the contract already\n * carries for the handlers `params`/`query`/`body` cannot serve. A host whose\n * adapter leaves `request` unset still gets a working surface — every route\n * answers, the subscription saves — with an unnamed device, which is the\n * honest degradation for a hint. `@12-apps/notifications/hono` populates it.\n *\n * Everything else rides beside the mapped routes on the aggregate unchanged:\n * `notify`, `notifyByPermission`, `dispatchDeliveries`, `drainPending`, the\n * three stores, `registerGenerator` and the transports registry. A host still\n * calls those directly — being mounted does not make the emit front door stop\n * being a library.\n */\n\nimport type { AnyServerManifest, WireRequest } from '@12-apps/wiring';\n\nimport {\n createApiNotifications,\n NOTIFICATIONS_JOBS,\n type ApiNotifications,\n type NotificationsRoute,\n type NotificationsServerConfig,\n} from '../server';\n\n/** The header names this surface reads — the device hint, and nothing else. */\nconst READ_HEADERS = ['user-agent'] as const;\n\n/** The headers the package expects, taken off the raw request when there is one. */\nfunction headersOf(request: WireRequest<never>): Record<string, string | undefined> {\n const raw = request.request;\n if (!raw) return {};\n return Object.fromEntries(\n READ_HEADERS.map((name) => [name, raw.headers.get(name) ?? undefined]),\n );\n}\n\n/** One `NotificationsRoute` as the wiring contract reads it. */\nfunction asWireRoute(route: NotificationsRoute): {\n method: NotificationsRoute['method'];\n path: string;\n handle(request: WireRequest<never>): Promise<{ status: number; body: unknown }>;\n} {\n return {\n method: route.method,\n path: route.path,\n handle: (request) =>\n route.handle({\n actor: request.actor,\n params: request.params,\n query: request.query,\n body: request.body,\n headers: headersOf(request),\n }),\n };\n}\n\n/** `createApiNotifications`, its routes re-shaped for the aggregate. */\nexport function createWireApiNotifications(\n config: NotificationsServerConfig,\n): Omit<ApiNotifications, 'routes'> & { routes: ReturnType<typeof asWireRoute>[] } {\n const api = createApiNotifications(config);\n return { ...api, routes: api.routes.map(asWireRoute) };\n}\n\nexport const notificationsServerManifest = {\n name: '@12-apps/notifications',\n http: { create: createWireApiNotifications },\n /**\n * The dispatch fast path and the retry sweep, with their cadence. The host\n * binds `{ dispatchDeliveries, drainPending }` off its own mount — the two\n * methods the aggregate already hands it — and deletes the hand-rolled\n * copies. See `../server/jobs` for why the numbers are the package's.\n */\n jobs: NOTIFICATIONS_JOBS,\n} as const satisfies AnyServerManifest;\n"],"mappings":"
|
|
1
|
+
{"version":3,"sources":["../../src/manifest/server.ts"],"sourcesContent":["/**\n * `@12-apps/notifications/manifest/server` — the server capabilities.\n *\n * `http.create` wraps `createApiNotifications` in a WIRE VIEW, and the reason\n * is one field. `NotificationsRequest` carries `headers` — the contract's\n * `WireRequest` does not, because headers are the adapter's business\n * everywhere else — and exactly one descriptor reads it: push-subscribe takes\n * `user-agent` as the DEVICE HINT it labels a subscription with. Without the\n * view the field would simply be absent at runtime while still type-checking,\n * and every saved device would come back unnamed: a silent quality loss, the\n * failure mode the wiring contract exists to convert into a loud one.\n *\n * So the view derives `headers` from the raw request the contract already\n * carries for the handlers `params`/`query`/`body` cannot serve. A host whose\n * adapter leaves `request` unset still gets a working surface — every route\n * answers, the subscription saves — with an unnamed device, which is the\n * honest degradation for a hint. `@12-apps/notifications/hono` populates it.\n *\n * Everything else rides beside the mapped routes on the aggregate unchanged:\n * `notify`, `notifyByPermission`, `dispatchDeliveries`, `drainPending`, the\n * three stores, `registerGenerator` and the transports registry. A host still\n * calls those directly — being mounted does not make the emit front door stop\n * being a library.\n */\n\nimport type { AnyServerManifest, WireRequest } from '@12-apps/wiring';\n\nimport {\n createApiNotifications,\n NOTIFICATIONS_JOBS,\n type ApiNotifications,\n type NotificationsRoute,\n type NotificationsServerConfig,\n} from '../server';\n\n/** The header names this surface reads — the device hint, and nothing else. */\nconst READ_HEADERS = ['user-agent'] as const;\n\n/** The headers the package expects, taken off the raw request when there is one. */\nfunction headersOf(request: WireRequest<never>): Record<string, string | undefined> {\n const raw = request.request;\n if (!raw) return {};\n return Object.fromEntries(\n READ_HEADERS.map((name) => [name, raw.headers.get(name) ?? undefined]),\n );\n}\n\n/** One `NotificationsRoute` as the wiring contract reads it. */\nfunction asWireRoute(route: NotificationsRoute): {\n method: NotificationsRoute['method'];\n path: string;\n handle(request: WireRequest<never>): Promise<{ status: number; body: unknown }>;\n} {\n return {\n method: route.method,\n path: route.path,\n handle: (request) =>\n route.handle({\n actor: request.actor,\n params: request.params,\n query: request.query,\n body: request.body,\n headers: headersOf(request),\n }),\n };\n}\n\n/** `createApiNotifications`, its routes re-shaped for the aggregate. */\nexport function createWireApiNotifications(\n config: NotificationsServerConfig,\n): Omit<ApiNotifications, 'routes'> & { routes: ReturnType<typeof asWireRoute>[] } {\n const api = createApiNotifications(config);\n return { ...api, routes: api.routes.map(asWireRoute) };\n}\n\nexport const notificationsServerManifest = {\n name: '@12-apps/notifications',\n http: { create: createWireApiNotifications },\n /**\n * The dispatch fast path and the retry sweep, with their cadence. The host\n * binds `{ dispatchDeliveries, drainPending }` off its own mount — the two\n * methods the aggregate already hands it — and deletes the hand-rolled\n * copies. See `../server/jobs` for why the numbers are the package's.\n */\n jobs: NOTIFICATIONS_JOBS,\n} as const satisfies AnyServerManifest;\n"],"mappings":";;;;;;;;;;;;;;AAoCA,IAAM,eAAe,CAAC,YAAY;AAGlC,SAAS,UAAU,SAAiE;AAClF,QAAM,MAAM,QAAQ;AACpB,MAAI,CAAC,IAAK,QAAO,CAAC;AAClB,SAAO,OAAO;AAAA,IACZ,aAAa,IAAI,CAAC,SAAS,CAAC,MAAM,IAAI,QAAQ,IAAI,IAAI,KAAK,MAAS,CAAC;AAAA,EACvE;AACF;AANS;AAST,SAAS,YAAY,OAInB;AACA,SAAO;AAAA,IACL,QAAQ,MAAM;AAAA,IACd,MAAM,MAAM;AAAA,IACZ,QAAQ,wBAAC,YACP,MAAM,OAAO;AAAA,MACX,OAAO,QAAQ;AAAA,MACf,QAAQ,QAAQ;AAAA,MAChB,OAAO,QAAQ;AAAA,MACf,MAAM,QAAQ;AAAA,MACd,SAAS,UAAU,OAAO;AAAA,IAC5B,CAAC,GAPK;AAAA,EAQV;AACF;AAjBS;AAoBF,SAAS,2BACd,QACiF;AACjF,QAAM,MAAM,uBAAuB,MAAM;AACzC,SAAO,EAAE,GAAG,KAAK,QAAQ,IAAI,OAAO,IAAI,WAAW,EAAE;AACvD;AALgB;AAOT,IAAM,8BAA8B;AAAA,EACzC,MAAM;AAAA,EACN,MAAM,EAAE,QAAQ,2BAA2B;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAO3C,MAAM;AACR;","names":[]}
|
package/dist/manifest/web.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { c as createWebNotifications } from '../create-web-notifications-
|
|
1
|
+
import { c as createWebNotifications } from '../create-web-notifications-BHCzaU2y.js';
|
|
2
2
|
import 'react';
|
|
3
|
-
import '../wire-
|
|
3
|
+
import '../wire-6dzyfDE7.js';
|
|
4
4
|
import '../types-CXLAG3UU.js';
|
|
5
5
|
|
|
6
6
|
/**
|
package/dist/manifest/web.js
CHANGED
package/dist/manifest/web.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/manifest/web.ts"],"sourcesContent":["/**\n * `@12-apps/notifications/manifest/web` — the web capabilities.\n *\n * `surface.create` IS `createWebNotifications`, unchanged: the bell, the inbox\n * slide-over, the preferences screen and the bound store, built once per\n * adoption by the consumer's binder (the members are component TYPES, so\n * rebuilding per render unmounts the panel mid-interaction — the memoisation\n * rule every hand wiring carries as a comment today).\n *\n * ## Why this manifest exists now\n *\n * It was narrowed away with a reason that reads plausibly and is FALSE:\n * \"listing `web` would oblige every server host adopting this manifest to\n * answer for a React surface it never mounts.\" The consumer does not work\n * that way. A capability declared for the other runtime is reported\n * `out-of-scope` — \"a web host answers for this\" — and `assemble()` returns\n * fine; only a capability applicable to THIS runtime and unanswered is\n * `unbound`. `wiring`'s own fixture package declares both halves and its\n * server-host suite asserts exactly that.\n *\n * The narrowing therefore bought nothing and cost the thing the capability is\n * for. `./react` and `./web-push` ship Bell, Panel and Preferences — the exact\n * screens the adaptation report wanted to stop being hand-duplicated in hosts,\n * and the origin host duplicated the preferences screen and the push setup\n * anyway, because a manifest that never mentions them is a manifest nobody\n * discovers them from.\n *\n * ## Why there are no `areas`\n *\n * The bell is not a routed screen: it lives in a host's header chrome, beside\n * whatever else that host puts there, and no package can suggest a route for\n * it. Preferences IS routed, but where it belongs differs per host — under\n * account settings in one app, under a tenant's configuration in another — and\n * it is offered as `page` on the surface for the host to route at its own\n * call site. A suggested nav row would be wrong for every host but the first,\n * which is the `AreaContribution` doctrine's own test.\n */\n\nimport type { AnyWebManifest } from '@12-apps/wiring';\n\nimport { createWebNotifications } from '../react/create-web-notifications';\n\nexport const notificationsWebManifest = {\n name: '@12-apps/notifications',\n surface: { create: createWebNotifications },\n} as const satisfies AnyWebManifest;\n"],"mappings":"
|
|
1
|
+
{"version":3,"sources":["../../src/manifest/web.ts"],"sourcesContent":["/**\n * `@12-apps/notifications/manifest/web` — the web capabilities.\n *\n * `surface.create` IS `createWebNotifications`, unchanged: the bell, the inbox\n * slide-over, the preferences screen and the bound store, built once per\n * adoption by the consumer's binder (the members are component TYPES, so\n * rebuilding per render unmounts the panel mid-interaction — the memoisation\n * rule every hand wiring carries as a comment today).\n *\n * ## Why this manifest exists now\n *\n * It was narrowed away with a reason that reads plausibly and is FALSE:\n * \"listing `web` would oblige every server host adopting this manifest to\n * answer for a React surface it never mounts.\" The consumer does not work\n * that way. A capability declared for the other runtime is reported\n * `out-of-scope` — \"a web host answers for this\" — and `assemble()` returns\n * fine; only a capability applicable to THIS runtime and unanswered is\n * `unbound`. `wiring`'s own fixture package declares both halves and its\n * server-host suite asserts exactly that.\n *\n * The narrowing therefore bought nothing and cost the thing the capability is\n * for. `./react` and `./web-push` ship Bell, Panel and Preferences — the exact\n * screens the adaptation report wanted to stop being hand-duplicated in hosts,\n * and the origin host duplicated the preferences screen and the push setup\n * anyway, because a manifest that never mentions them is a manifest nobody\n * discovers them from.\n *\n * ## Why there are no `areas`\n *\n * The bell is not a routed screen: it lives in a host's header chrome, beside\n * whatever else that host puts there, and no package can suggest a route for\n * it. Preferences IS routed, but where it belongs differs per host — under\n * account settings in one app, under a tenant's configuration in another — and\n * it is offered as `page` on the surface for the host to route at its own\n * call site. A suggested nav row would be wrong for every host but the first,\n * which is the `AreaContribution` doctrine's own test.\n */\n\nimport type { AnyWebManifest } from '@12-apps/wiring';\n\nimport { createWebNotifications } from '../react/create-web-notifications';\n\nexport const notificationsWebManifest = {\n name: '@12-apps/notifications',\n surface: { create: createWebNotifications },\n} as const satisfies AnyWebManifest;\n"],"mappings":";;;;;;;;AA0CO,IAAM,2BAA2B;AAAA,EACtC,MAAM;AAAA,EACN,SAAS,EAAE,QAAQ,uBAAuB;AAC5C;","names":[]}
|
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
import {
|
|
2
|
+
relativeTime
|
|
3
|
+
} from "./chunk-O5BVUXPO.js";
|
|
4
|
+
import {
|
|
5
|
+
BellIcon,
|
|
6
|
+
useInboxList
|
|
7
|
+
} from "./chunk-BW723CX2.js";
|
|
8
|
+
import {
|
|
9
|
+
__name
|
|
10
|
+
} from "./chunk-7QVYU63E.js";
|
|
11
|
+
|
|
12
|
+
// src/react/panel.tsx
|
|
13
|
+
import { useCallback } from "react";
|
|
14
|
+
import { EmptyState } from "@12-apps/ui/data-display/EmptyState";
|
|
15
|
+
import { LoadingState } from "@12-apps/ui/data-display/LoadingState";
|
|
16
|
+
import { Button as Button2 } from "@12-apps/ui/form/Button";
|
|
17
|
+
import { Drawer, DrawerContent, DrawerHeader } from "@12-apps/ui/layout/Drawer";
|
|
18
|
+
import { Box as Box2 } from "@12-apps/ui/mui/Box";
|
|
19
|
+
import { useMediaQuery } from "@12-apps/ui/mui/useMediaQuery";
|
|
20
|
+
import { useTheme } from "@12-apps/ui/mui/styles";
|
|
21
|
+
|
|
22
|
+
// src/react/row.tsx
|
|
23
|
+
import { Button } from "@12-apps/ui/form/Button";
|
|
24
|
+
import { Box } from "@12-apps/ui/mui/Box";
|
|
25
|
+
import { alpha } from "@12-apps/ui/mui/styles";
|
|
26
|
+
import { Text } from "@12-apps/ui/typography/Text";
|
|
27
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
28
|
+
var contentButtonSx = {
|
|
29
|
+
flex: 1,
|
|
30
|
+
minWidth: 0,
|
|
31
|
+
display: "flex",
|
|
32
|
+
flexDirection: "column",
|
|
33
|
+
gap: 0.25,
|
|
34
|
+
textAlign: "left",
|
|
35
|
+
border: "none",
|
|
36
|
+
background: "none",
|
|
37
|
+
p: 0,
|
|
38
|
+
cursor: "pointer",
|
|
39
|
+
color: "text.primary",
|
|
40
|
+
fontFamily: "inherit"
|
|
41
|
+
};
|
|
42
|
+
var unreadDotSx = {
|
|
43
|
+
width: 8,
|
|
44
|
+
height: 8,
|
|
45
|
+
borderRadius: "50%",
|
|
46
|
+
bgcolor: "primary.main",
|
|
47
|
+
flex: "0 0 auto"
|
|
48
|
+
};
|
|
49
|
+
function NotificationRow({
|
|
50
|
+
notification,
|
|
51
|
+
messages,
|
|
52
|
+
onOpen,
|
|
53
|
+
onDelete
|
|
54
|
+
}) {
|
|
55
|
+
const unread = notification.readAt === null;
|
|
56
|
+
return /* @__PURE__ */ jsxs(
|
|
57
|
+
Box,
|
|
58
|
+
{
|
|
59
|
+
"data-testid": `notification-${notification.id}`,
|
|
60
|
+
sx: {
|
|
61
|
+
display: "flex",
|
|
62
|
+
alignItems: "flex-start",
|
|
63
|
+
gap: 1,
|
|
64
|
+
py: 1.5,
|
|
65
|
+
px: 1,
|
|
66
|
+
borderBottom: "1px solid",
|
|
67
|
+
borderColor: "divider",
|
|
68
|
+
bgcolor: unread ? (t) => alpha(t.palette.primary.main, 0.06) : "transparent"
|
|
69
|
+
},
|
|
70
|
+
children: [
|
|
71
|
+
/* @__PURE__ */ jsxs(
|
|
72
|
+
Box,
|
|
73
|
+
{
|
|
74
|
+
component: "button",
|
|
75
|
+
type: "button",
|
|
76
|
+
onClick: () => onOpen(notification),
|
|
77
|
+
"aria-label": unread ? `${notification.title} (${messages.unreadSuffix})` : notification.title,
|
|
78
|
+
sx: contentButtonSx,
|
|
79
|
+
children: [
|
|
80
|
+
/* @__PURE__ */ jsxs(Box, { sx: { display: "flex", alignItems: "center", gap: 0.75 }, children: [
|
|
81
|
+
unread ? /* @__PURE__ */ jsx(Box, { "aria-hidden": true, sx: unreadDotSx }) : null,
|
|
82
|
+
/* @__PURE__ */ jsx(Text, { variant: "body", size: "sm", weight: unread ? "bold" : "medium", as: "span", children: notification.title })
|
|
83
|
+
] }),
|
|
84
|
+
/* @__PURE__ */ jsx(Text, { variant: "caption", size: "xs", color: "secondary", as: "span", children: notification.body }),
|
|
85
|
+
/* @__PURE__ */ jsx(Text, { variant: "caption", size: "xs", color: "secondary", as: "span", italic: true, children: relativeTime(notification.createdAt, messages) })
|
|
86
|
+
]
|
|
87
|
+
}
|
|
88
|
+
),
|
|
89
|
+
/* @__PURE__ */ jsx(
|
|
90
|
+
Button,
|
|
91
|
+
{
|
|
92
|
+
variant: "ghost",
|
|
93
|
+
color: "neutral",
|
|
94
|
+
size: "xs",
|
|
95
|
+
"aria-label": messages.deleteOne(notification.title),
|
|
96
|
+
onClick: () => onDelete(notification.id),
|
|
97
|
+
dataTestId: `notification-delete-${notification.id}`,
|
|
98
|
+
children: "\u2715"
|
|
99
|
+
}
|
|
100
|
+
)
|
|
101
|
+
]
|
|
102
|
+
}
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
__name(NotificationRow, "NotificationRow");
|
|
106
|
+
|
|
107
|
+
// src/react/panel.tsx
|
|
108
|
+
import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
109
|
+
function PanelBody({
|
|
110
|
+
state,
|
|
111
|
+
messages,
|
|
112
|
+
onRetry,
|
|
113
|
+
onLoadMore,
|
|
114
|
+
onOpen,
|
|
115
|
+
onDelete
|
|
116
|
+
}) {
|
|
117
|
+
if (state.status === "pending" || state.status === "idle") {
|
|
118
|
+
return /* @__PURE__ */ jsx2(
|
|
119
|
+
LoadingState,
|
|
120
|
+
{
|
|
121
|
+
variant: "spinner",
|
|
122
|
+
message: messages.loading,
|
|
123
|
+
size: "md",
|
|
124
|
+
dataTestId: "notifications-loading"
|
|
125
|
+
}
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
if (state.status === "error") {
|
|
129
|
+
return /* @__PURE__ */ jsx2(
|
|
130
|
+
EmptyState,
|
|
131
|
+
{
|
|
132
|
+
variant: "minimal",
|
|
133
|
+
title: messages.loadFailedTitle,
|
|
134
|
+
description: messages.loadFailedBody,
|
|
135
|
+
onRefresh: onRetry,
|
|
136
|
+
refreshLabel: messages.retry,
|
|
137
|
+
dataTestId: "notifications-error"
|
|
138
|
+
}
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
if (state.items.length === 0) {
|
|
142
|
+
return /* @__PURE__ */ jsx2(
|
|
143
|
+
EmptyState,
|
|
144
|
+
{
|
|
145
|
+
variant: "illustrated",
|
|
146
|
+
illustration: /* @__PURE__ */ jsx2(BellIcon, { size: 44, dim: true }),
|
|
147
|
+
title: messages.emptyTitle,
|
|
148
|
+
description: messages.emptyBody,
|
|
149
|
+
dataTestId: "notifications-empty"
|
|
150
|
+
}
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
return /* @__PURE__ */ jsxs2(Box2, { children: [
|
|
154
|
+
state.items.map((notification) => /* @__PURE__ */ jsx2(
|
|
155
|
+
NotificationRow,
|
|
156
|
+
{
|
|
157
|
+
notification,
|
|
158
|
+
messages,
|
|
159
|
+
onOpen,
|
|
160
|
+
onDelete
|
|
161
|
+
},
|
|
162
|
+
notification.id
|
|
163
|
+
)),
|
|
164
|
+
state.nextCursor ? /* @__PURE__ */ jsx2(Box2, { sx: { display: "flex", justifyContent: "center", py: 1.5 }, children: /* @__PURE__ */ jsx2(
|
|
165
|
+
Button2,
|
|
166
|
+
{
|
|
167
|
+
variant: "outline",
|
|
168
|
+
color: "neutral",
|
|
169
|
+
size: "sm",
|
|
170
|
+
disabled: state.loadingMore,
|
|
171
|
+
onClick: onLoadMore,
|
|
172
|
+
dataTestId: "notifications-load-more",
|
|
173
|
+
children: state.loadingMore ? messages.loadingMore : messages.loadMore
|
|
174
|
+
}
|
|
175
|
+
) }) : null
|
|
176
|
+
] });
|
|
177
|
+
}
|
|
178
|
+
__name(PanelBody, "PanelBody");
|
|
179
|
+
function NotificationsPanel({
|
|
180
|
+
open,
|
|
181
|
+
onClose,
|
|
182
|
+
onNavigate,
|
|
183
|
+
store,
|
|
184
|
+
messages
|
|
185
|
+
}) {
|
|
186
|
+
const theme = useTheme();
|
|
187
|
+
const isMobile = useMediaQuery(theme.breakpoints.down("sm"));
|
|
188
|
+
const state = useInboxList(store, open);
|
|
189
|
+
const openNotification = useCallback(
|
|
190
|
+
(notification) => {
|
|
191
|
+
if (notification.readAt === null) store.markRead([notification.id]);
|
|
192
|
+
if (notification.link && onNavigate) {
|
|
193
|
+
onClose();
|
|
194
|
+
onNavigate(notification.link);
|
|
195
|
+
}
|
|
196
|
+
},
|
|
197
|
+
[store, onClose, onNavigate]
|
|
198
|
+
);
|
|
199
|
+
const hasUnread = state.items.some((item) => item.readAt === null);
|
|
200
|
+
return /* @__PURE__ */ jsxs2(
|
|
201
|
+
Drawer,
|
|
202
|
+
{
|
|
203
|
+
open,
|
|
204
|
+
onClose,
|
|
205
|
+
anchor: "right",
|
|
206
|
+
variant: "right",
|
|
207
|
+
width: isMobile ? "100vw" : 400,
|
|
208
|
+
dataTestId: "notifications-panel",
|
|
209
|
+
children: [
|
|
210
|
+
/* @__PURE__ */ jsx2(DrawerHeader, { onClose, children: messages.panelTitle }),
|
|
211
|
+
/* @__PURE__ */ jsxs2(DrawerContent, { children: [
|
|
212
|
+
hasUnread ? /* @__PURE__ */ jsx2(Box2, { sx: { display: "flex", justifyContent: "flex-end", pb: 1 }, children: /* @__PURE__ */ jsx2(
|
|
213
|
+
Button2,
|
|
214
|
+
{
|
|
215
|
+
variant: "ghost",
|
|
216
|
+
color: "primary",
|
|
217
|
+
size: "xs",
|
|
218
|
+
onClick: () => store.markAllRead(),
|
|
219
|
+
dataTestId: "notifications-mark-all-read",
|
|
220
|
+
children: messages.markAllRead
|
|
221
|
+
}
|
|
222
|
+
) }) : null,
|
|
223
|
+
/* @__PURE__ */ jsx2(
|
|
224
|
+
PanelBody,
|
|
225
|
+
{
|
|
226
|
+
state,
|
|
227
|
+
messages,
|
|
228
|
+
onRetry: () => store.invalidate(),
|
|
229
|
+
onLoadMore: () => store.loadMore(),
|
|
230
|
+
onOpen: openNotification,
|
|
231
|
+
onDelete: (id) => store.remove(id)
|
|
232
|
+
}
|
|
233
|
+
)
|
|
234
|
+
] })
|
|
235
|
+
]
|
|
236
|
+
}
|
|
237
|
+
);
|
|
238
|
+
}
|
|
239
|
+
__name(NotificationsPanel, "NotificationsPanel");
|
|
240
|
+
export {
|
|
241
|
+
NotificationsPanel
|
|
242
|
+
};
|
|
243
|
+
//# sourceMappingURL=panel-UFXNO4AF.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/react/panel.tsx","../src/react/row.tsx"],"sourcesContent":["/**\n * The notification-centre slide-over: newest-first list with unread styling,\n * per-item open (marks read + deep-links), soft delete, mark-all, empty /\n * loading / error states and a \"load more\" cursor pager.\n *\n * Rendering is app-agnostic — the host passes `onNavigate` (its router's\n * navigate) for deep links. Without one a link is simply not followed, which is\n * what lets the panel mount in a host that has no router at all.\n */\nimport { useCallback, type JSX } from 'react';\n\nimport { EmptyState } from '@12-apps/ui/data-display/EmptyState';\nimport { LoadingState } from '@12-apps/ui/data-display/LoadingState';\nimport { Button } from '@12-apps/ui/form/Button';\nimport { Drawer, DrawerContent, DrawerHeader } from '@12-apps/ui/layout/Drawer';\nimport { Box } from '@12-apps/ui/mui/Box';\nimport { useMediaQuery } from '@12-apps/ui/mui/useMediaQuery';\nimport { useTheme } from '@12-apps/ui/mui/styles';\n\nimport type { NotificationMessages } from '../messages';\nimport type { InboxNotification } from '../wire';\n\nimport { BellIcon } from './bell-icon';\nimport { useInboxList } from './hooks';\nimport type { InboxState, InboxStore } from './inbox-state';\nimport { NotificationRow } from './row';\n\ninterface PanelBodyProps {\n state: InboxState;\n messages: NotificationMessages;\n onRetry: () => void;\n onLoadMore: () => void;\n onOpen: (notification: InboxNotification) => void;\n onDelete: (id: string) => void;\n}\n\n/** The scrollable panel body: loading / error / empty / the list + pager. */\nfunction PanelBody({\n state,\n messages,\n onRetry,\n onLoadMore,\n onOpen,\n onDelete,\n}: PanelBodyProps): JSX.Element {\n if (state.status === 'pending' || state.status === 'idle') {\n return (\n <LoadingState\n variant=\"spinner\"\n message={messages.loading}\n size=\"md\"\n dataTestId=\"notifications-loading\"\n />\n );\n }\n if (state.status === 'error') {\n return (\n <EmptyState\n variant=\"minimal\"\n title={messages.loadFailedTitle}\n description={messages.loadFailedBody}\n onRefresh={onRetry}\n refreshLabel={messages.retry}\n dataTestId=\"notifications-error\"\n />\n );\n }\n if (state.items.length === 0) {\n return (\n <EmptyState\n variant=\"illustrated\"\n illustration={<BellIcon size={44} dim />}\n title={messages.emptyTitle}\n description={messages.emptyBody}\n dataTestId=\"notifications-empty\"\n />\n );\n }\n return (\n <Box>\n {state.items.map((notification) => (\n <NotificationRow\n key={notification.id}\n notification={notification}\n messages={messages}\n onOpen={onOpen}\n onDelete={onDelete}\n />\n ))}\n {state.nextCursor ? (\n <Box sx={{ display: 'flex', justifyContent: 'center', py: 1.5 }}>\n <Button\n variant=\"outline\"\n color=\"neutral\"\n size=\"sm\"\n disabled={state.loadingMore}\n onClick={onLoadMore}\n dataTestId=\"notifications-load-more\"\n >\n {state.loadingMore ? messages.loadingMore : messages.loadMore}\n </Button>\n </Box>\n ) : null}\n </Box>\n );\n}\n\nexport interface NotificationsPanelProps {\n open: boolean;\n onClose: () => void;\n /** Navigate to a notification's in-app link (the host's router). */\n onNavigate?: (link: string) => void;\n}\n\nexport function NotificationsPanel({\n open,\n onClose,\n onNavigate,\n store,\n messages,\n}: NotificationsPanelProps & {\n store: InboxStore;\n messages: NotificationMessages;\n}): JSX.Element {\n // `useTheme` from @mui/material/styles falls back to the DEFAULT theme when\n // no provider is mounted, where the callback form of `useMediaQuery` would\n // hand the callback a null theme and throw. A published component must render\n // in a host that has not wrapped it yet.\n const theme = useTheme();\n const isMobile = useMediaQuery(theme.breakpoints.down('sm'));\n const state = useInboxList(store, open);\n\n const openNotification = useCallback(\n (notification: InboxNotification) => {\n if (notification.readAt === null) store.markRead([notification.id]);\n if (notification.link && onNavigate) {\n onClose();\n onNavigate(notification.link);\n }\n },\n [store, onClose, onNavigate],\n );\n\n const hasUnread = state.items.some((item) => item.readAt === null);\n\n return (\n <Drawer\n open={open}\n onClose={onClose}\n anchor=\"right\"\n variant=\"right\"\n width={isMobile ? '100vw' : 400}\n dataTestId=\"notifications-panel\"\n >\n <DrawerHeader onClose={onClose}>{messages.panelTitle}</DrawerHeader>\n <DrawerContent>\n {hasUnread ? (\n <Box sx={{ display: 'flex', justifyContent: 'flex-end', pb: 1 }}>\n <Button\n variant=\"ghost\"\n color=\"primary\"\n size=\"xs\"\n onClick={() => store.markAllRead()}\n dataTestId=\"notifications-mark-all-read\"\n >\n {messages.markAllRead}\n </Button>\n </Box>\n ) : null}\n <PanelBody\n state={state}\n messages={messages}\n onRetry={() => store.invalidate()}\n onLoadMore={() => store.loadMore()}\n onOpen={openNotification}\n onDelete={(id) => store.remove(id)}\n />\n </DrawerContent>\n </Drawer>\n );\n}\n","/** One inbox row: unread accent, content (opens/marks read), timestamp, delete. */\nimport type { JSX } from 'react';\n\nimport { Button } from '@12-apps/ui/form/Button';\nimport { Box } from '@12-apps/ui/mui/Box';\nimport { alpha, type Theme } from '@12-apps/ui/mui/styles';\nimport { Text } from '@12-apps/ui/typography/Text';\n\nimport type { NotificationMessages } from '../messages';\nimport type { InboxNotification } from '../wire';\n\nimport { relativeTime } from './relative-time';\n\nconst contentButtonSx = {\n flex: 1,\n minWidth: 0,\n display: 'flex',\n flexDirection: 'column',\n gap: 0.25,\n textAlign: 'left',\n border: 'none',\n background: 'none',\n p: 0,\n cursor: 'pointer',\n color: 'text.primary',\n fontFamily: 'inherit',\n} as const;\n\nconst unreadDotSx = {\n width: 8,\n height: 8,\n borderRadius: '50%',\n bgcolor: 'primary.main',\n flex: '0 0 auto',\n} as const;\n\nexport function NotificationRow({\n notification,\n messages,\n onOpen,\n onDelete,\n}: {\n notification: InboxNotification;\n messages: NotificationMessages;\n onOpen: (notification: InboxNotification) => void;\n onDelete: (id: string) => void;\n}): JSX.Element {\n const unread = notification.readAt === null;\n return (\n <Box\n data-testid={`notification-${notification.id}`}\n sx={{\n display: 'flex',\n alignItems: 'flex-start',\n gap: 1,\n py: 1.5,\n px: 1,\n borderBottom: '1px solid',\n borderColor: 'divider',\n bgcolor: unread ? (t: Theme) => alpha(t.palette.primary.main, 0.06) : 'transparent',\n }}\n >\n <Box\n component=\"button\"\n type=\"button\"\n onClick={() => onOpen(notification)}\n aria-label={\n unread ? `${notification.title} (${messages.unreadSuffix})` : notification.title\n }\n sx={contentButtonSx}\n >\n <Box sx={{ display: 'flex', alignItems: 'center', gap: 0.75 }}>\n {unread ? <Box aria-hidden sx={unreadDotSx} /> : null}\n <Text variant=\"body\" size=\"sm\" weight={unread ? 'bold' : 'medium'} as=\"span\">\n {notification.title}\n </Text>\n </Box>\n <Text variant=\"caption\" size=\"xs\" color=\"secondary\" as=\"span\">\n {notification.body}\n </Text>\n <Text variant=\"caption\" size=\"xs\" color=\"secondary\" as=\"span\" italic>\n {relativeTime(notification.createdAt, messages)}\n </Text>\n </Box>\n\n <Button\n variant=\"ghost\"\n color=\"neutral\"\n size=\"xs\"\n aria-label={messages.deleteOne(notification.title)}\n onClick={() => onDelete(notification.id)}\n dataTestId={`notification-delete-${notification.id}`}\n >\n ✕\n </Button>\n </Box>\n );\n}\n"],"mappings":"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","names":["Button","Box","jsx","jsxs","Box","Button"]}
|