@12-apps/notifications 4.7.0 → 4.8.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 (59) hide show
  1. package/ADOPTING.md +32 -0
  2. package/dist/{chunk-TIGTBSAQ.js → chunk-7234ROOP.js} +11 -5
  3. package/dist/chunk-7234ROOP.js.map +1 -0
  4. package/dist/{chunk-Y34FX24X.js → chunk-BGI4BQ66.js} +2 -10
  5. package/dist/chunk-BGI4BQ66.js.map +1 -0
  6. package/dist/chunk-BW723CX2.js +214 -0
  7. package/dist/chunk-BW723CX2.js.map +1 -0
  8. package/dist/chunk-CQZMTFPY.js +76 -0
  9. package/dist/chunk-CQZMTFPY.js.map +1 -0
  10. package/dist/{chunk-HQU4R4SG.js → chunk-GGAZOLNN.js} +2 -2
  11. package/dist/{chunk-QRAXX3GR.js → chunk-KU4FVOFA.js} +2 -2
  12. package/dist/chunk-M2TVBVH2.js +15 -0
  13. package/dist/chunk-M2TVBVH2.js.map +1 -0
  14. package/dist/chunk-M5XTROYM.js +263 -0
  15. package/dist/chunk-M5XTROYM.js.map +1 -0
  16. package/dist/chunk-O5BVUXPO.js +22 -0
  17. package/dist/chunk-O5BVUXPO.js.map +1 -0
  18. package/dist/{create-api-notifications-B3u6Kx3x.d.ts → create-api-notifications-WaexrZnn.d.ts} +12 -4
  19. package/dist/{create-web-notifications-BpNR8qH3.d.ts → create-web-notifications-DrgqtU4Q.d.ts} +15 -4
  20. package/dist/{generators-B9xt3sRh.d.ts → generators-CH5m8Ucj.d.ts} +1 -1
  21. package/dist/hono/index.d.ts +5 -5
  22. package/dist/hono/index.js +4 -3
  23. package/dist/hono/index.js.map +1 -1
  24. package/dist/index.d.ts +3 -3
  25. package/dist/index.js +5 -3
  26. package/dist/{jobs-BDZ7aGHV.d.ts → jobs-CcsSA0YR.d.ts} +1 -1
  27. package/dist/manifest/server.d.ts +6 -6
  28. package/dist/manifest/server.js +5 -4
  29. package/dist/manifest/server.js.map +1 -1
  30. package/dist/manifest/web.d.ts +3 -3
  31. package/dist/manifest/web.js +3 -2
  32. package/dist/manifest/web.js.map +1 -1
  33. package/dist/panel-UFXNO4AF.js +243 -0
  34. package/dist/panel-UFXNO4AF.js.map +1 -0
  35. package/dist/preferences-screen-V3ZBWP7A.js +294 -0
  36. package/dist/preferences-screen-V3ZBWP7A.js.map +1 -0
  37. package/dist/react/index.d.ts +4 -4
  38. package/dist/react/index.js +17 -11
  39. package/dist/server/index.d.ts +7 -7
  40. package/dist/server/index.js +5 -4
  41. package/dist/{types-CXLAG3UU.d.ts → types-CYJN1xCM.d.ts} +39 -2
  42. package/dist/web-push/index.d.ts +2 -2
  43. package/dist/{web-push-Cs14Wp9u.d.ts → web-push-iER5StBm.d.ts} +1 -1
  44. package/dist/{wire-6dzyfDE7.d.ts → wire-CmRrB3_s.d.ts} +1 -1
  45. package/package.json +3 -3
  46. package/src/index.ts +1 -0
  47. package/src/react/create-web-notifications.tsx +19 -10
  48. package/src/react/page-lazy.tsx +73 -0
  49. package/src/react/panel-lazy.tsx +74 -0
  50. package/src/server/db.ts +12 -1
  51. package/src/server/dispatch.ts +4 -0
  52. package/src/server/router.ts +17 -1
  53. package/src/types.ts +39 -1
  54. package/dist/chunk-6HLHQDKS.js +0 -1022
  55. package/dist/chunk-6HLHQDKS.js.map +0 -1
  56. package/dist/chunk-TIGTBSAQ.js.map +0 -1
  57. package/dist/chunk-Y34FX24X.js.map +0 -1
  58. /package/dist/{chunk-HQU4R4SG.js.map → chunk-GGAZOLNN.js.map} +0 -0
  59. /package/dist/{chunk-QRAXX3GR.js.map → chunk-KU4FVOFA.js.map} +0 -0
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/react/panel.tsx","../src/react/row.tsx"],"sourcesContent":["/**\n * The notification-centre slide-over: newest-first list with unread styling,\n * per-item open (marks read + deep-links), soft delete, mark-all, empty /\n * loading / error states and a \"load more\" cursor pager.\n *\n * Rendering is app-agnostic — the host passes `onNavigate` (its router's\n * navigate) for deep links. Without one a link is simply not followed, which is\n * what lets the panel mount in a host that has no router at all.\n */\nimport { useCallback, type JSX } from 'react';\n\nimport { EmptyState } from '@12-apps/ui/data-display/EmptyState';\nimport { LoadingState } from '@12-apps/ui/data-display/LoadingState';\nimport { Button } from '@12-apps/ui/form/Button';\nimport { Drawer, DrawerContent, DrawerHeader } from '@12-apps/ui/layout/Drawer';\nimport { Box } from '@12-apps/ui/mui/Box';\nimport { useMediaQuery } from '@12-apps/ui/mui/useMediaQuery';\nimport { useTheme } from '@12-apps/ui/mui/styles';\n\nimport type { NotificationMessages } from '../messages';\nimport type { InboxNotification } from '../wire';\n\nimport { BellIcon } from './bell-icon';\nimport { useInboxList } from './hooks';\nimport type { InboxState, InboxStore } from './inbox-state';\nimport { NotificationRow } from './row';\n\ninterface PanelBodyProps {\n state: InboxState;\n messages: NotificationMessages;\n onRetry: () => void;\n onLoadMore: () => void;\n onOpen: (notification: InboxNotification) => void;\n onDelete: (id: string) => void;\n}\n\n/** The scrollable panel body: loading / error / empty / the list + pager. */\nfunction PanelBody({\n state,\n messages,\n onRetry,\n onLoadMore,\n onOpen,\n onDelete,\n}: PanelBodyProps): JSX.Element {\n if (state.status === 'pending' || state.status === 'idle') {\n return (\n <LoadingState\n variant=\"spinner\"\n message={messages.loading}\n size=\"md\"\n dataTestId=\"notifications-loading\"\n />\n );\n }\n if (state.status === 'error') {\n return (\n <EmptyState\n variant=\"minimal\"\n title={messages.loadFailedTitle}\n description={messages.loadFailedBody}\n onRefresh={onRetry}\n refreshLabel={messages.retry}\n dataTestId=\"notifications-error\"\n />\n );\n }\n if (state.items.length === 0) {\n return (\n <EmptyState\n variant=\"illustrated\"\n illustration={<BellIcon size={44} dim />}\n title={messages.emptyTitle}\n description={messages.emptyBody}\n dataTestId=\"notifications-empty\"\n />\n );\n }\n return (\n <Box>\n {state.items.map((notification) => (\n <NotificationRow\n key={notification.id}\n notification={notification}\n messages={messages}\n onOpen={onOpen}\n onDelete={onDelete}\n />\n ))}\n {state.nextCursor ? (\n <Box sx={{ display: 'flex', justifyContent: 'center', py: 1.5 }}>\n <Button\n variant=\"outline\"\n color=\"neutral\"\n size=\"sm\"\n disabled={state.loadingMore}\n onClick={onLoadMore}\n dataTestId=\"notifications-load-more\"\n >\n {state.loadingMore ? messages.loadingMore : messages.loadMore}\n </Button>\n </Box>\n ) : null}\n </Box>\n );\n}\n\nexport interface NotificationsPanelProps {\n open: boolean;\n onClose: () => void;\n /** Navigate to a notification's in-app link (the host's router). */\n onNavigate?: (link: string) => void;\n}\n\nexport function NotificationsPanel({\n open,\n onClose,\n onNavigate,\n store,\n messages,\n}: NotificationsPanelProps & {\n store: InboxStore;\n messages: NotificationMessages;\n}): JSX.Element {\n // `useTheme` from @mui/material/styles falls back to the DEFAULT theme when\n // no provider is mounted, where the callback form of `useMediaQuery` would\n // hand the callback a null theme and throw. A published component must render\n // in a host that has not wrapped it yet.\n const theme = useTheme();\n const isMobile = useMediaQuery(theme.breakpoints.down('sm'));\n const state = useInboxList(store, open);\n\n const openNotification = useCallback(\n (notification: InboxNotification) => {\n if (notification.readAt === null) store.markRead([notification.id]);\n if (notification.link && onNavigate) {\n onClose();\n onNavigate(notification.link);\n }\n },\n [store, onClose, onNavigate],\n );\n\n const hasUnread = state.items.some((item) => item.readAt === null);\n\n return (\n <Drawer\n open={open}\n onClose={onClose}\n anchor=\"right\"\n variant=\"right\"\n width={isMobile ? '100vw' : 400}\n dataTestId=\"notifications-panel\"\n >\n <DrawerHeader onClose={onClose}>{messages.panelTitle}</DrawerHeader>\n <DrawerContent>\n {hasUnread ? (\n <Box sx={{ display: 'flex', justifyContent: 'flex-end', pb: 1 }}>\n <Button\n variant=\"ghost\"\n color=\"primary\"\n size=\"xs\"\n onClick={() => store.markAllRead()}\n dataTestId=\"notifications-mark-all-read\"\n >\n {messages.markAllRead}\n </Button>\n </Box>\n ) : null}\n <PanelBody\n state={state}\n messages={messages}\n onRetry={() => store.invalidate()}\n onLoadMore={() => store.loadMore()}\n onOpen={openNotification}\n onDelete={(id) => store.remove(id)}\n />\n </DrawerContent>\n </Drawer>\n );\n}\n","/** One inbox row: unread accent, content (opens/marks read), timestamp, delete. */\nimport type { JSX } from 'react';\n\nimport { Button } from '@12-apps/ui/form/Button';\nimport { Box } from '@12-apps/ui/mui/Box';\nimport { alpha, type Theme } from '@12-apps/ui/mui/styles';\nimport { Text } from '@12-apps/ui/typography/Text';\n\nimport type { NotificationMessages } from '../messages';\nimport type { InboxNotification } from '../wire';\n\nimport { relativeTime } from './relative-time';\n\nconst contentButtonSx = {\n flex: 1,\n minWidth: 0,\n display: 'flex',\n flexDirection: 'column',\n gap: 0.25,\n textAlign: 'left',\n border: 'none',\n background: 'none',\n p: 0,\n cursor: 'pointer',\n color: 'text.primary',\n fontFamily: 'inherit',\n} as const;\n\nconst unreadDotSx = {\n width: 8,\n height: 8,\n borderRadius: '50%',\n bgcolor: 'primary.main',\n flex: '0 0 auto',\n} as const;\n\nexport function NotificationRow({\n notification,\n messages,\n onOpen,\n onDelete,\n}: {\n notification: InboxNotification;\n messages: NotificationMessages;\n onOpen: (notification: InboxNotification) => void;\n onDelete: (id: string) => void;\n}): JSX.Element {\n const unread = notification.readAt === null;\n return (\n <Box\n data-testid={`notification-${notification.id}`}\n sx={{\n display: 'flex',\n alignItems: 'flex-start',\n gap: 1,\n py: 1.5,\n px: 1,\n borderBottom: '1px solid',\n borderColor: 'divider',\n bgcolor: unread ? (t: Theme) => alpha(t.palette.primary.main, 0.06) : 'transparent',\n }}\n >\n <Box\n component=\"button\"\n type=\"button\"\n onClick={() => onOpen(notification)}\n aria-label={\n unread ? `${notification.title} (${messages.unreadSuffix})` : notification.title\n }\n sx={contentButtonSx}\n >\n <Box sx={{ display: 'flex', alignItems: 'center', gap: 0.75 }}>\n {unread ? <Box aria-hidden sx={unreadDotSx} /> : null}\n <Text variant=\"body\" size=\"sm\" weight={unread ? 'bold' : 'medium'} as=\"span\">\n {notification.title}\n </Text>\n </Box>\n <Text variant=\"caption\" size=\"xs\" color=\"secondary\" as=\"span\">\n {notification.body}\n </Text>\n <Text variant=\"caption\" size=\"xs\" color=\"secondary\" as=\"span\" italic>\n {relativeTime(notification.createdAt, messages)}\n </Text>\n </Box>\n\n <Button\n variant=\"ghost\"\n color=\"neutral\"\n size=\"xs\"\n aria-label={messages.deleteOne(notification.title)}\n onClick={() => onDelete(notification.id)}\n dataTestId={`notification-delete-${notification.id}`}\n >\n ✕\n </Button>\n </Box>\n );\n}\n"],"mappings":"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","names":["Button","Box","jsx","jsxs","Box","Button"]}
@@ -0,0 +1,294 @@
1
+ import {
2
+ enableWebPush,
3
+ getExistingPushSubscription
4
+ } from "./chunk-CQZMTFPY.js";
5
+ import {
6
+ NOTIFICATION_CHANNELS
7
+ } from "./chunk-BGI4BQ66.js";
8
+ import {
9
+ __name
10
+ } from "./chunk-7QVYU63E.js";
11
+
12
+ // src/react/preferences-screen.tsx
13
+ import { useCallback, useEffect as useEffect2, useState as useState2 } from "react";
14
+ import { LoadingState } from "@12-apps/ui/data-display/LoadingState";
15
+ import { Switch } from "@12-apps/ui/form/Switch";
16
+ import { Box as Box2 } from "@12-apps/ui/mui/Box";
17
+ import { Text as Text2 } from "@12-apps/ui/typography/Text";
18
+
19
+ // src/react/web-push-setup.tsx
20
+ import { useEffect, useState } from "react";
21
+ import { Button } from "@12-apps/ui/form/Button";
22
+ import { Box } from "@12-apps/ui/mui/Box";
23
+ import { Text } from "@12-apps/ui/typography/Text";
24
+ import { jsx, jsxs } from "react/jsx-runtime";
25
+ var cardSx = {
26
+ p: 2,
27
+ border: "1px solid",
28
+ borderColor: "divider",
29
+ borderRadius: 2
30
+ };
31
+ function InstallFirstHint({ hint }) {
32
+ return /* @__PURE__ */ jsxs(Box, { sx: cardSx, "data-testid": "web-push-install-hint", children: [
33
+ /* @__PURE__ */ jsx(Text, { variant: "body", size: "sm", weight: "semibold", as: "p", children: hint.title }),
34
+ /* @__PURE__ */ jsx(Text, { variant: "caption", size: "xs", color: "secondary", as: "p", children: hint.body })
35
+ ] });
36
+ }
37
+ __name(InstallFirstHint, "InstallFirstHint");
38
+ async function resolveState(api) {
39
+ const subscription = await getExistingPushSubscription();
40
+ if (!subscription) return "idle";
41
+ const registration = await api.getPushRegistration({ endpoint: subscription.endpoint }).catch(() => null);
42
+ return registration?.registered ? "on" : "idle";
43
+ }
44
+ __name(resolveState, "resolveState");
45
+ function statusText(state, messages) {
46
+ if (state === "on") return messages.devicePushOn;
47
+ if (state === "denied") return messages.devicePushDenied;
48
+ if (state === "failed") return messages.devicePushFailed;
49
+ return messages.devicePushIdle;
50
+ }
51
+ __name(statusText, "statusText");
52
+ function WebPushDeviceSetup({
53
+ available,
54
+ api,
55
+ messages,
56
+ config
57
+ }) {
58
+ const [state, setState] = useState("checking");
59
+ const [needsInstall] = useState(() => config.needsInstallFirst?.() ?? false);
60
+ useEffect(() => {
61
+ let cancelled = false;
62
+ void resolveState(api).then((next) => {
63
+ if (!cancelled) setState(next);
64
+ });
65
+ return () => {
66
+ cancelled = true;
67
+ };
68
+ }, [api]);
69
+ if (!available) return null;
70
+ if (needsInstall && config.installHint) return /* @__PURE__ */ jsx(InstallFirstHint, { hint: config.installHint });
71
+ const enable = /* @__PURE__ */ __name(async () => {
72
+ setState("busy");
73
+ const result = await enableWebPush(api, config.swPath);
74
+ if (result.ok) setState("on");
75
+ else setState(result.reason === "permission-denied" ? "denied" : "failed");
76
+ }, "enable");
77
+ return /* @__PURE__ */ jsxs(
78
+ Box,
79
+ {
80
+ sx: {
81
+ ...cardSx,
82
+ display: "flex",
83
+ alignItems: "center",
84
+ justifyContent: "space-between",
85
+ gap: 2
86
+ },
87
+ "data-testid": "web-push-device-setup",
88
+ children: [
89
+ /* @__PURE__ */ jsxs(Box, { sx: { minWidth: 0 }, children: [
90
+ /* @__PURE__ */ jsx(Text, { variant: "body", size: "sm", weight: "semibold", as: "p", children: messages.devicePushTitle }),
91
+ /* @__PURE__ */ jsx(Text, { variant: "caption", size: "xs", color: "secondary", as: "p", children: statusText(state, messages) })
92
+ ] }),
93
+ state !== "on" ? /* @__PURE__ */ jsx(
94
+ Button,
95
+ {
96
+ variant: "outline",
97
+ color: "primary",
98
+ size: "sm",
99
+ disabled: state === "busy" || state === "checking",
100
+ onClick: () => void enable(),
101
+ dataTestId: "web-push-enable",
102
+ children: state === "busy" ? messages.devicePushEnabling : messages.devicePushEnable
103
+ }
104
+ ) : null
105
+ ]
106
+ }
107
+ );
108
+ }
109
+ __name(WebPushDeviceSetup, "WebPushDeviceSetup");
110
+
111
+ // src/react/preferences-screen.tsx
112
+ import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
113
+ function CategoryCard({
114
+ category,
115
+ channels,
116
+ availability,
117
+ messages,
118
+ onToggle
119
+ }) {
120
+ const labels = messages.categoryLabels[category];
121
+ return /* @__PURE__ */ jsxs2(
122
+ Box2,
123
+ {
124
+ sx: { p: 2, border: "1px solid", borderColor: "divider", borderRadius: 2 },
125
+ "data-testid": `prefs-${category}`,
126
+ children: [
127
+ /* @__PURE__ */ jsx2(Text2, { variant: "body", size: "sm", weight: "semibold", as: "p", children: labels?.title ?? messages.categoryFallbackTitle(category) }),
128
+ labels?.description ? /* @__PURE__ */ jsx2(Text2, { variant: "caption", size: "xs", color: "secondary", as: "p", children: labels.description }) : null,
129
+ /* @__PURE__ */ jsx2(
130
+ Box2,
131
+ {
132
+ sx: {
133
+ mt: 1.5,
134
+ display: "grid",
135
+ gridTemplateColumns: { xs: "1fr 1fr", sm: "repeat(4, 1fr)" },
136
+ gap: 1
137
+ },
138
+ children: NOTIFICATION_CHANNELS.map((channel) => /* @__PURE__ */ jsx2(
139
+ Switch,
140
+ {
141
+ size: "sm",
142
+ color: "primary",
143
+ label: messages.channelLabels[channel] ?? channel,
144
+ checked: channels[channel] && availability[channel],
145
+ disabled: !availability[channel],
146
+ onChange: (_, checked) => onToggle(channel, checked),
147
+ dataTestId: `prefs-${category}-${channel}`
148
+ },
149
+ channel
150
+ ))
151
+ }
152
+ )
153
+ ]
154
+ }
155
+ );
156
+ }
157
+ __name(CategoryCard, "CategoryCard");
158
+ function UnavailableHints({
159
+ availability,
160
+ messages
161
+ }) {
162
+ const unavailable = NOTIFICATION_CHANNELS.filter((channel) => !availability[channel]);
163
+ if (unavailable.length === 0) return null;
164
+ return /* @__PURE__ */ jsx2(Box2, { sx: { display: "flex", flexDirection: "column", gap: 0.5 }, children: unavailable.map((channel) => /* @__PURE__ */ jsxs2(
165
+ Text2,
166
+ {
167
+ variant: "caption",
168
+ size: "xs",
169
+ color: "secondary",
170
+ as: "p",
171
+ "data-testid": `prefs-hint-${channel}`,
172
+ children: [
173
+ messages.channelLabels[channel] ?? channel,
174
+ ":",
175
+ " ",
176
+ messages.channelUnavailableHints[channel] ?? ""
177
+ ]
178
+ },
179
+ channel
180
+ )) });
181
+ }
182
+ __name(UnavailableHints, "UnavailableHints");
183
+ function usePreferences(api) {
184
+ const [payload, setPayload] = useState2(null);
185
+ useEffect2(() => {
186
+ let cancelled = false;
187
+ void api.getPreferences().then((next) => {
188
+ if (!cancelled) setPayload(next);
189
+ }).catch(() => void 0);
190
+ return () => {
191
+ cancelled = true;
192
+ };
193
+ }, [api]);
194
+ const toggle = useCallback(
195
+ (category, channel, enabled) => {
196
+ setPayload((current) => {
197
+ const row = current?.preferences[category];
198
+ if (!current || !row) return current;
199
+ return {
200
+ ...current,
201
+ preferences: { ...current.preferences, [category]: { ...row, [channel]: enabled } }
202
+ };
203
+ });
204
+ const reconcile = /* @__PURE__ */ __name(() => {
205
+ void api.getPreferences().then(setPayload).catch(() => void 0);
206
+ }, "reconcile");
207
+ void api.savePreference(category, channel, enabled).then((result) => {
208
+ if (result.ok) setPayload(result.data);
209
+ else reconcile();
210
+ }).catch(reconcile);
211
+ },
212
+ [api]
213
+ );
214
+ return { payload, toggle };
215
+ }
216
+ __name(usePreferences, "usePreferences");
217
+ function PreferencesScreen({
218
+ footer,
219
+ api,
220
+ messages,
221
+ webPush
222
+ }) {
223
+ const { payload, toggle } = usePreferences(api);
224
+ if (!payload) {
225
+ return /* @__PURE__ */ jsx2(
226
+ LoadingState,
227
+ {
228
+ variant: "spinner",
229
+ message: messages.loadingMore,
230
+ size: "md",
231
+ dataTestId: "notification-prefs-loading"
232
+ }
233
+ );
234
+ }
235
+ const { preferences, availability, categories } = payload;
236
+ return /* @__PURE__ */ jsxs2(
237
+ Box2,
238
+ {
239
+ component: "section",
240
+ "data-testid": "notification-prefs-view",
241
+ sx: {
242
+ maxWidth: 640,
243
+ mx: "auto",
244
+ width: "100%",
245
+ px: 2,
246
+ py: 4,
247
+ display: "flex",
248
+ flexDirection: "column",
249
+ gap: 2
250
+ },
251
+ children: [
252
+ /* @__PURE__ */ jsxs2(Box2, { children: [
253
+ /* @__PURE__ */ jsx2(Text2, { variant: "heading", size: "lg", as: "h1", children: messages.preferencesTitle }),
254
+ /* @__PURE__ */ jsxs2(Text2, { variant: "caption", size: "sm", color: "secondary", as: "p", children: [
255
+ messages.preferencesLead,
256
+ " ",
257
+ footer
258
+ ] })
259
+ ] }),
260
+ /* @__PURE__ */ jsx2(
261
+ WebPushDeviceSetup,
262
+ {
263
+ available: availability.WEB_PUSH,
264
+ api,
265
+ messages,
266
+ config: webPush
267
+ }
268
+ ),
269
+ categories.map((category) => /* @__PURE__ */ jsx2(
270
+ CategoryCard,
271
+ {
272
+ category,
273
+ channels: preferences[category] ?? {
274
+ EMAIL: false,
275
+ SMS: false,
276
+ WHATSAPP: false,
277
+ WEB_PUSH: false
278
+ },
279
+ availability,
280
+ messages,
281
+ onToggle: (channel, enabled) => toggle(category, channel, enabled)
282
+ },
283
+ category
284
+ )),
285
+ /* @__PURE__ */ jsx2(UnavailableHints, { availability, messages })
286
+ ]
287
+ }
288
+ );
289
+ }
290
+ __name(PreferencesScreen, "PreferencesScreen");
291
+ export {
292
+ PreferencesScreen
293
+ };
294
+ //# sourceMappingURL=preferences-screen-V3ZBWP7A.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/react/preferences-screen.tsx","../src/react/web-push-setup.tsx"],"sourcesContent":["/**\n * The notification-preferences screen: the category × channel matrix over\n * `GET/PUT <mount>/notification-preferences`.\n *\n * Toggles auto-save (optimistic, per change); channels that cannot reach the\n * user right now (no phone on file / channel not declared) render disabled with\n * a hint. Web Push additionally carries the per-BROWSER enable step, since a\n * preference alone cannot reach a device that never subscribed.\n */\nimport { useCallback, useEffect, useState, type JSX } from 'react';\n\nimport { LoadingState } from '@12-apps/ui/data-display/LoadingState';\nimport { Switch } from '@12-apps/ui/form/Switch';\nimport { Box } from '@12-apps/ui/mui/Box';\nimport { Text } from '@12-apps/ui/typography/Text';\n\nimport type { NotificationMessages } from '../messages';\nimport { NOTIFICATION_CHANNELS, type NotificationChannel } from '../types';\n\nimport type { NotificationsApiClient, PreferencesPayload } from './api';\nimport { WebPushDeviceSetup, type WebPushSetupConfig } from './web-push-setup';\n\ntype Availability = Record<NotificationChannel, boolean>;\n\n/** One category's row of channel switches. */\nfunction CategoryCard({\n category,\n channels,\n availability,\n messages,\n onToggle,\n}: {\n category: string;\n channels: Record<NotificationChannel, boolean>;\n availability: Availability;\n messages: NotificationMessages;\n onToggle: (channel: NotificationChannel, enabled: boolean) => void;\n}): JSX.Element {\n const labels = messages.categoryLabels[category];\n return (\n <Box\n sx={{ p: 2, border: '1px solid', borderColor: 'divider', borderRadius: 2 }}\n data-testid={`prefs-${category}`}\n >\n <Text variant=\"body\" size=\"sm\" weight=\"semibold\" as=\"p\">\n {labels?.title ?? messages.categoryFallbackTitle(category)}\n </Text>\n {labels?.description ? (\n <Text variant=\"caption\" size=\"xs\" color=\"secondary\" as=\"p\">\n {labels.description}\n </Text>\n ) : null}\n <Box\n sx={{\n mt: 1.5,\n display: 'grid',\n gridTemplateColumns: { xs: '1fr 1fr', sm: 'repeat(4, 1fr)' },\n gap: 1,\n }}\n >\n {NOTIFICATION_CHANNELS.map((channel) => (\n <Switch\n key={channel}\n size=\"sm\"\n color=\"primary\"\n label={messages.channelLabels[channel] ?? channel}\n // An unavailable channel reads OFF regardless of the stored choice:\n // a toggle that says \"on\" for a channel that cannot reach you is a\n // promise the pipeline will not keep.\n checked={channels[channel] && availability[channel]}\n disabled={!availability[channel]}\n onChange={(_, checked) => onToggle(channel, checked)}\n // `dataTestId`, not `data-testid`: the UI Switch puts this one on\n // the INPUT (and derives `-container` / `-label` from it), which is\n // the element a click and a `disabled` assertion need.\n dataTestId={`prefs-${category}-${channel}`}\n />\n ))}\n </Box>\n </Box>\n );\n}\n\n/** Why disabled toggles are disabled, one line per unavailable channel. */\nfunction UnavailableHints({\n availability,\n messages,\n}: {\n availability: Availability;\n messages: NotificationMessages;\n}): JSX.Element | null {\n const unavailable = NOTIFICATION_CHANNELS.filter((channel) => !availability[channel]);\n if (unavailable.length === 0) return null;\n return (\n <Box sx={{ display: 'flex', flexDirection: 'column', gap: 0.5 }}>\n {unavailable.map((channel) => (\n <Text\n key={channel}\n variant=\"caption\"\n size=\"xs\"\n color=\"secondary\"\n as=\"p\"\n data-testid={`prefs-hint-${channel}`}\n >\n {messages.channelLabels[channel] ?? channel}:{' '}\n {messages.channelUnavailableHints[channel] ?? ''}\n </Text>\n ))}\n </Box>\n );\n}\n\nexport interface PreferencesScreenProps {\n /** Rendered under the lead paragraph — a \"back to account\" link, typically. */\n footer?: JSX.Element;\n}\n\n/** Optimistic per-toggle auto-save; a failed PUT takes the server's answer. */\nfunction usePreferences(api: NotificationsApiClient): {\n payload: PreferencesPayload | null;\n toggle: (category: string, channel: NotificationChannel, enabled: boolean) => void;\n} {\n const [payload, setPayload] = useState<PreferencesPayload | null>(null);\n\n useEffect(() => {\n let cancelled = false;\n void api\n .getPreferences()\n .then((next) => {\n if (!cancelled) setPayload(next);\n })\n .catch(() => undefined);\n return () => {\n cancelled = true;\n };\n }, [api]);\n\n const toggle = useCallback(\n (category: string, channel: NotificationChannel, enabled: boolean) => {\n setPayload((current) => {\n const row = current?.preferences[category];\n if (!current || !row) return current;\n return {\n ...current,\n preferences: { ...current.preferences, [category]: { ...row, [channel]: enabled } },\n };\n });\n const reconcile = (): void => {\n void api.getPreferences().then(setPayload).catch(() => undefined);\n };\n void api\n .savePreference(category, channel, enabled)\n .then((result) => {\n // The PUT answers with the whole matrix, so a success REPLACES the\n // optimistic guess with the server's own row — which is what catches a\n // save the server merged differently from the way the screen assumed.\n if (result.ok) setPayload(result.data);\n else reconcile();\n })\n // The packaged transport never rejects (it folds a failure into\n // `ok: false`), but a HOST transport may — and an unhandled rejection\n // would leave the toggle showing a choice the server never took.\n .catch(reconcile);\n },\n [api],\n );\n\n return { payload, toggle };\n}\n\nexport function PreferencesScreen({\n footer,\n api,\n messages,\n webPush,\n}: PreferencesScreenProps & {\n api: NotificationsApiClient;\n messages: NotificationMessages;\n webPush: WebPushSetupConfig;\n}): JSX.Element {\n const { payload, toggle } = usePreferences(api);\n\n if (!payload) {\n return (\n <LoadingState\n variant=\"spinner\"\n message={messages.loadingMore}\n size=\"md\"\n dataTestId=\"notification-prefs-loading\"\n />\n );\n }\n\n const { preferences, availability, categories } = payload;\n return (\n <Box\n component=\"section\"\n data-testid=\"notification-prefs-view\"\n sx={{\n maxWidth: 640,\n mx: 'auto',\n width: '100%',\n px: 2,\n py: 4,\n display: 'flex',\n flexDirection: 'column',\n gap: 2,\n }}\n >\n <Box>\n <Text variant=\"heading\" size=\"lg\" as=\"h1\">\n {messages.preferencesTitle}\n </Text>\n <Text variant=\"caption\" size=\"sm\" color=\"secondary\" as=\"p\">\n {messages.preferencesLead} {footer}\n </Text>\n </Box>\n <WebPushDeviceSetup\n available={availability.WEB_PUSH}\n api={api}\n messages={messages}\n config={webPush}\n />\n {categories.map((category) => (\n <CategoryCard\n key={category}\n category={category}\n channels={preferences[category] ?? {\n EMAIL: false,\n SMS: false,\n WHATSAPP: false,\n WEB_PUSH: false,\n }}\n availability={availability}\n messages={messages}\n onToggle={(channel, enabled) => toggle(category, channel, enabled)}\n />\n ))}\n <UnavailableHints availability={availability} messages={messages} />\n </Box>\n );\n}\n","/**\n * The per-BROWSER Web Push enable step, which sits above the preference matrix\n * because a preference alone cannot reach a device that never subscribed.\n */\nimport { useEffect, useState, type JSX } from 'react';\n\nimport { Button } from '@12-apps/ui/form/Button';\nimport { Box } from '@12-apps/ui/mui/Box';\nimport { Text } from '@12-apps/ui/typography/Text';\n\nimport type { NotificationMessages } from '../messages';\n\nimport type { NotificationsApiClient } from './api';\nimport { enableWebPush, getExistingPushSubscription } from './web-push-client';\n\n/** The panel's copy for a host whose platform blocks browser-level push. */\nexport interface WebPushPlatformHint {\n title: string;\n body: string;\n}\n\nexport interface WebPushSetupConfig {\n /**\n * The host's service-worker path. Path-routed SPAs each control their own\n * scope, and the file itself is the host's.\n */\n swPath?: string;\n /**\n * Whether THIS platform must be installed to the home screen before a\n * subscription can exist at all.\n *\n * iOS is the case: Safari has no browser-level Web Push, so \"Ativar\" there\n * asks no permission, creates no subscription, and fails with nothing a user\n * could act on. The check is a config seam rather than a dependency because\n * \"is this an installable iOS browser\" is a question a host's PWA layer\n * already answers (the origin passes\n * `() => isIosInstallable() && !isStandalone()` from `@12-apps/pwa`).\n */\n needsInstallFirst?: () => boolean;\n /** What to say instead of the button when the check above is true. */\n installHint?: WebPushPlatformHint;\n}\n\nconst cardSx = {\n p: 2,\n border: '1px solid',\n borderColor: 'divider',\n borderRadius: 2,\n} as const;\n\nfunction InstallFirstHint({ hint }: { hint: WebPushPlatformHint }): JSX.Element {\n return (\n <Box sx={cardSx} data-testid=\"web-push-install-hint\">\n <Text variant=\"body\" size=\"sm\" weight=\"semibold\" as=\"p\">\n {hint.title}\n </Text>\n <Text variant=\"caption\" size=\"xs\" color=\"secondary\" as=\"p\">\n {hint.body}\n </Text>\n </Box>\n );\n}\n\ntype SetupState = 'idle' | 'checking' | 'on' | 'busy' | 'failed' | 'denied';\n\n/**\n * \"Is this browser receiving alerts?\" — asked of the browser AND of the server.\n *\n * The browser alone is not enough, and answering from it alone is how a real\n * person is silenced while being told they are fine. Two users share a counter\n * PC: Ana enables push, then Otávio signs in on the same browser and enables it\n * too. `PushManager` hands out the SAME endpoint for the same browser profile,\n * so the POST re-owns the row and Ana's is gone. Ana signs back in, her browser\n * still holds the subscription object, and a browser-only check renders \"Este\n * navegador está recebendo alertas.\" with no button to fix it — forever. The\n * 404/410 prune reaches the same state from the other direction.\n *\n * So the server is asked whether it still has THIS endpoint under THIS user. A\n * `false` (or an unreachable server) shows *Ativar* again, and one click\n * re-registers the row — which is the same request the happy path makes, so the\n * recovery costs nothing to maintain.\n */\nasync function resolveState(api: NotificationsApiClient): Promise<SetupState> {\n const subscription = await getExistingPushSubscription();\n if (!subscription) return 'idle';\n const registration = await api\n .getPushRegistration({ endpoint: subscription.endpoint })\n .catch(() => null);\n return registration?.registered ? 'on' : 'idle';\n}\n\nfunction statusText(state: SetupState, messages: NotificationMessages): string {\n if (state === 'on') return messages.devicePushOn;\n if (state === 'denied') return messages.devicePushDenied;\n if (state === 'failed') return messages.devicePushFailed;\n return messages.devicePushIdle;\n}\n\nexport function WebPushDeviceSetup({\n available,\n api,\n messages,\n config,\n}: {\n available: boolean;\n api: NotificationsApiClient;\n messages: NotificationMessages;\n config: WebPushSetupConfig;\n}): JSX.Element | null {\n const [state, setState] = useState<SetupState>('checking');\n // Read once, at mount: neither can change without a new document.\n const [needsInstall] = useState(() => config.needsInstallFirst?.() ?? false);\n\n useEffect(() => {\n let cancelled = false;\n void resolveState(api).then((next) => {\n if (!cancelled) setState(next);\n });\n return () => {\n cancelled = true;\n };\n }, [api]);\n\n if (!available) return null;\n if (needsInstall && config.installHint) return <InstallFirstHint hint={config.installHint} />;\n\n const enable = async (): Promise<void> => {\n setState('busy');\n const result = await enableWebPush(api, config.swPath);\n if (result.ok) setState('on');\n else setState(result.reason === 'permission-denied' ? 'denied' : 'failed');\n };\n\n return (\n <Box\n sx={{\n ...cardSx,\n display: 'flex',\n alignItems: 'center',\n justifyContent: 'space-between',\n gap: 2,\n }}\n data-testid=\"web-push-device-setup\"\n >\n <Box sx={{ minWidth: 0 }}>\n <Text variant=\"body\" size=\"sm\" weight=\"semibold\" as=\"p\">\n {messages.devicePushTitle}\n </Text>\n <Text variant=\"caption\" size=\"xs\" color=\"secondary\" as=\"p\">\n {statusText(state, messages)}\n </Text>\n </Box>\n {state !== 'on' ? (\n <Button\n variant=\"outline\"\n color=\"primary\"\n size=\"sm\"\n disabled={state === 'busy' || state === 'checking'}\n onClick={() => void enable()}\n dataTestId=\"web-push-enable\"\n >\n {state === 'busy' ? messages.devicePushEnabling : messages.devicePushEnable}\n </Button>\n ) : null}\n </Box>\n );\n}\n"],"mappings":"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","names":["useEffect","useState","Box","Text","jsx","jsxs","Box","Text","useState","useEffect"]}
@@ -1,8 +1,8 @@
1
- import { N as NotificationsApiClient } from '../create-web-notifications-BpNR8qH3.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-BpNR8qH3.js';
1
+ import { N as NotificationsApiClient } from '../create-web-notifications-DrgqtU4Q.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-DrgqtU4Q.js';
3
3
  import { JSX } from 'react';
4
- import { N as NotificationMessages } from '../wire-6dzyfDE7.js';
5
- import '../types-CXLAG3UU.js';
4
+ import { N as NotificationMessages } from '../wire-CmRrB3_s.js';
5
+ import '../types-CYJN1xCM.js';
6
6
 
7
7
  /** Inline SVG bell (no icon-library dependency in this package). */
8
8
 
@@ -1,23 +1,29 @@
1
+ import {
2
+ relativeTime
3
+ } from "../chunk-O5BVUXPO.js";
4
+ import {
5
+ disableWebPush,
6
+ enableWebPush,
7
+ getExistingPushSubscription,
8
+ pushSupported
9
+ } from "../chunk-CQZMTFPY.js";
10
+ import {
11
+ NotificationsHttpError,
12
+ createNotificationsApiClient,
13
+ createWebNotifications,
14
+ httpNotificationsTransport
15
+ } from "../chunk-M5XTROYM.js";
1
16
  import {
2
17
  BADGE_POLL_MS,
3
18
  BADGE_RECONCILE_MS,
4
19
  BellIcon,
5
- NotificationsHttpError,
6
20
  PAGE_SIZE,
7
21
  createInboxStore,
8
- createNotificationsApiClient,
9
- createWebNotifications,
10
- disableWebPush,
11
- enableWebPush,
12
- getExistingPushSubscription,
13
- httpNotificationsTransport,
14
- pushSupported,
15
- relativeTime,
16
22
  useInboxList,
17
23
  useInboxState,
18
24
  useUnreadCount
19
- } from "../chunk-6HLHQDKS.js";
20
- import "../chunk-Y34FX24X.js";
25
+ } from "../chunk-BW723CX2.js";
26
+ import "../chunk-M2TVBVH2.js";
21
27
  import "../chunk-7QVYU63E.js";
