@12-apps/notifications 4.6.0 → 4.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. package/dist/chunk-BW723CX2.js +214 -0
  2. package/dist/chunk-BW723CX2.js.map +1 -0
  3. package/dist/chunk-CQZMTFPY.js +76 -0
  4. package/dist/chunk-CQZMTFPY.js.map +1 -0
  5. package/dist/{chunk-WU6QJLSZ.js → chunk-CUZW62JS.js} +2 -2
  6. package/dist/{chunk-AHNRSA6U.js → chunk-HHMRCMQU.js} +2 -2
  7. package/dist/chunk-M2TVBVH2.js +15 -0
  8. package/dist/chunk-M2TVBVH2.js.map +1 -0
  9. package/dist/chunk-MMLV4EZT.js +263 -0
  10. package/dist/chunk-MMLV4EZT.js.map +1 -0
  11. package/dist/chunk-O5BVUXPO.js +22 -0
  12. package/dist/chunk-O5BVUXPO.js.map +1 -0
  13. package/dist/{chunk-ORXJH3VM.js → chunk-WHBMPHQE.js} +28 -18
  14. package/dist/chunk-WHBMPHQE.js.map +1 -0
  15. package/dist/{chunk-4TTYQVPK.js → chunk-XE7HZVMH.js} +2 -9
  16. package/dist/chunk-XE7HZVMH.js.map +1 -0
  17. package/dist/{create-api-notifications-CgBdjfyF.d.ts → create-api-notifications-B3u6Kx3x.d.ts} +28 -5
  18. package/dist/{create-web-notifications-Du3hTs7P.d.ts → create-web-notifications-BHCzaU2y.d.ts} +14 -3
  19. package/dist/hono/index.d.ts +2 -2
  20. package/dist/hono/index.js +4 -3
  21. package/dist/hono/index.js.map +1 -1
  22. package/dist/index.d.ts +1 -1
  23. package/dist/index.js +5 -3
  24. package/dist/{jobs-DhDjrAX5.d.ts → jobs-BDZ7aGHV.d.ts} +1 -1
  25. package/dist/manifest/server.d.ts +3 -3
  26. package/dist/manifest/server.js +5 -4
  27. package/dist/manifest/server.js.map +1 -1
  28. package/dist/manifest/web.d.ts +2 -2
  29. package/dist/manifest/web.js +3 -2
  30. package/dist/manifest/web.js.map +1 -1
  31. package/dist/panel-UFXNO4AF.js +243 -0
  32. package/dist/panel-UFXNO4AF.js.map +1 -0
  33. package/dist/preferences-screen-IOW6Y2H2.js +294 -0
  34. package/dist/preferences-screen-IOW6Y2H2.js.map +1 -0
  35. package/dist/react/index.d.ts +3 -3
  36. package/dist/react/index.js +17 -11
  37. package/dist/server/index.d.ts +4 -4
  38. package/dist/server/index.js +5 -4
  39. package/dist/{wire-5IRin4zH.d.ts → wire-6dzyfDE7.d.ts} +31 -5
  40. package/package.json +2 -2
  41. package/src/messages.ts +37 -4
  42. package/src/react/create-web-notifications.tsx +19 -10
  43. package/src/react/page-lazy.tsx +73 -0
  44. package/src/react/panel-lazy.tsx +74 -0
  45. package/src/server/context.ts +10 -0
  46. package/src/server/create-api-notifications.ts +30 -6
  47. package/src/server/routes.ts +32 -16
  48. package/dist/chunk-4TTYQVPK.js.map +0 -1
  49. package/dist/chunk-ORXJH3VM.js.map +0 -1
  50. package/dist/chunk-YE24MDS6.js +0 -1022
  51. package/dist/chunk-YE24MDS6.js.map +0 -1
  52. /package/dist/{chunk-WU6QJLSZ.js.map → chunk-CUZW62JS.js.map} +0 -0
  53. /package/dist/{chunk-AHNRSA6U.js.map → chunk-HHMRCMQU.js.map} +0 -0
