@12-apps/notifications 4.9.0 → 4.10.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (64) hide show
  1. package/ADOPTING.md +60 -1
  2. package/README.md +73 -0
  3. package/dist/{chunk-BGI4BQ66.js → chunk-6W7INOYQ.js} +1 -1
  4. package/dist/chunk-6W7INOYQ.js.map +1 -0
  5. package/dist/{chunk-M5XTROYM.js → chunk-I5QUMTCN.js} +141 -17
  6. package/dist/chunk-I5QUMTCN.js.map +1 -0
  7. package/dist/chunk-RTURLH5U.js +33 -0
  8. package/dist/chunk-RTURLH5U.js.map +1 -0
  9. package/dist/{chunk-GGAZOLNN.js → chunk-SWOWHIFE.js} +2 -2
  10. package/dist/{chunk-KZG7QGFA.js → chunk-WVRODNXQ.js} +8 -4
  11. package/dist/chunk-WVRODNXQ.js.map +1 -0
  12. package/dist/{chunk-KU4FVOFA.js → chunk-ZIR3ILFH.js} +2 -2
  13. package/dist/chunk-ZY32PC34.js +231 -0
  14. package/dist/chunk-ZY32PC34.js.map +1 -0
  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-DV3Y8k7e.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-OPB3DBLJ.js} +116 -61
  31. package/dist/panel-OPB3DBLJ.js.map +1 -0
  32. package/dist/{preferences-screen-V3ZBWP7A.js → preferences-screen-SXUIGECY.js} +2 -2
  33. package/dist/react/index.d.ts +119 -7
  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/bell-button.tsx +112 -16
  45. package/src/react/create-web-notifications.tsx +40 -5
  46. package/src/react/index.ts +29 -0
  47. package/src/react/live-card.tsx +279 -0
  48. package/src/react/live-config.ts +79 -0
  49. package/src/react/live-section.tsx +179 -0
  50. package/src/react/live-seen.ts +138 -0
  51. package/src/react/panel-lazy.tsx +6 -0
  52. package/src/react/panel.tsx +144 -34
  53. package/src/react/relative-time.ts +14 -2
  54. package/src/server/transports/web-push.ts +22 -0
  55. package/src/types.ts +10 -1
  56. package/dist/chunk-BGI4BQ66.js.map +0 -1
  57. package/dist/chunk-KZG7QGFA.js.map +0 -1
  58. package/dist/chunk-M5XTROYM.js.map +0 -1
  59. package/dist/chunk-O5BVUXPO.js +0 -22
  60. package/dist/chunk-O5BVUXPO.js.map +0 -1
  61. package/dist/panel-UFXNO4AF.js.map +0 -1
  62. /package/dist/{chunk-GGAZOLNN.js.map → chunk-SWOWHIFE.js.map} +0 -0
  63. /package/dist/{chunk-KU4FVOFA.js.map → chunk-ZIR3ILFH.js.map} +0 -0
  64. /package/dist/{preferences-screen-V3ZBWP7A.js.map → preferences-screen-SXUIGECY.js.map} +0 -0