22
28
  export {
23
29
  BADGE_POLL_MS,
@@ -1,11 +1,11 @@
1
- import { A as ApiNotifications } from '../create-api-notifications-B3u6Kx3x.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-B3u6Kx3x.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-Cs14Wp9u.js';
1
+ import { A as ApiNotifications } from '../create-api-notifications-WaexrZnn.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-WaexrZnn.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-iER5StBm.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-BDZ7aGHV.js';
6
- import '../generators-B9xt3sRh.js';
7
- import '../types-CXLAG3UU.js';
8
- import '../wire-6dzyfDE7.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-CcsSA0YR.js';
6
+ import '../generators-CH5m8Ucj.js';
7
+ import '../types-CYJN1xCM.js';
8
+ import '../wire-CmRrB3_s.js';
9
9
  import '@12-apps/wiring';
10
10
 
11
11
  /**
@@ -4,7 +4,7 @@ import {
4
4
  NOTIFICATIONS_JOBS,
5
5
  NOTIFICATIONS_SWEEP_QUEUE,
6
6
  wireNotifyPort
7
- } from "../chunk-QRAXX3GR.js";
7
+ } from "../chunk-KU4FVOFA.js";
8
8
  import {
9
9
  EMAIL_DRIVERS,
10
10
  NotificationProviderError,
@@ -25,9 +25,10 @@ import {
25
25
  smsTransport,
26
26
  webPushTransport,
27
27
  whatsAppTransport
28
- } from "../chunk-TIGTBSAQ.js";
29
- import "../chunk-HQU4R4SG.js";
30
- import "../chunk-Y34FX24X.js";
28
+ } from "../chunk-7234ROOP.js";
29
+ import "../chunk-GGAZOLNN.js";
30
+ import "../chunk-BGI4BQ66.js";
31
+ import "../chunk-M2TVBVH2.js";
31
32
  import "../chunk-7QVYU63E.js";
32
33
  export {
33
34
  EMAIL_DRIVERS,
@@ -84,12 +84,44 @@ interface NotificationEvent<TPayload = unknown> {
84
84
  * generator never touches existing generators, the router, or any transport
85
85
  * (open/closed).
86
86
  */
87
+ /**
88
+ * Who the content is being rendered FOR — the reader, at the moment the
89
+ * generator is asked.
90
+ *
91
+ * A notification is stored as rendered TEXT: title and body are columns, so
92
+ * the language is chosen once, when the row is written, and never again. That
93
+ * makes this the only honest place to ask. A generator is registered at BOOT —
94
+ * a host that resolved its words there would pin every future reader to
95
+ * whichever language the process happened to start in, invisibly, because a
96
+ * single-locale host cannot tell the difference.
97
+ *
98
+ * The tag is the RECIPIENT's, never the request's. The person who triggers a
99
+ * notification is routinely not the person who reads it: an invite is sent
100
+ * because an administrator acted and is read by the invitee. Reading
101
+ * `Accept-Language` here would be a bug that only ever surfaces as somebody
102
+ * being told things in a language they do not speak.
103
+ *
104
+ * Absent means "nobody said" — a host with one audience, or one that stores no
105
+ * per-user language, populates nothing and every generator answers with its
106
+ * own default exactly as it did before this existed.
107
+ */
108
+ interface NotificationGenerateContext {
109
+ readonly locale?: string | null;
110
+ }
87
111
  interface NotificationGenerator<TPayload = unknown> {
88
112
  /** The event key, dot-namespaced ("order.paid"). One generator per type. */
89
113
  type: string;
90
114
  /** The preference category the router gates this type's fan-out on. */
91
115
  category: NotificationCategory;
92
- generate: (payload: TPayload) => NotificationContent;
116
+ /**
117
+ * Render this event's content for ONE recipient.
118
+ *
119
+ * `context` is OPTIONAL, and that is what keeps every generator written
120
+ * before it working: a one-parameter `generate` is assignable to this
121
+ * signature unchanged. A host that passes nothing is stating a fact — it has
122
+ * no language for this reader — rather than asserting a default.
123
+ */
124
+ generate: (payload: TPayload, context?: NotificationGenerateContext) => NotificationContent;
93
125
  }
94
126
  /**
95
127
  * The recipient as a transport sees them: resolved destinations only. Built by
@@ -100,6 +132,11 @@ interface NotificationGenerator<TPayload = unknown> {
100
132
  interface TransportRecipient {
101
133
  userId: string;
102
134
  email: string | null;
135
+ /**
136
+ * The recipient's own language, when the host's contact directory states
137
+ * one. Absent means "nobody said" — see {@link NotificationGenerateContext}.
138
+ */
139
+ locale?: string | null;
103
140
  /** Phone as the host stores it (transports normalize per provider rules). */
104
141
  phone: string | null;
105
142
  /** How many active browser push subscriptions the user holds. */
@@ -152,4 +189,4 @@ interface NotificationLogger {
152
189
  error(message: string, ...meta: unknown[]): void;
153
190
  }
154
191
 
155
- export { type DeliveryStatus as D, NOTIFICATION_CHANNELS as N, type TransportRecipient as T, type NotificationCategory as a, type NotificationChannel as b, type NotificationContent as c, type NotificationEvent as d, type NotificationGenerator as e, type NotificationLogger as f, type NotificationRecipient as g, type NotificationTaxonomy as h, type NotificationTransport as i, taxonomyOf as t };
192
+ export { type DeliveryStatus as D, NOTIFICATION_CHANNELS as N, type TransportRecipient as T, type NotificationCategory as a, type NotificationChannel as b, type NotificationContent as c, type NotificationEvent as d, type NotificationGenerateContext as e, type NotificationGenerator as f, type NotificationLogger as g, type NotificationRecipient as h, type NotificationTaxonomy as i, type NotificationTransport as j, taxonomyOf as t };
@@ -1,5 +1,5 @@
1
- import { c as WebPushSender } from '../web-push-Cs14Wp9u.js';
2
- import '../types-CXLAG3UU.js';
1
+ import { c as WebPushSender } from '../web-push-iER5StBm.js';
2
+ import '../types-CYJN1xCM.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 { f as NotificationLogger, c as NotificationContent, i as NotificationTransport } from './types-CXLAG3UU.js';
1
+ import { g as NotificationLogger, c as NotificationContent, j as NotificationTransport } from './types-CYJN1xCM.js';
2
2
 
3
3
  /**
4
4
  * The DRIVER port — the reason a host adds a vendor with a config entry and no
@@ -1,4 +1,4 @@
1
- import { a as NotificationCategory, b as NotificationChannel } from './types-CXLAG3UU.js';
1
+ import { a as NotificationCategory, b as NotificationChannel } from './types-CYJN1xCM.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.7.0",
3
+ "version": "4.8.0",
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.",
@@ -51,7 +51,7 @@
51
51
  "prisma:sync:check": "node scripts/sync-notifications-schema.mjs --check"
52
52
  },
53
53
  "dependencies": {
54
- "@12-apps/ui": "^6.11.0"
54
+ "@12-apps/ui": "^6.11.1"
55
55
  },
56
56
  "peerDependencies": {
57
57
  "@12-apps/wiring": ">=1.3.0",
@@ -80,7 +80,7 @@
80
80
  "devDependencies": {
81
81
  "@12-apps/eslint-config": "^1.22.0",
82
82
  "@12-apps/typescript-config": "^1.21.0",
83
- "@12-apps/wiring": "^1.14.0",
83
+ "@12-apps/wiring": "^1.16.0",
84
84
  "@testing-library/react": "^16.1.0",
85
85
  "@types/react": "19.2.2",
86
86
  "@types/react-dom": "19.2.2",
package/src/index.ts CHANGED
@@ -28,6 +28,7 @@ export {
28
28
  type NotificationChannel,
29
29
  type NotificationContent,
30
30
  type NotificationEvent,
31
+ type NotificationGenerateContext,
31
32
  type NotificationGenerator,
32
33
  type NotificationLogger,
33
34
  type NotificationRecipient,