@12-apps/notifications 4.9.0 → 4.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (62) hide show
  1. package/ADOPTING.md +60 -1
  2. package/README.md +73 -0
  3. package/dist/{chunk-M5XTROYM.js → chunk-5Y7QRORV.js} +8 -4
  4. package/dist/chunk-5Y7QRORV.js.map +1 -0
  5. package/dist/{chunk-BGI4BQ66.js → chunk-6W7INOYQ.js} +1 -1
  6. package/dist/chunk-6W7INOYQ.js.map +1 -0
  7. package/dist/chunk-JCVRQ42B.js +226 -0
  8. package/dist/chunk-JCVRQ42B.js.map +1 -0
  9. package/dist/chunk-RTURLH5U.js +33 -0
  10. package/dist/chunk-RTURLH5U.js.map +1 -0
  11. package/dist/{chunk-GGAZOLNN.js → chunk-SWOWHIFE.js} +2 -2
  12. package/dist/{chunk-KZG7QGFA.js → chunk-WVRODNXQ.js} +8 -4
  13. package/dist/chunk-WVRODNXQ.js.map +1 -0
  14. package/dist/{chunk-KU4FVOFA.js → chunk-ZIR3ILFH.js} +2 -2
  15. package/dist/{create-api-notifications-B-VMezVB.d.ts → create-api-notifications-CcPYrM3p.d.ts} +4 -4
  16. package/dist/{create-web-notifications-B-xQ6R0B.d.ts → create-web-notifications-_NVYmlvy.d.ts} +89 -4
  17. package/dist/{generators-BsuXe8Mp.d.ts → generators-qAD4fNPq.d.ts} +1 -1
  18. package/dist/hono/index.d.ts +5 -5
  19. package/dist/hono/index.js +4 -3
  20. package/dist/hono/index.js.map +1 -1
  21. package/dist/index.d.ts +4 -3
  22. package/dist/index.js +12 -2
  23. package/dist/{jobs-D64B_bg7.d.ts → jobs--fex87-q.d.ts} +1 -1
  24. package/dist/live-DYxEFO49.d.ts +158 -0
  25. package/dist/manifest/server.d.ts +6 -6
  26. package/dist/manifest/server.js +5 -4
  27. package/dist/manifest/server.js.map +1 -1
  28. package/dist/manifest/web.d.ts +4 -3
  29. package/dist/manifest/web.js +1 -1
  30. package/dist/{panel-UFXNO4AF.js → panel-T36JEMO3.js} +37 -13
  31. package/dist/panel-T36JEMO3.js.map +1 -0
  32. package/dist/{preferences-screen-V3ZBWP7A.js → preferences-screen-SXUIGECY.js} +2 -2
  33. package/dist/react/index.d.ts +55 -6
  34. package/dist/react/index.js +8 -2
  35. package/dist/server/index.d.ts +7 -7
  36. package/dist/server/index.js +5 -4
  37. package/dist/{types-BFmAY0P5.d.ts → types-BlqZkCWZ.d.ts} +10 -1
  38. package/dist/web-push/index.d.ts +2 -2
  39. package/dist/{web-push-D-ysQd5h.d.ts → web-push-Dnyaha2z.d.ts} +21 -1
  40. package/dist/{wire-_coV3tJk.d.ts → wire-BG1kuoXX.d.ts} +1 -1
  41. package/package.json +2 -2
  42. package/src/index.ts +10 -0
  43. package/src/live.ts +179 -0
  44. package/src/react/create-web-notifications.tsx +15 -1
  45. package/src/react/index.ts +29 -0
  46. package/src/react/live-card.tsx +279 -0
  47. package/src/react/live-config.ts +79 -0
  48. package/src/react/live-section.tsx +141 -0
  49. package/src/react/panel-lazy.tsx +3 -0
  50. package/src/react/panel.tsx +57 -10
  51. package/src/react/relative-time.ts +14 -2
  52. package/src/server/transports/web-push.ts +22 -0
  53. package/src/types.ts +10 -1
  54. package/dist/chunk-BGI4BQ66.js.map +0 -1
  55. package/dist/chunk-KZG7QGFA.js.map +0 -1
  56. package/dist/chunk-M5XTROYM.js.map +0 -1
  57. package/dist/chunk-O5BVUXPO.js +0 -22
  58. package/dist/chunk-O5BVUXPO.js.map +0 -1
  59. package/dist/panel-UFXNO4AF.js.map +0 -1
  60. /package/dist/{chunk-GGAZOLNN.js.map → chunk-SWOWHIFE.js.map} +0 -0
  61. /package/dist/{chunk-KU4FVOFA.js.map → chunk-ZIR3ILFH.js.map} +0 -0
  62. /package/dist/{preferences-screen-V3ZBWP7A.js.map → preferences-screen-SXUIGECY.js.map} +0 -0