@@ -0,0 +1,294 @@
1
+ import {
2
+ enableWebPush,
3
+ getExistingPushSubscription
4
+ } from "./chunk-CQZMTFPY.js";
5
+ import {
6
+ NOTIFICATION_CHANNELS
7
+ } from "./chunk-XE7HZVMH.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-IOW6Y2H2.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,7 +1,7 @@
1
- import { N as NotificationsApiClient } from '../create-web-notifications-Du3hTs7P.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-Du3hTs7P.js';
1
+ import { N as NotificationsApiClient } from '../create-web-notifications-BHCzaU2y.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-BHCzaU2y.js';
3
3
  import { JSX } from 'react';
4
- import { N as NotificationMessages } from '../wire-5IRin4zH.js';
4
+ import { N as NotificationMessages } from '../wire-6dzyfDE7.js';
5
5
  import '../types-CXLAG3UU.js';
6
6
 
7
7
  /** Inline SVG bell (no icon-library dependency in this package). */
@@ -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-MMLV4EZT.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-YE24MDS6.js";
20
- import "../chunk-4TTYQVPK.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-CgBdjfyF.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-CgBdjfyF.js';
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
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';
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-DhDjrAX5.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-BDZ7aGHV.js';
6
6
  import '../generators-B9xt3sRh.js';
7
7
  import '../types-CXLAG3UU.js';
8
- import '../wire-5IRin4zH.js';
8
+ import '../wire-6dzyfDE7.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-WU6QJLSZ.js";
7
+ } from "../chunk-CUZW62JS.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-ORXJH3VM.js";
29
- import "../chunk-AHNRSA6U.js";
30
- import "../chunk-4TTYQVPK.js";
28
+ } from "../chunk-WHBMPHQE.js";
29
+ import "../chunk-HHMRCMQU.js";
30
+ import "../chunk-XE7HZVMH.js";
31
+ import "../chunk-M2TVBVH2.js";
31
32
  import "../chunk-7QVYU63E.js";
32
33
  export {
33
34
  EMAIL_DRIVERS,
@@ -83,7 +83,20 @@ interface NotificationMessages extends NotificationWireMessages {
83
83
  devicePushEnabling: string;
84
84
  }
85
85
  /**
86
- * The messages in force.
86
+ * What a copy field takes once its words can follow a reader.
87
+ *
88
+ * Declared here rather than imported from `@12-apps/i18n`: this package must
89
+ * stay liftable into a repo that has never heard of it, so the two agree
90
+ * STRUCTURALLY and nothing forces the dependency. The context is deliberately
91
+ * loose — a raw tag off the wire, unnarrowed — because matching it is the host
92
+ * resolver's job, not this package's.
93
+ */
94
+ type NotificationsCopyResolver<T> = (context: {
95
+ readonly locale?: string | null;
96
+ }) => T;
97
+ type NotificationsCopySource<T> = T | NotificationsCopyResolver<T>;
98
+ /**
99
+ * The messages in force, for ONE reader.
87
100
  *
88
101
  * A pass-through rather than a merge: there is nothing left to merge WITH, and
89
102
  * that is the point of the change. The old version spread the host's table over
@@ -94,11 +107,24 @@ interface NotificationMessages extends NotificationWireMessages {
94
107
  * the same screen.
95
108
  *
96
109
  * Kept as a function because all three mounts read it off a config object, and
97
- * because a later rule (a blank-string refusal, say) belongs in one place.
110
+ * because a later rule (a blank-string refusal, say) belongs in one place —
111
+ * which is exactly what made it the right place to put the RESOLUTION when the
112
+ * field learned to take a resolver.
113
+ *
114
+ * **Call it where the sentence is used.** `createApiNotifications` runs once
115
+ * per process, and at least one host memoises its call behind an `if
116
+ * (assembled) return assembled;`, so a value read there answers every later
117
+ * request in the language the process started with — and a single-locale host
118
+ * cannot tell the difference. The route handlers call it per request; the
119
+ * parsers below them keep taking a plain pack, so one request resolves exactly
120
+ * once and no helper can disagree with another about the language.
121
+ *
122
+ * The generic survives the widening: a host whose pack carries extra keys of
123
+ * its own still gets them back, resolver or not.
98
124
  */
99
125
  declare function messagesOf<T extends NotificationWireMessages>(config: {
100
- messages: T;
101
- }): T;
126
+ messages: NotificationsCopySource<T>;
127
+ }, locale?: string): T;
102
128
 