@@ -1,10 +1,12 @@
1
1
  import {
2
+ LiveSection,
2
3
  relativeTime
3
- } from "./chunk-O5BVUXPO.js";
4
+ } from "./chunk-ZY32PC34.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";
@@ -105,14 +107,15 @@ function NotificationRow({
105
107
  __name(NotificationRow, "NotificationRow");
106
108
 
107
109
  // src/react/panel.tsx
108
- import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
110
+ import { Fragment, jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
109
111
  function PanelBody({
110
112
  state,
111
113
  messages,
112
114
  onRetry,
113
115
  onLoadMore,
114
116
  onOpen,
115
- onDelete
117
+ onDelete,
118
+ hasLive
116
119
  }) {
117
120
  if (state.status === "pending" || state.status === "idle") {
118
121
  return /* @__PURE__ */ jsx2(
@@ -138,7 +141,7 @@ function PanelBody({
138
141
  }
139
142
  );
140
143
  }
141
- if (state.items.length === 0) {
144
+ if (state.items.length === 0 && !hasLive) {
142
145
  return /* @__PURE__ */ jsx2(
143
146
  EmptyState,
144
147
  {
@@ -150,53 +153,118 @@ function PanelBody({
150
153
  }
151
154
  );
152
155
  }
153
- return /* @__PURE__ */ jsxs2(Box2, { children: [
154
- state.items.map((notification) => /* @__PURE__ */ jsx2(
155
- NotificationRow,
156
- {
157
- notification,
158
- messages,
159
- onOpen,
160
- onDelete
156
+ return (
157
+ // A stable anchor for the inbox half, present whether or not it has rows.
158
+ // The panel's claim is that what is HAPPENING sits above what has already
159
+ // happened, and until this existed the only thing below the live section to
160
+ // point at was the empty state — which is exactly what stops rendering when
161
+ // something is live.
162
+ /* @__PURE__ */ jsxs2(Box2, { "data-testid": "notifications-inbox", children: [
163
+ state.items.map((notification) => /* @__PURE__ */ jsx2(
164
+ NotificationRow,
165
+ {
166
+ notification,
167
+ messages,
168
+ onOpen,
169
+ onDelete
170
+ },
171
+ notification.id
172
+ )),
173
+ state.nextCursor ? /* @__PURE__ */ jsx2(Box2, { sx: { display: "flex", justifyContent: "center", py: 1.5 }, children: /* @__PURE__ */ jsx2(
174
+ Button2,
175
+ {
176
+ variant: "outline",
177
+ color: "neutral",
178
+ size: "sm",
179
+ disabled: state.loadingMore,
180
+ onClick: onLoadMore,
181
+ dataTestId: "notifications-load-more",
182
+ children: state.loadingMore ? messages.loadingMore : messages.loadMore
183
+ }
184
+ ) }) : null
185
+ ] })
186
+ );
187
+ }
188
+ __name(PanelBody, "PanelBody");
189
+ function usePanelOpeners(store, onClose, onNavigate) {
190
+ const follow = useCallback(
191
+ (link) => {
192
+ if (!link || !onNavigate) return;
193
+ onClose();
194
+ onNavigate(link);
195
+ },
196
+ [onClose, onNavigate]
197
+ );
198
+ return {
199
+ openNotification: useCallback(
200
+ (notification) => {
201
+ if (notification.readAt === null) store.markRead([notification.id]);
202
+ follow(notification.link);
161
203
  },
162
- notification.id
163
- )),
164
- state.nextCursor ? /* @__PURE__ */ jsx2(Box2, { sx: { display: "flex", justifyContent: "center", py: 1.5 }, children: /* @__PURE__ */ jsx2(
204
+ [store, follow]
205
+ ),
206
+ openLive: useCallback((activity) => follow(activity.link), [follow])
207
+ };
208
+ }
209
+ __name(usePanelOpeners, "usePanelOpeners");
210
+ function PanelInbox({
211
+ state,
212
+ messages,
213
+ store,
214
+ onOpen,
215
+ hasLive
216
+ }) {
217
+ const hasUnread = state.items.some((item) => item.readAt === null);
218
+ return /* @__PURE__ */ jsxs2(Fragment, { children: [
219
+ hasUnread ? /* @__PURE__ */ jsx2(Box2, { sx: { display: "flex", justifyContent: "flex-end", pb: 1 }, children: /* @__PURE__ */ jsx2(
165
220
  Button2,
166
221
  {
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
222
+ variant: "ghost",
223
+ color: "primary",
224
+ size: "xs",
225
+ onClick: () => store.markAllRead(),
226
+ dataTestId: "notifications-mark-all-read",
227
+ children: messages.markAllRead
228
+ }
229
+ ) }) : null,
230
+ /* @__PURE__ */ jsx2(
231
+ PanelBody,
232
+ {
233
+ state,
234
+ messages,
235
+ onRetry: () => store.invalidate(),
236
+ onLoadMore: () => store.loadMore(),
237
+ onOpen,
238
+ onDelete: (id) => store.remove(id),
239
+ hasLive
174
240
  }
175
- ) }) : null
241
+ )
176
242
  ] });
177
243
  }
178
- __name(PanelBody, "PanelBody");
244
+ __name(PanelInbox, "PanelInbox");
179
245
  function NotificationsPanel({
180
246
  open,
181
247
  onClose,
182
248
  onNavigate,
183
249
  store,
184
- messages
250
+ messages,
251
+ live,
252
+ liveSeen
185
253
  }) {
186
254
  const theme = useTheme();
187
255
  const isMobile = useMediaQuery(theme.breakpoints.down("sm"));
188
256
  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);
257
+ const { openNotification, openLive } = usePanelOpeners(store, onClose, onNavigate);
258
+ const renderInbox = /* @__PURE__ */ __name((liveCount) => /* @__PURE__ */ jsx2(
259
+ PanelInbox,
260
+ {
261
+ state,
262
+ messages,
263
+ store,
264
+ onOpen: openNotification,
265
+ hasLive: liveCount > 0
266
+ }
267
+ ), "renderInbox");
200
268
  return /* @__PURE__ */ jsxs2(
201
269
  Drawer,
202
270
  {
@@ -208,30 +276,17 @@ function NotificationsPanel({
208
276
  dataTestId: "notifications-panel",
209
277
  children: [
210
278
  /* @__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
- ] })
279
+ /* @__PURE__ */ jsx2(DrawerContent, { children: live ? /* @__PURE__ */ jsx2(
280
+ LiveSection,
281
+ {
282
+ config: live,
283
+ messages,
284
+ active: open,
285
+ ...onNavigate ? { onOpen: openLive } : {},
286
+ ...liveSeen ? { seen: liveSeen } : {},
287
+ children: renderInbox
288
+ }
289
+ ) : renderInbox(0) })
235
290
  ]
236
291
  }
237
292
  );
@@ -240,4 +295,4 @@ __name(NotificationsPanel, "NotificationsPanel");
240
295
  export {
241
296
  NotificationsPanel
242
297
  };
243
- //# sourceMappingURL=panel-UFXNO4AF.js.map
298
+ //# sourceMappingURL=panel-OPB3DBLJ.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 type { LiveSeenStore } from './live-seen';\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 * Whether the live section above has anything in it.\n *\n * The empty state is a CLAIM about the whole panel — \"nenhuma notificação\" —\n * and a live entry is a notification, so an inbox with no rows under a pinned\n * pedido is not empty. Without this the panel said both things at once.\n */\n hasLive: boolean;\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 hasLive,\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 && !hasLive) {\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 // A stable anchor for the inbox half, present whether or not it has rows.\n // The panel's claim is that what is HAPPENING sits above what has already\n // happened, and until this existed the only thing below the live section to\n // point at was the empty state — which is exactly what stops rendering when\n // something is live.\n <Box data-testid=\"notifications-inbox\">\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\n/**\n * Everything BELOW the live section: the mark-all control and the list.\n *\n * Its own component rather than a block inside the panel because it is rendered\n * from two places — through `LiveSection`, and directly when the host turned\n * live activities off — and because the panel is at its line ceiling. \"Marcar\n * todas como lidas\" belongs to the INBOX and travels with it: a live entry has\n * nothing to mark.\n */\nfunction PanelInbox({\n state,\n messages,\n store,\n onOpen,\n hasLive,\n}: {\n state: InboxState;\n messages: NotificationMessages;\n store: InboxStore;\n onOpen: (notification: InboxNotification) => void;\n hasLive: boolean;\n}): JSX.Element {\n const hasUnread = state.items.some((item) => item.readAt === null);\n return (\n <>\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={onOpen}\n onDelete={(id) => store.remove(id)}\n hasLive={hasLive}\n />\n </>\n );\n}\n\nexport function NotificationsPanel({\n open,\n onClose,\n onNavigate,\n store,\n messages,\n live,\n liveSeen,\n}: NotificationsPanelProps & {\n store: InboxStore;\n messages: NotificationMessages;\n live?: LiveActivitiesConfig;\n liveSeen?: LiveSeenStore;\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 // A function rather than an element so the live count can reach it: with a\n // live section it is called by `LiveSection` — the only place the host's hook\n // may be called — and without one it is called here with zero.\n const renderInbox = (liveCount: number): JSX.Element => (\n <PanelInbox\n state={state}\n messages={messages}\n store={store}\n onOpen={openNotification}\n hasLive={liveCount > 0}\n />\n );\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 {...(liveSeen ? { seen: liveSeen } : {})}\n >\n {renderInbox}\n </LiveSection>\n ) : (\n renderInbox(0)\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,50 @@
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';
3
- import { JSX } from 'react';
4
- import { N as NotificationMessages } from '../wire-_coV3tJk.js';
5
- import '../types-BFmAY0P5.js';
1
+ import { L as LiveActivitiesConfig, N as NotificationsApiClient } from '../create-web-notifications-DV3Y8k7e.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-DV3Y8k7e.js';
3
+ import { JSX, ReactNode } from 'react';
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';
8
+
9
+ /**
10
+ * What the reader has already been shown, so the bell can say NEW rather than
11
+ * merely PRESENT.
12
+ *
13
+ * A live activity is unlike an inbox row in the one way that matters here: it
14
+ * stays on the panel for as long as the thing is happening, so its presence
15
+ * cannot mean "you have not seen this". A pedido that has been `Preparo` for
16
+ * ten minutes is still live and still worth counting, but nothing has happened
17
+ * — and a badge that shouts for a subject the reader has already looked at is a
18
+ * badge people stop reading.
19
+ *
20
+ * So presence and novelty are answered separately: the COUNT comes from how
21
+ * many are live, and the TONE comes from this. The panel writes it — being on
22
+ * screen is what seen means — and the bell reads it.
23
+ *
24
+ * ## Per subject, not one watermark
25
+ *
26
+ * A single "newest instant already seen" is smaller and was the first cut, and
27
+ * it is wrong in a way that shows up in normal use: a pedido placed ten minutes
28
+ * ago but only now reaching the client arrives with an `updatedAt` BEHIND the
29
+ * watermark, and would be silently marked as already seen. The reader has never
30
+ * laid eyes on it. Keyed by subject, an id that has not been recorded is new
31
+ * whatever its clock says.
32
+ *
33
+ * Bounded by pruning rather than by expiry: every write keeps only the subjects
34
+ * that are live at that moment, so the record can never outgrow the number of
35
+ * things happening at once. A subject that finishes and later comes back is
36
+ * news again, which is correct — it is a different occurrence.
37
+ */
38
+
39
+ /** id -> the `updatedAt` that was on screen. */
40
+ type SeenMap = Readonly<Record<string, string>>;
41
+ interface LiveSeenStore {
42
+ /** What has been shown, keyed by subject id. */
43
+ read: () => SeenMap;
44
+ /** Record that exactly these are on screen now, forgetting subjects that are not. */
45
+ mark: (activities: readonly LiveActivity[]) => void;
46
+ subscribe: (listener: () => void) => () => void;
47
+ }
6
48
 
7
49
  /** Inline SVG bell (no icon-library dependency in this package). */
8
50
 
@@ -11,12 +53,82 @@ declare function BellIcon({ size, dim, }: {
11
53
  dim?: boolean;
12
54
  }): JSX.Element;
13
55
 
56
+ /**
57
+ * The pinned block at the top of the panel: everything that is happening NOW,
58
+ * above everything that has already happened.
59
+ *
60
+ * ## Why it is here and not a second surface
61
+ *
62
+ * The notification centre is where a person goes to find out what they missed.
63
+ * Splitting "happening" into its own bell would make them check two places to
64
+ * answer one question, and the half they would stop checking is the one that
65
+ * only has something in it occasionally — which is this one. Above the list,
66
+ * inside the same drawer, it is on the path they already walk.
67
+ *
68
+ * ## What it deliberately does NOT do
69
+ *
70
+ * - It does not touch `unread`. A live entry is not news; counting it would put
71
+ * a number on the bell that no amount of reading can clear.
72
+ * - It renders nothing at all when there is nothing live — no heading, no empty
73
+ * state, no reserved space. A panel with one permanent empty section in it is
74
+ * a panel that has taught its reader to skip the top.
75
+ * - It does not fetch. `useActivities` is the host's, and `active` tells it
76
+ * whether anyone is looking.
77
+ */
78
+
79
+ interface LiveSectionProps {
80
+ config: LiveActivitiesConfig;
81
+ messages: NotificationMessages;
82
+ /** Whether the panel is open — passed straight through to the host's hook. */
83
+ active: boolean;
84
+ /**
85
+ * Follow a card's link.
86
+ *
87
+ * Optional, and the panel omits it for a host with no router: a card that
88
+ * cannot go anywhere renders as text rather than as a named control that
89
+ * does nothing.
90
+ */
91
+ onOpen?: (activity: LiveActivity) => void;
92
+ /**
93
+ * The rest of the panel, given how many entries are live.
94
+ *
95
+ * A render prop rather than a sibling, because the count is knowable only
96
+ * where the host's hook is CALLED, and it cannot be called anywhere else:
97
+ * `live` is optional on the panel, so reading it there would mean calling a
98
+ * hook conditionally — the failure React reports as a crash in some unrelated
99
+ * component.
100
+ *
101
+ * The inbox needs the number for exactly one decision, and it is the decision
102
+ * this section exists to inform: whether "no notifications" is true. A live
103
+ * entry IS a notification, so a panel showing one under that sentence is
104
+ * contradicting itself.
105
+ */
106
+ children?: (liveCount: number) => ReactNode;
107
+ /**
108
+ * Where "the reader has seen these" is recorded, for the bell to read.
109
+ *
110
+ * Written HERE because this is the component that puts them on screen, and
111
+ * being on screen is what seen means. Optional so the section stays usable by
112
+ * a host that mounts it outside the panel.
113
+ */
114
+ seen?: LiveSeenStore;
115
+ }
116
+ declare function LiveSection({ config, messages, active, onOpen, children, seen, }: LiveSectionProps): JSX.Element;
117
+
14
118
  /**
15
119
  * "há 5 min"-style relative timestamp, falling back to an absolute date for
16
120
  * anything older than a week. Every word comes from the messages table, so a
17
121
  * host in another locale changes the copy and the locale together.
122
+ *
123
+ * `now` is a parameter rather than a read, and the live section is why. A
124
+ * relative phrase is only true for the instant it was computed, so something
125
+ * has to CAUSE the render that recomputes it — and the live entries' own data
126
+ * cannot: a host backed by react-query gets the previous object back whenever a
127
+ * poll is deep-equal (`structuralSharing`, on by default), which within one
128
+ * stage it always is. The section therefore ticks a clock and hands it down.
129
+ * Defaulted, so every existing caller reads the wall clock exactly as before.
18
130
  */
19
- declare function relativeTime(iso: string, messages: NotificationMessages): string;
131
+ declare function relativeTime(iso: string, messages: NotificationMessages, now?: number): string;
20
132
 
21
133
  /**
22
134
  * The browser half of Web Push: register the service worker, ask permission,
@@ -44,4 +156,4 @@ declare function enableWebPush(api: NotificationsApiClient, swPath?: string): Pr
44
156
  /** Disable flow: unsubscribe the browser and drop the server-side row. */
45
157
  declare function disableWebPush(api: NotificationsApiClient): Promise<void>;
46
158
 
47
- export { BellIcon, NotificationsApiClient, type PushSetupResult, disableWebPush, enableWebPush, getExistingPushSubscription, pushSupported, relativeTime };
159
+ 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-I5QUMTCN.js";
7
7
  import {
8
+ LiveSection,
8
9
  relativeTime
9
- } from "../chunk-O5BVUXPO.js";
10
+ } from "../chunk-ZY32PC34.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. */
@@ -1,5 +1,5 @@
1
- import { c as WebPushSender } from '../web-push-D-ysQd5h.js';
2
- import '../types-BFmAY0P5.js';
1
+ import { c as WebPushSender } from '../web-push-Dnyaha2z.js';
2
+ import '../types-BlqZkCWZ.js';
3
3
 
4
4
  /**
5
5
  * `@12-apps/notifications/web-push` — the VAPID sender, behind its own subpath.
@@ -1,4 +1,4 @@
1
- import { g as NotificationLogger, d as NotificationContent, j as NotificationTransport } from './types-BFmAY0P5.js';
1
+ import { g as NotificationLogger, d as NotificationContent, j as NotificationTransport } from './types-BlqZkCWZ.js';
2
2
 
3
3
  /**
4
4
  * The DRIVER port — the reason a host adds a vendor with a config entry and no
@@ -98,6 +98,26 @@ interface WebPushMessage {
98
98
  body: string;
99
99
  link: string | null;
100
100
  data: Record<string, unknown>;
101
+ /**
102
+ * The OS tray tag, or `null` for an ordinary event.
103
+ *
104
+ * Present for a notification about a LIVE SUBJECT (see `../../live`), and
105
+ * this is the whole of the live feature's reach onto a locked phone: a tag
106
+ * makes the next push about the same subject REPLACE the one already in the
107
+ * tray — silently — instead of stacking under it. Four stages then cost one
108
+ * entry and one buzz, and the entry that remains is the current one.
109
+ *
110
+ * {@link formatWebPush} always sets it, `null` included, so on the WIRE the
111
+ * key is always there and a service worker reads one shape. A worker that
112
+ * ignores it keeps today's behaviour exactly, which is what makes this safe
113
+ * to ship ahead of the workers.
114
+ *
115
+ * OPTIONAL in the type, and only in the type: a host that hand-writes its own
116
+ * `NotificationTransport<WebPushMessage>` — a fixture, a second driver —
117
+ * would otherwise stop compiling on a field it has no way to know about and
118
+ * no reason to set.
119
+ */
120
+ tag?: string | null;
101
121
  }
102
122
  interface WebPushDriverDeclaration extends DriverDeclarationBase {
103
123
  channel: 'WEB_PUSH';
@@ -1,4 +1,4 @@
1
- import { b as NotificationCategory, c as NotificationChannel } from './types-BFmAY0P5.js';
1
+ import { b as NotificationCategory, c as NotificationChannel } from './types-BlqZkCWZ.js';
2
2
 
3
3
  /**
4
4
  * Every sentence this package can say to a USER, stated by the HOST.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@12-apps/notifications",
3
- "version": "4.9.0",
3
+ "version": "4.10.1",
4
4
  "type": "module",
5
5
  "sideEffects": false,
6
6
  "description": "Plug-and-play notification system (12-15): an always-on in-app inbox, per-user × per-category channel preferences, and email / SMS / WhatsApp / web-push transports behind vendor DRIVERS so a second provider is a config entry. Framework-free core (.), host-mounted backend surface (./server: inbox / preferences / push-subscription endpoints, the channel router with delivery records + retry sweep, the permission fan-out, duck-typed Prisma seam), Hono adapter (./hono), React surface (./react: bell + badge, inbox drawer, preferences screen), VAPID sender (./web-push) and the package-owned Prisma partial + migrations. Standardized adoption contract in ADOPTING.md.",
@@ -71,7 +71,7 @@
71
71
  "prisma:sync:check": "node scripts/sync-notifications-schema.mjs --check"
72
72
  },
73
73
  "dependencies": {
74
- "@12-apps/ui": "^6.14.0"
74
+ "@12-apps/ui": "^6.18.0"
75
75
  },
76
76
  "peerDependencies": {
77
77
  "@12-apps/wiring": ">=1.3.0",
package/src/index.ts CHANGED
@@ -65,6 +65,16 @@ export {
65
65
  type NotificationMessages,
66
66
  } from './messages';
67
67
 
68
+ export {
69
+ LIVE_PUSH_TAG_PREFIX,
70
+ LIVE_SUBJECT_KEY,
71
+ liveActivityLane,
72
+ livePushTag,
73
+ type LiveActivity,
74
+ type LiveActivityLane,
75
+ type LiveActivityStep,
76
+ } from './live';
77
+
68
78
  export {
69
79
  inboxWire,
70
80
  type InboxNotification,