package/dist/index.d.ts CHANGED
@@ -1,6 +1,7 @@
1
- export { D as DeliveryStatus, a as NOTIFICATION_CHANNELS, b as NotificationCategory, c as NotificationChannel, d as NotificationContent, e as NotificationEvent, f as NotificationGenerateContext, N as NotificationGenerator, g as NotificationLogger, h as NotificationRecipient, i as NotificationTaxonomy, j as NotificationTransport, T as TransportRecipient, t as taxonomyOf } from './types-BFmAY0P5.js';
2
- export { N as NotificationGeneratorRegistry, c as createGeneratorRegistry } from './generators-BsuXe8Mp.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-_coV3tJk.js';
1
+ export { D as DeliveryStatus, a as NOTIFICATION_CHANNELS, b as NotificationCategory, c as NotificationChannel, d as NotificationContent, e as NotificationEvent, f as NotificationGenerateContext, N as NotificationGenerator, g as NotificationLogger, h as NotificationRecipient, i as NotificationTaxonomy, j as NotificationTransport, T as TransportRecipient, t as taxonomyOf } from './types-BlqZkCWZ.js';
2
+ export { N as NotificationGeneratorRegistry, c as createGeneratorRegistry } from './generators-qAD4fNPq.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-BG1kuoXX.js';
4
+ export { L as LIVE_PUSH_TAG_PREFIX, a as LIVE_SUBJECT_KEY, b as LiveActivity, c as LiveActivityLane, d as LiveActivityStep, l as liveActivityLane, e as livePushTag } from './live-DYxEFO49.js';
4
5
 
5
6
  /** Thrown by `notify` when no generator is registered for the event type. */
6
7
  declare class UnknownNotificationTypeError extends Error {
package/dist/index.js CHANGED
@@ -9,17 +9,25 @@ import {
9
9
  mergeChoices,
10
10
  mergeStoredRow,
11
11
  normalizePhoneE164
12
- } from "./chunk-GGAZOLNN.js";
12
+ } from "./chunk-SWOWHIFE.js";
13
13
  import {
14
14
  messagesOf
15
15
  } from "./chunk-M2TVBVH2.js";
16
+ import {
17
+ LIVE_PUSH_TAG_PREFIX,
18
+ LIVE_SUBJECT_KEY,
19
+ liveActivityLane,
20
+ livePushTag
21
+ } from "./chunk-RTURLH5U.js";
16
22
  import {
17
23
  NOTIFICATION_CHANNELS,
18
24
  taxonomyOf
19
- } from "./chunk-BGI4BQ66.js";
25
+ } from "./chunk-6W7INOYQ.js";
20
26
  import "./chunk-7QVYU63E.js";