103
129
  /**
104
130
  * The preference POLICY, with no storage in it (12-15): which channels a
@@ -194,4 +220,4 @@ interface NotificationRow {
194
220
  /** Row → wire. Dates become ISO strings; a null `data` becomes `{}`. */
195
221
  declare function inboxWire(row: NotificationRow): InboxNotification;
196
222
 
197
- export { type ChannelMatrix as C, DEFAULT_CHANNEL_ROW as D, type InboxNotification as I, type ListNotificationsResult as L, type NotificationMessages as N, type ChannelRow as a, type NotificationRow as b, type NotificationWireMessages as c, defaultChannelMatrix as d, enabledChannelsOf as e, mergeStoredRow as f, messagesOf as g, inboxWire as i, mergeChoices as m };
223
+ export { type ChannelMatrix as C, DEFAULT_CHANNEL_ROW as D, type InboxNotification as I, type ListNotificationsResult as L, type NotificationMessages as N, type ChannelRow as a, type NotificationRow as b, type NotificationWireMessages as c, defaultChannelMatrix as d, enabledChannelsOf as e, mergeStoredRow as f, messagesOf as g, type NotificationsCopySource as h, inboxWire as i, mergeChoices as m };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@12-apps/notifications",
3
- "version": "4.6.0",
3
+ "version": "4.7.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.",
@@ -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.10.0"
54
+ "@12-apps/ui": "^6.11.1"
55
55
  },