21
27
  export {
22
28
  DEFAULT_CHANNEL_ROW,
29
+ LIVE_PUSH_TAG_PREFIX,
30
+ LIVE_SUBJECT_KEY,
23
31
  NOTIFICATION_CHANNELS,
24
32
  UnknownNotificationRecipientError,
25
33
  UnknownNotificationTypeError,
@@ -27,6 +35,8 @@ export {
27
35
  defaultChannelMatrix,
28
36
  enabledChannelsOf,
29
37
  inboxWire,
38
+ liveActivityLane,
39
+ livePushTag,
30
40
  mergeChoices,
31
41
  mergeStoredRow,
32
42
  messagesOf,
@@ -1,5 +1,5 @@
1
1
  import { WireJobBlueprint } from '@12-apps/wiring';
2
- import { A as ApiNotifications } from './create-api-notifications-B-VMezVB.js';
2
+ import { A as ApiNotifications } from './create-api-notifications-CcPYrM3p.js';
3
3
 
4
4
  /**
5
5
  * The two background jobs getting a message out actually needs.
@@ -0,0 +1,158 @@
1
+ /**
2
+ * LIVE ACTIVITIES — the notification centre's second kind of entry.
3
+ *
4
+ * An inbox notification is an EVENT: it happened, it is stamped, it is read or
5
+ * unread, and it is still there tomorrow saying the same thing. A live activity
6
+ * is ONGOING STATE: it is pinned above the list, it has no read/unread, nobody
7
+ * deletes it, it updates itself, and it is GONE the moment the thing it tracks
8
+ * finishes.
9
+ *
10
+ * The distinction is not cosmetic. A row that says "your order is on its way",
11
+ * read an hour later, is a claim about the past presented as news — and the
12
+ * more reliable the inbox is, the more of those a person accumulates. Somewhere
13
+ * in that list is the one question they actually have, which is *where is it
14
+ * now*. A live activity answers that question and then removes itself, which is
15
+ * the property no amount of better event copy can buy.
16
+ *
17
+ * ## This file is domain-free, and that is the whole design
18
+ *
19
+ * Nothing here knows what is being tracked. A host publishes activities through
20
+ * `LiveActivitiesConfig.useActivities` (see `./react/live-config`) and this
21
+ * package owns the contract, the placement, the rendering and the OS-level
22
+ * collapse. The package that raises an alert has never been this package's
23
+ * business — see the `generators` seam — and the thing an alert is ABOUT is not
24
+ * either.
25
+ *
26
+ * ## The lane
27
+ *
28
+ * `steps` + `activeStepId` describe a progress lane, because "how far along is
29
+ * this" is the shape almost every ongoing subject has. Both are optional in
30
+ * effect: an activity with no lane (`steps: []`, `activeStepId: null`) is a
31
+ * perfectly good live entry — a heading, a sentence and a timestamp that keeps
32
+ * moving.
33
+ */
34
+ /** One stop on a live activity's lane. */
35
+ interface LiveActivityStep {
36
+ /** Stable across updates — the id is what `activeStepId` names. */
37
+ id: string;
38
+ /** What the reader sees under the dot. The HOST's word, in the reader's language. */
39
+ label: string;
40
+ }
41
+ /** One thing that is happening right now, as the notification centre shows it. */
42
+ interface LiveActivity {
43
+ /**
44
+ * Stable for the whole life of the subject.
45
+ *
46
+ * The SAME id on every update, because it is the identity of the thing being
47
+ * tracked and not of the message: the panel keys on it so a stage change
48
+ * re-renders one card rather than swapping two, and it is what
49
+ * {@link livePushTag} collapses an OS notification onto.
50
+ *
51
+ * **Unique among the activities live at one moment**, for the same reason:
52
+ * it is the React key and the card's test id. Two activities sharing one
53
+ * gives a duplicate-key warning and a card that silently shows the wrong
54
+ * subject.
55
+ */
56
+ id: string;
57
+ /**
58
+ * Which host concern this belongs to, e.g. `order`, `delivery`, `import`.
59
+ *
60
+ * Free-form and never rendered — it exists so a host publishing from two
61
+ * sources can tell its own activities apart in a test or a log without
62
+ * parsing `id`.
63
+ */
64
+ kind: string;
65
+ /** The heading — what is happening. */
66
+ title: string;
67
+ /** The line under it: the detail that makes the heading actionable, or none. */
68
+ body: string | null;
69
+ /** Where tapping the card goes, as a same-origin path. `null` renders no link. */
70
+ link: string | null;
71
+ /** The lane, in the order it is walked. Empty when this subject has no lane. */
72
+ steps: readonly LiveActivityStep[];
73
+ /**
74
+ * The stop the subject is AT — one of `steps`, or `null` for a laneless entry.
75
+ *
76
+ * An id that names no step is neither, and it is handled rather than trusted:
77
+ * see {@link liveActivityLane}.
78
+ */
79
+ activeStepId: string | null;
80
+ /**
81
+ * ISO-8601 — when the subject last MOVED, not when it was last polled.
82
+ *
83
+ * A timestamp that advances on every read would render "just now" forever,
84
+ * which is the one thing a live entry must not say when nothing is happening.
85
+ */
86
+ updatedAt: string;
87
+ }
88
+ /** A live activity's lane, resolved for rendering. */
89
+ interface LiveActivityLane {
90
+ steps: readonly LiveActivityStep[];
91
+ activeStepId: string;
92
+ /** Every stop BEFORE the active one — never the active one itself. */
93
+ completed: ReadonlySet<string>;
94
+ }
95
+ /**
96
+ * The lane to draw, or `null` when this activity has none to draw.
97
+ *
98
+ * Three cases collapse to `null`, and the third is the one worth writing down:
99
+ * no steps, no active step, and **an active step the lane does not contain**.
100
+ * That last one is not a theoretical defect — it is what a host produces the
101
+ * first time a subject reaches a stage the lane was filtered to exclude, and
102
+ * the symptom is a row of dots with NONE of them lit, which reads as a process
103
+ * that has stopped. A card with no lane still says what is happening and when
104
+ * it last moved; a dead lane says the tracking is broken.
105
+ *
106
+ * `completed` deliberately stops short of the active stop: a completed step
107
+ * draws a tick, and ticking the stop the subject is sitting in claims it has
108
+ * already left.
109
+ */
110
+ declare function liveActivityLane(activity: LiveActivity): LiveActivityLane | null;
111
+ /**
112
+ * The reserved `data` key that ties a NOTIFICATION to a live subject.
113
+ *
114
+ * A notification and a live activity are different objects with different
115
+ * lifetimes, and this is the only thing that joins them: a generator whose
116
+ * event is about something also tracked live puts the activity's id here, and
117
+ * every channel that can collapse gets to.
118
+ *
119
+ * data: { [LIVE_SUBJECT_KEY]: `order:${orderId}` }
120
+ *
121
+ * A plain string on purpose. An object here would grow a second wire contract
122
+ * inside a column this package stores verbatim.
123
+ */
124
+ declare const LIVE_SUBJECT_KEY = "liveSubject";
125
+ /**
126
+ * Namespaces the tray tag, so a live tag does not collide with the ones a host
127
+ * already uses.
128
+ *
129
+ * A convention rather than an enforcement — nothing stops a host emitting its
130
+ * own `live:`-prefixed tags — but it means the two id spaces have to be made to
131
+ * meet rather than meeting by accident.
132
+ */
133
+ declare const LIVE_PUSH_TAG_PREFIX = "live:";
134
+ /**
135
+ * The OS notification tag for a push about a live subject, or `null` for an
136
+ * ordinary event.
137
+ *
138
+ * ## What the tag buys, and why it is the PWA half of this feature
139
+ *
140
+ * A `tag` makes a new notification REPLACE the one already in the tray instead
141
+ * of stacking under it, and — unless the sender asks otherwise — replace it
142
+ * *silently*. So a subject that moves through four stages costs one tray entry
143
+ * and one buzz, and the entry that remains is the CURRENT one. Without it a
144
+ * phone accumulates one alert per stage, all of them still asserting a stage
145
+ * the subject has since left; the freshest is at the top and the reader has to
146
+ * work out that the three below it are history.
147
+ *
148
+ * That is as close as the web platform gets to an ongoing/live notification,
149
+ * and it is the half that reaches a person who does not have the app open —
150
+ * which is most of the time a live activity is live.
151
+ *
152
+ * The id is passed through unchanged: it is the host's, it is already unique
153
+ * per subject, and a hash would make the tray impossible to reason about from a
154
+ * log line.
155
+ */
156
+ declare function livePushTag(data: Readonly<Record<string, unknown>> | null | undefined): string | null;
157
+
158
+ export { LIVE_PUSH_TAG_PREFIX as L, LIVE_SUBJECT_KEY as a, type LiveActivity as b, type LiveActivityLane as c, type LiveActivityStep as d, livePushTag as e, liveActivityLane as l };
@@ -1,13 +1,13 @@
1
1
  import { EmailPreviewRoute } from '../email/previews/index.js';
2
2
  import * as _12_apps_wiring from '@12-apps/wiring';
3
3
  import { WireRequest } from '@12-apps/wiring';
4
- import { N as NotificationsServerConfig, A as ApiNotifications, b as NotificationsRoute } from '../create-api-notifications-B-VMezVB.js';
5
- import { N as NotificationsJobDeps } from '../jobs-D64B_bg7.js';
4
+ import { N as NotificationsServerConfig, A as ApiNotifications, b as NotificationsRoute } from '../create-api-notifications-CcPYrM3p.js';
5
+ import { N as NotificationsJobDeps } from '../jobs--fex87-q.js';
6
6
  import { E as EmailPreviewsConfig } from '../catalog-BOVZ2b-b.js';
7
- import '../generators-BsuXe8Mp.js';
8
- import '../types-BFmAY0P5.js';
9
- import '../wire-_coV3tJk.js';
10
- import '../web-push-D-ysQd5h.js';
7
+ import '../generators-qAD4fNPq.js';
8
+ import '../types-BlqZkCWZ.js';
9
+ import '../wire-BG1kuoXX.js';
10
+ import '../web-push-Dnyaha2z.js';
11
11
  import '../template-Dlt5S54z.js';
12
12
 
13
13
  /** One `NotificationsRoute` as the wiring contract reads it. */
@@ -3,13 +3,14 @@ import {
3
3
  } from "../chunk-FTFLU6KH.js";
4
4
  import {
5
5
  NOTIFICATIONS_JOBS
6
- } from "../chunk-KU4FVOFA.js";
6
+ } from "../chunk-ZIR3ILFH.js";
7
7
  import {
8
8
  createApiNotifications
9
- } from "../chunk-KZG7QGFA.js";
10
- import "../chunk-GGAZOLNN.js";
9
+ } from "../chunk-WVRODNXQ.js";
10
+ import "../chunk-SWOWHIFE.js";
11
11
  import "../chunk-M2TVBVH2.js";
12
- import "../chunk-BGI4BQ66.js";
12
+ import "../chunk-RTURLH5U.js";
13
+ import "../chunk-6W7INOYQ.js";
13
14
  import "../chunk-EKUSNUBT.js";
14
15
  import {
15
16
  __name
@@ -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 { emailPreviewRoutes } from '../email/previews/routes';\nimport type { EmailPreviewsConfig } from '../email/previews/catalog';\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\n/**\n * The preview console's server half — the two endpoints over the catalogue.\n *\n * A CONSTANT, not a factory. `http.create(config)` receives whatever the host\n * bound at adoption, and \"which messages exist\" is precisely a host's binding:\n * a package cannot know that a product sends a \"your quota is exhausted\"\n * notice, let alone what data it renders from. Writing `EmailPreviewsConfig`\n * as a factory argument instead would move that decision out of\n * `bindings.http`, where `assemble()` can report on it, into a call the report\n * never sees.\n *\n * The routes carry `kind: 'authenticated'` — see `../email/previews/routes`\n * for why the descriptor states a posture it cannot itself enforce.\n */\nexport const notificationEmailPreviewsServerManifest = {\n name: '@12-apps/notifications-email-previews',\n http: {\n create: (config: EmailPreviewsConfig) => ({ routes: emailPreviewRoutes(config) }),\n },\n} as const satisfies AnyServerManifest;\n"],"mappings":";;;;;;;;;;;;;;;;;;AAsCA,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;AAgBO,IAAM,0CAA0C;AAAA,EACrD,MAAM;AAAA,EACN,MAAM;AAAA,IACJ,QAAQ,wBAAC,YAAiC,EAAE,QAAQ,mBAAmB,MAAM,EAAE,IAAvE;AAAA,EACV;AACF;","names":[]}
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 { emailPreviewRoutes } from '../email/previews/routes';\nimport type { EmailPreviewsConfig } from '../email/previews/catalog';\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\n/**\n * The preview console's server half — the two endpoints over the catalogue.\n *\n * A CONSTANT, not a factory. `http.create(config)` receives whatever the host\n * bound at adoption, and \"which messages exist\" is precisely a host's binding:\n * a package cannot know that a product sends a \"your quota is exhausted\"\n * notice, let alone what data it renders from. Writing `EmailPreviewsConfig`\n * as a factory argument instead would move that decision out of\n * `bindings.http`, where `assemble()` can report on it, into a call the report\n * never sees.\n *\n * The routes carry `kind: 'authenticated'` — see `../email/previews/routes`\n * for why the descriptor states a posture it cannot itself enforce.\n */\nexport const notificationEmailPreviewsServerManifest = {\n name: '@12-apps/notifications-email-previews',\n http: {\n create: (config: EmailPreviewsConfig) => ({ routes: emailPreviewRoutes(config) }),\n },\n} as const satisfies AnyServerManifest;\n"],"mappings":";;;;;;;;;;;;;;;;;;;AAsCA,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;AAgBO,IAAM,0CAA0C;AAAA,EACrD,MAAM;AAAA,EACN,MAAM;AAAA,IACJ,QAAQ,wBAAC,YAAiC,EAAE,QAAQ,mBAAmB,MAAM,EAAE,IAAvE;AAAA,EACV;AACF;","names":[]}
@@ -1,8 +1,9 @@
1
1
  import { c as createEmailPreviewScreen } from '../preview-screen-DYJRAnAY.js';
2
- import { c as createWebNotifications } from '../create-web-notifications-B-xQ6R0B.js';
2
+ import { c as createWebNotifications } from '../create-web-notifications-_NVYmlvy.js';
3
3
  import 'react';
4
- import '../wire-_coV3tJk.js';
5
- import '../types-BFmAY0P5.js';
4
+ import '../wire-BG1kuoXX.js';
5
+ import '../types-BlqZkCWZ.js';
6
+ import '../live-DYxEFO49.js';
6
7
 
7
8
  /**
8
9
  * `@12-apps/notifications/manifest/web` — the web capabilities.
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  createWebNotifications
3
- } from "../chunk-M5XTROYM.js";
3
+ } from "../chunk-5Y7QRORV.js";
4
4
  import "../chunk-BW723CX2.js";
5
5
  import {
6
6
  createEmailPreviewScreen
@@ -1,10 +1,12 @@
1
1
  import {
2
+ LiveSection,
2
3
  relativeTime
3
- } from "./chunk-O5BVUXPO.js";
4
+ } from "./chunk-JCVRQ42B.js";
4
5
  import {
5
6
  BellIcon,
6
7
  useInboxList
7
8
  } from "./chunk-BW723CX2.js";
9
+ import "./chunk-RTURLH5U.js";
8
10
  import {
9
11
  __name
10
12
  } from "./chunk-7QVYU63E.js";
@@ -176,26 +178,39 @@ function PanelBody({
176
178
  ] });
177
179
  }
178
180
  __name(PanelBody, "PanelBody");
181
+ function usePanelOpeners(store, onClose, onNavigate) {
182
+ const follow = useCallback(
183
+ (link) => {
184
+ if (!link || !onNavigate) return;
185
+ onClose();
186
+ onNavigate(link);
187
+ },
188
+ [onClose, onNavigate]
189
+ );
190
+ return {
191
+ openNotification: useCallback(
192
+ (notification) => {
193
+ if (notification.readAt === null) store.markRead([notification.id]);
194
+ follow(notification.link);
195
+ },
196
+ [store, follow]
197
+ ),
198
+ openLive: useCallback((activity) => follow(activity.link), [follow])
199
+ };
200
+ }
201
+ __name(usePanelOpeners, "usePanelOpeners");
179
202
  function NotificationsPanel({
180
203
  open,
181
204
  onClose,
182
205
  onNavigate,
183
206
  store,
184
- messages
207
+ messages,
208
+ live
185
209
  }) {
186
210
  const theme = useTheme();
187
211
  const isMobile = useMediaQuery(theme.breakpoints.down("sm"));
188
212
  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
- );
213
+ const { openNotification, openLive } = usePanelOpeners(store, onClose, onNavigate);
199
214
  const hasUnread = state.items.some((item) => item.readAt === null);
200
215
  return /* @__PURE__ */ jsxs2(
201
216
  Drawer,
@@ -209,6 +224,15 @@ function NotificationsPanel({
209
224
  children: [
210
225
  /* @__PURE__ */ jsx2(DrawerHeader, { onClose, children: messages.panelTitle }),
211
226
  /* @__PURE__ */ jsxs2(DrawerContent, { children: [
227
+ live ? /* @__PURE__ */ jsx2(
228
+ LiveSection,
229
+ {
230
+ config: live,
231
+ messages,
232
+ active: open,
233
+ ...onNavigate ? { onOpen: openLive } : {}
234
+ }
235
+ ) : null,
212
236
  hasUnread ? /* @__PURE__ */ jsx2(Box2, { sx: { display: "flex", justifyContent: "flex-end", pb: 1 }, children: /* @__PURE__ */ jsx2(
213
237
  Button2,
214
238
  {
@@ -240,4 +264,4 @@ __name(NotificationsPanel, "NotificationsPanel");
240
264
  export {
241
265
  NotificationsPanel
242
266
  };
243
- //# sourceMappingURL=panel-UFXNO4AF.js.map
267
+ //# sourceMappingURL=panel-T36JEMO3.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 { LiveActivity } from '../live';\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 type { LiveActivitiesConfig } from './live-config';\nimport { LiveSection } from './live-section';\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\n/**\n * The two open gestures, which are ONE deep-link path with a mark-read in front\n * of half of it.\n *\n * Lifted out of the component because both kinds of entry follow a link the\n * same way and a host with no router follows neither — one rule, stated once,\n * rather than the same three lines written twice.\n */\nfunction usePanelOpeners(\n store: InboxStore,\n onClose: () => void,\n onNavigate?: (link: string) => void,\n): {\n openNotification: (notification: InboxNotification) => void;\n openLive: (activity: LiveActivity) => void;\n} {\n const follow = useCallback(\n (link: string | null) => {\n if (!link || !onNavigate) return;\n onClose();\n onNavigate(link);\n },\n [onClose, onNavigate],\n );\n\n return {\n openNotification: useCallback(\n (notification: InboxNotification) => {\n if (notification.readAt === null) store.markRead([notification.id]);\n follow(notification.link);\n },\n [store, follow],\n ),\n openLive: useCallback((activity: LiveActivity) => follow(activity.link), [follow]),\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 live,\n}: NotificationsPanelProps & {\n store: InboxStore;\n messages: NotificationMessages;\n live?: LiveActivitiesConfig;\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, openLive } = usePanelOpeners(store, onClose, onNavigate);\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 {/*\n Above the mark-all control as well as above the list, deliberately:\n \"marcar todas como lidas\" belongs to the INBOX, and a live entry has\n nothing to mark. A control between the two blocks would read as\n applying to both.\n */}\n {live ? (\n <LiveSection\n config={live}\n messages={messages}\n active={open}\n {...(onNavigate ? { onOpen: openLive } : {})}\n />\n ) : null}\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"]}
@@ -4,7 +4,7 @@ import {
4
4
  } from "./chunk-CQZMTFPY.js";
5
5
  import {
6
6
  NOTIFICATION_CHANNELS
7
- } from "./chunk-BGI4BQ66.js";
7
+ } from "./chunk-6W7INOYQ.js";
8
8
  import {
9
9
  __name
10
10
  } from "./chunk-7QVYU63E.js";
@@ -291,4 +291,4 @@ __name(PreferencesScreen, "PreferencesScreen");
291
291
  export {
292
292
  PreferencesScreen
293
293
  };
294
- //# sourceMappingURL=preferences-screen-V3ZBWP7A.js.map
294
+ //# sourceMappingURL=preferences-screen-SXUIGECY.js.map
@@ -1,8 +1,10 @@
1
- import { N as NotificationsApiClient } from '../create-web-notifications-B-xQ6R0B.js';
2
- export { B as BADGE_POLL_MS, a as BADGE_RECONCILE_MS, b as BellButtonProps, I as InboxListStatus, d as InboxState, e as InboxStore, f as NotificationsHttpError, g as NotificationsPanelProps, h as NotificationsResult, i as NotificationsSignalHook, j as NotificationsSubscribe, k as NotificationsTransport, l as NotificationsWebConfig, P as PAGE_SIZE, m as PreferencesPayload, n as PreferencesScreenProps, o as PushRegistrationPayload, W as WebNotifications, p as WebPushPlatformHint, q as WebPushSetupConfig, r as createInboxStore, s as createNotificationsApiClient, c as createWebNotifications, t as httpNotificationsTransport, u as useInboxList, v as useInboxState, w as useUnreadCount } from '../create-web-notifications-B-xQ6R0B.js';
1
+ import { L as LiveActivitiesConfig, N as NotificationsApiClient } from '../create-web-notifications-_NVYmlvy.js';
2
+ export { B as BADGE_POLL_MS, a as BADGE_RECONCILE_MS, b as BellButtonProps, I as InboxListStatus, d as InboxState, e as InboxStore, f as LiveActivitiesHook, g as LiveActivityMessages, h as NotificationsHttpError, i as NotificationsPanelProps, j as NotificationsResult, k as NotificationsSignalHook, l as NotificationsSubscribe, m as NotificationsTransport, n as NotificationsWebConfig, P as PAGE_SIZE, o as PreferencesPayload, p as PreferencesScreenProps, q as PushRegistrationPayload, W as WebNotifications, r as WebPushPlatformHint, s as WebPushSetupConfig, t as createInboxStore, u as createNotificationsApiClient, c as createWebNotifications, v as httpNotificationsTransport, w as useInboxList, x as useInboxState, y as useUnreadCount } from '../create-web-notifications-_NVYmlvy.js';
3
3
  import { JSX } from 'react';
4
- import { N as NotificationMessages } from '../wire-_coV3tJk.js';
5
- import '../types-BFmAY0P5.js';
4
+ import { b as LiveActivity } from '../live-DYxEFO49.js';
5
+ export { c as LiveActivityLane, d as LiveActivityStep, l as liveActivityLane } from '../live-DYxEFO49.js';
6
+ import { N as NotificationMessages } from '../wire-BG1kuoXX.js';
7
+ import '../types-BlqZkCWZ.js';
6
8
 
7
9
  /** Inline SVG bell (no icon-library dependency in this package). */
8
10
 
@@ -11,12 +13,59 @@ declare function BellIcon({ size, dim, }: {
11
13
  dim?: boolean;
12
14
  }): JSX.Element;
13
15
 
16
+ /**
17
+ * The pinned block at the top of the panel: everything that is happening NOW,
18
+ * above everything that has already happened.
19
+ *
20
+ * ## Why it is here and not a second surface
21
+ *
22
+ * The notification centre is where a person goes to find out what they missed.
23
+ * Splitting "happening" into its own bell would make them check two places to
24
+ * answer one question, and the half they would stop checking is the one that
25
+ * only has something in it occasionally — which is this one. Above the list,
26
+ * inside the same drawer, it is on the path they already walk.
27
+ *
28
+ * ## What it deliberately does NOT do
29
+ *
30
+ * - It does not touch `unread`. A live entry is not news; counting it would put
31
+ * a number on the bell that no amount of reading can clear.
32
+ * - It renders nothing at all when there is nothing live — no heading, no empty
33
+ * state, no reserved space. A panel with one permanent empty section in it is
34
+ * a panel that has taught its reader to skip the top.
35
+ * - It does not fetch. `useActivities` is the host's, and `active` tells it
36
+ * whether anyone is looking.
37
+ */
38
+
39
+ interface LiveSectionProps {
40
+ config: LiveActivitiesConfig;
41
+ messages: NotificationMessages;
42
+ /** Whether the panel is open — passed straight through to the host's hook. */
43
+ active: boolean;
44
+ /**
45
+ * Follow a card's link.
46
+ *
47
+ * Optional, and the panel omits it for a host with no router: a card that
48
+ * cannot go anywhere renders as text rather than as a named control that
49
+ * does nothing.
50
+ */
51
+ onOpen?: (activity: LiveActivity) => void;
52
+ }
53
+ declare function LiveSection({ config, messages, active, onOpen, }: LiveSectionProps): JSX.Element | null;
54
+
14
55
  /**
15
56
  * "há 5 min"-style relative timestamp, falling back to an absolute date for
16
57
  * anything older than a week. Every word comes from the messages table, so a
17
58
  * host in another locale changes the copy and the locale together.
59
+ *
60
+ * `now` is a parameter rather than a read, and the live section is why. A
61
+ * relative phrase is only true for the instant it was computed, so something
62
+ * has to CAUSE the render that recomputes it — and the live entries' own data
63
+ * cannot: a host backed by react-query gets the previous object back whenever a
64
+ * poll is deep-equal (`structuralSharing`, on by default), which within one
65
+ * stage it always is. The section therefore ticks a clock and hands it down.
66
+ * Defaulted, so every existing caller reads the wall clock exactly as before.
18
67
  */
19
- declare function relativeTime(iso: string, messages: NotificationMessages): string;
68
+ declare function relativeTime(iso: string, messages: NotificationMessages, now?: number): string;
20
69
 
21
70
  /**
22
71
  * The browser half of Web Push: register the service worker, ask permission,
@@ -44,4 +93,4 @@ declare function enableWebPush(api: NotificationsApiClient, swPath?: string): Pr
44
93
  /** Disable flow: unsubscribe the browser and drop the server-side row. */
45
94
  declare function disableWebPush(api: NotificationsApiClient): Promise<void>;
46
95
 
47
- export { BellIcon, NotificationsApiClient, type PushSetupResult, disableWebPush, enableWebPush, getExistingPushSubscription, pushSupported, relativeTime };
96
+ export { BellIcon, LiveActivitiesConfig, LiveActivity, LiveSection, type LiveSectionProps, NotificationsApiClient, type PushSetupResult, disableWebPush, enableWebPush, getExistingPushSubscription, pushSupported, relativeTime };
@@ -3,10 +3,11 @@ import {
3
3
  createNotificationsApiClient,
4
4
  createWebNotifications,
5
5
  httpNotificationsTransport
6
- } from "../chunk-M5XTROYM.js";
6
+ } from "../chunk-5Y7QRORV.js";
7
7
  import {
8
+ LiveSection,
8
9
  relativeTime
9
- } from "../chunk-O5BVUXPO.js";
10
+ } from "../chunk-JCVRQ42B.js";
10
11
  import {
11
12
  BADGE_POLL_MS,
12
13
  BADGE_RECONCILE_MS,
@@ -24,11 +25,15 @@ import {
24
25
  pushSupported
25
26
  } from "../chunk-CQZMTFPY.js";
26
27
  import "../chunk-M2TVBVH2.js";
28
+ import {
29
+ liveActivityLane
30
+ } from "../chunk-RTURLH5U.js";
27
31
  import "../chunk-7QVYU63E.js";
28
32
  export {
29
33
  BADGE_POLL_MS,
30
34
  BADGE_RECONCILE_MS,
31
35
  BellIcon,
36
+ LiveSection,
32
37
  NotificationsHttpError,
33
38
  PAGE_SIZE,
34
39
  createInboxStore,
@@ -38,6 +43,7 @@ export {
38
43
  enableWebPush,
39
44
  getExistingPushSubscription,
40
45
  httpNotificationsTransport,
46
+ liveActivityLane,
41
47
  pushSupported,
42
48
  relativeTime,
43
49
  useInboxList,
@@ -1,11 +1,11 @@
1
- import { A as ApiNotifications } from '../create-api-notifications-B-VMezVB.js';
2
- export { C as CommittedNotification, E as EMAIL_DRIVERS, c as EmailDriver, d as EmailDriverDeclaration, e as EmailMessage, f as ExtraDrivers, L as ListNotificationsInput, g as NotificationAudienceDirectory, h as NotificationChannelPolicy, i as NotificationCommittedListener, j as NotificationContactDirectory, k as NotificationCreateData, l as NotificationDelegate, m as NotificationDeliveryDelegate, n as NotificationDeliveryRow, o as NotificationDeliveryWhere, p as NotificationDispatchScheduler, q as NotificationInboxStore, r as NotificationPageAfter, s as NotificationPreferenceDelegate, t as NotificationPreferenceRow, u as NotificationPreferenceStore, v as NotificationRouter, w as NotificationWhere, a as NotificationsActor, x as NotificationsApiError, y as NotificationsDb, z as NotificationsDbClient, B as NotificationsDbProvider, D as NotificationsRequest, F as NotificationsResponse, b as NotificationsRoute, N as NotificationsServerConfig, G as NotifyByPermission, H as NotifyOptions, I as NotifyResult, P as PermissionNotificationResult, J as PermissionNotificationSkip, K as PushSubscriptionDelegate, M as PushSubscriptionInput, O as PushSubscriptionRow, Q as PushSubscriptionStore, S as SMS_DRIVERS, R as SmsDriver, T as SmsDriverDeclaration, U as SmsMessage, V as TransportDeclaration, W as TransportRegistry, X as WHATSAPP_DRIVERS, Y as WhatsAppDriver, Z as WhatsAppDriverDeclaration, _ as WhatsAppMessage, $ as createApiNotifications, a0 as createTransportRegistry, a1 as emailTransport, a2 as foldApiError, a3 as formatEmail, a4 as formatSms, a5 as formatWhatsApp, a6 as ok, a7 as smsTransport, a8 as whatsAppTransport } from '../create-api-notifications-B-VMezVB.js';
3
- export { D as DriverDeclarationBase, F as FetchImpl, N as NotificationProviderError, W as WEB_PUSH_DRIVERS, a as WebPushDriverDeclaration, b as WebPushMessage, c as WebPushSender, d as WebPushSubscription, e as WebPushSubscriptionSource, f as absoluteLink, g as formatWebPush, w as webPushTransport } from '../web-push-D-ysQd5h.js';
1
+ import { A as ApiNotifications } from '../create-api-notifications-CcPYrM3p.js';
2
+ export { C as CommittedNotification, E as EMAIL_DRIVERS, c as EmailDriver, d as EmailDriverDeclaration, e as EmailMessage, f as ExtraDrivers, L as ListNotificationsInput, g as NotificationAudienceDirectory, h as NotificationChannelPolicy, i as NotificationCommittedListener, j as NotificationContactDirectory, k as NotificationCreateData, l as NotificationDelegate, m as NotificationDeliveryDelegate, n as NotificationDeliveryRow, o as NotificationDeliveryWhere, p as NotificationDispatchScheduler, q as NotificationInboxStore, r as NotificationPageAfter, s as NotificationPreferenceDelegate, t as NotificationPreferenceRow, u as NotificationPreferenceStore, v as NotificationRouter, w as NotificationWhere, a as NotificationsActor, x as NotificationsApiError, y as NotificationsDb, z as NotificationsDbClient, B as NotificationsDbProvider, D as NotificationsRequest, F as NotificationsResponse, b as NotificationsRoute, N as NotificationsServerConfig, G as NotifyByPermission, H as NotifyOptions, I as NotifyResult, P as PermissionNotificationResult, J as PermissionNotificationSkip, K as PushSubscriptionDelegate, M as PushSubscriptionInput, O as PushSubscriptionRow, Q as PushSubscriptionStore, S as SMS_DRIVERS, R as SmsDriver, T as SmsDriverDeclaration, U as SmsMessage, V as TransportDeclaration, W as TransportRegistry, X as WHATSAPP_DRIVERS, Y as WhatsAppDriver, Z as WhatsAppDriverDeclaration, _ as WhatsAppMessage, $ as createApiNotifications, a0 as createTransportRegistry, a1 as emailTransport, a2 as foldApiError, a3 as formatEmail, a4 as formatSms, a5 as formatWhatsApp, a6 as ok, a7 as smsTransport, a8 as whatsAppTransport } from '../create-api-notifications-CcPYrM3p.js';
3
+ export { D as DriverDeclarationBase, F as FetchImpl, N as NotificationProviderError, W as WEB_PUSH_DRIVERS, a as WebPushDriverDeclaration, b as WebPushMessage, c as WebPushSender, d as WebPushSubscription, e as WebPushSubscriptionSource, f as absoluteLink, g as formatWebPush, w as webPushTransport } from '../web-push-Dnyaha2z.js';
4
4
  import { NotifyPort } from '@12-apps/wiring/ports';
5
- export { a as NOTIFICATIONS_DRAIN_CRON, b as NOTIFICATIONS_DRAIN_LEASE_MS, c as NOTIFICATIONS_JOBS, d as NOTIFICATIONS_SWEEP_QUEUE, N as NotificationsJobDeps } from '../jobs-D64B_bg7.js';
6
- import '../generators-BsuXe8Mp.js';
7
- import '../types-BFmAY0P5.js';
8
- import '../wire-_coV3tJk.js';
5
+ export { a as NOTIFICATIONS_DRAIN_CRON, b as NOTIFICATIONS_DRAIN_LEASE_MS, c as NOTIFICATIONS_JOBS, d as NOTIFICATIONS_SWEEP_QUEUE, N as NotificationsJobDeps } from '../jobs--fex87-q.js';
6
+ import '../generators-qAD4fNPq.js';
7
+ import '../types-BlqZkCWZ.js';
8
+ import '../wire-BG1kuoXX.js';
9
9
  import '../template-Dlt5S54z.js';
10
10
  import '@12-apps/wiring';
11
11
 
@@ -4,7 +4,7 @@ import {
4
4
  NOTIFICATIONS_JOBS,
5
5
  NOTIFICATIONS_SWEEP_QUEUE,
6
6
  wireNotifyPort
7
- } from "../chunk-KU4FVOFA.js";
7
+ } from "../chunk-ZIR3ILFH.js";
8
8
  import {
9
9
  EMAIL_DRIVERS,
10
10
  NotificationProviderError,
@@ -25,10 +25,11 @@ import {
25
25
  smsTransport,
26
26
  webPushTransport,
27
27
  whatsAppTransport
28
- } from "../chunk-KZG7QGFA.js";
29
- import "../chunk-GGAZOLNN.js";
28
+ } from "../chunk-WVRODNXQ.js";
29
+ import "../chunk-SWOWHIFE.js";
30
30
  import "../chunk-M2TVBVH2.js";
31
- import "../chunk-BGI4BQ66.js";
31
+ import "../chunk-RTURLH5U.js";
32
+ import "../chunk-6W7INOYQ.js";
32
33
  import "../chunk-EKUSNUBT.js";
33
34
  import "../chunk-7QVYU63E.js";
34
35
  export {
@@ -60,7 +60,16 @@ interface NotificationContent {
60
60
  body: string;
61
61
  /** In-app deep link (a same-origin path such as `/orders/123`). */
62
62
  link?: string;
63
- /** Structured extras for consumers that want more than text. */
63
+ /**
64
+ * Structured extras for consumers that want more than text.
65
+ *
66
+ * ONE KEY IS RESERVED: `liveSubject` (`LIVE_SUBJECT_KEY` in `./live`) ties
67
+ * this notification to a live activity, and the WEB_PUSH transport turns it
68
+ * into the tray `tag` that collapses every push about one subject onto a
69
+ * single entry. A host already using that name for something else acquires
70
+ * that behaviour without asking for it — so it is named here, on the field a
71
+ * generator actually writes, and not only where the feature is documented.
72
+ */
64
73
  data?: Record<string, unknown>;
65
74
  }
66
75
  /** Who receives a notification. `clientId` scopes it to a tenant when set. */