56
56
  "peerDependencies": {
57
57
  "@12-apps/wiring": ">=1.3.0",
package/src/messages.ts CHANGED
@@ -85,7 +85,21 @@ export interface NotificationMessages extends NotificationWireMessages {
85
85
  }
86
86
 
87
87
  /**
88
- * The messages in force.
88
+ * What a copy field takes once its words can follow a reader.
89
+ *
90
+ * Declared here rather than imported from `@12-apps/i18n`: this package must
91
+ * stay liftable into a repo that has never heard of it, so the two agree
92
+ * STRUCTURALLY and nothing forces the dependency. The context is deliberately
93
+ * loose — a raw tag off the wire, unnarrowed — because matching it is the host
94
+ * resolver's job, not this package's.
95
+ */
96
+ export type NotificationsCopyResolver<T> = (context: {
97
+ readonly locale?: string | null;
98
+ }) => T;
99
+ export type NotificationsCopySource<T> = T | NotificationsCopyResolver<T>;
100
+
101
+ /**
102
+ * The messages in force, for ONE reader.
89
103
  *
90
104
  * A pass-through rather than a merge: there is nothing left to merge WITH, and
91
105
  * that is the point of the change. The old version spread the host's table over
@@ -96,8 +110,27 @@ export interface NotificationMessages extends NotificationWireMessages {
96
110
  * the same screen.
97
111
  *
98
112
  * Kept as a function because all three mounts read it off a config object, and
99
- * because a later rule (a blank-string refusal, say) belongs in one place.
113
+ * because a later rule (a blank-string refusal, say) belongs in one place —
114
+ * which is exactly what made it the right place to put the RESOLUTION when the
115
+ * field learned to take a resolver.
116
+ *
117
+ * **Call it where the sentence is used.** `createApiNotifications` runs once
118
+ * per process, and at least one host memoises its call behind an `if
119
+ * (assembled) return assembled;`, so a value read there answers every later
120
+ * request in the language the process started with — and a single-locale host
121
+ * cannot tell the difference. The route handlers call it per request; the
122
+ * parsers below them keep taking a plain pack, so one request resolves exactly
123
+ * once and no helper can disagree with another about the language.
124
+ *
125
+ * The generic survives the widening: a host whose pack carries extra keys of
126
+ * its own still gets them back, resolver or not.
100
127
  */
101
- export function messagesOf<T extends NotificationWireMessages>(config: { messages: T }): T {
102
- return config.messages;
128
+ export function messagesOf<T extends NotificationWireMessages>(
129
+ config: { messages: NotificationsCopySource<T> },
130
+ locale?: string,
131
+ ): T {
132
+ const source = config.messages;
133
+ return typeof source === 'function'
134
+ ? (source as NotificationsCopyResolver<T>)({ locale })
135
+ : source;
103
136
  }
@@ -10,8 +10,10 @@ import {
10
10
  type NotificationsSubscribe,
11
11
  } from './hooks';
12
12
  import { createInboxStore, type InboxStore } from './inbox-state';
13
- import { NotificationsPanel, type NotificationsPanelProps } from './panel';
14
- import { PreferencesScreen, type PreferencesScreenProps } from './preferences-screen';
13
+ import { lazyNotificationsPanel } from './panel-lazy';
14
+ import type { NotificationsPanelProps } from './panel';
15
+ import { lazyPreferencesPage } from './page-lazy';
16
+ import type { PreferencesScreenProps } from './preferences-screen';
15
17
  import { httpNotificationsTransport, type NotificationsTransport } from './transport';
16
18
  import type { WebPushSetupConfig } from './web-push-setup';
17
19
 
@@ -56,11 +58,22 @@ export interface NotificationsWebConfig {
56
58
  }
57
59
 
58
60
  export interface WebNotifications {
59
- /** The routed surface: the preferences screen. */
61
+ /**
62
+ * The routed surface: the preferences screen.
63
+ *
64
+ * Loaded on demand — see `page-lazy.tsx`. A host that mounts only the bell and
65
+ * the panel never downloads it, and a host that routes to it fetches it while
66
+ * entering that route.
67
+ */
60
68
  page: ComponentType<PreferencesScreenProps>;
61
69
  /** The bell, already bound to the shared store. */
62
70
  BellButton: ComponentType<BellButtonProps>;
63
- /** The inbox slide-over, sharing that store. */
71
+ /**
72
+ * The inbox slide-over, sharing that store.
73
+ *
74
+ * Loaded the first time it is opened — see `panel-lazy.tsx`. Until then a
75
+ * host's chrome carries the bell and nothing else.
76
+ */
64
77
  Panel: ComponentType<NotificationsPanelProps>;
65
78
  /**
66
79
  * Bell + panel as ONE element, for a host that just wants the feature in its
@@ -97,9 +110,7 @@ export function createWebNotifications(config: NotificationsWebConfig): WebNotif
97
110
  const Bell: ComponentType<BellButtonProps> = (props) => (
98
111
  <BellButton {...props} store={store} messages={messages} {...subscribeOption} />
99
112
  );
100
- const Panel: ComponentType<NotificationsPanelProps> = (props) => (
101
- <NotificationsPanel {...props} store={store} messages={messages} />
102
- );
113
+ const Panel = lazyNotificationsPanel({ store, messages });
103
114
 
104
115
  function useBoundUnreadCount(options: { enabled?: boolean } = {}): number {
105
116
  return useUnreadCount(store, { ...options, ...subscribeOption });
@@ -126,9 +137,7 @@ export function createWebNotifications(config: NotificationsWebConfig): WebNotif
126
137
  }
127
138
 
128
139
  return {
129
- page: (props) => (
130
- <PreferencesScreen {...props} api={api} messages={messages} webPush={webPush} />
131
- ),
140
+ page: lazyPreferencesPage({ api, messages, webPush }),
132
141
  BellButton: Bell,
133
142
  Panel,
134
143
  BellWithPanel,