@12-apps/notifications 4.7.1 → 4.9.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.
- package/ADOPTING.md +145 -0
- package/README.md +65 -0
- package/dist/catalog-BOVZ2b-b.d.ts +132 -0
- package/dist/{chunk-XE7HZVMH.js → chunk-BGI4BQ66.js} +1 -1
- package/dist/chunk-BGI4BQ66.js.map +1 -0
- package/dist/chunk-EKUSNUBT.js +222 -0
- package/dist/chunk-EKUSNUBT.js.map +1 -0
- package/dist/chunk-FTFLU6KH.js +114 -0
- package/dist/chunk-FTFLU6KH.js.map +1 -0
- package/dist/{chunk-HHMRCMQU.js → chunk-GGAZOLNN.js} +2 -2
- package/dist/{chunk-CUZW62JS.js → chunk-KU4FVOFA.js} +2 -2
- package/dist/{chunk-WHBMPHQE.js → chunk-KZG7QGFA.js} +28 -6
- package/dist/chunk-KZG7QGFA.js.map +1 -0
- package/dist/{chunk-MMLV4EZT.js → chunk-M5XTROYM.js} +2 -2
- package/dist/chunk-ZFIYBNZ7.js +502 -0
- package/dist/chunk-ZFIYBNZ7.js.map +1 -0
- package/dist/{create-api-notifications-B3u6Kx3x.d.ts → create-api-notifications-B-VMezVB.d.ts} +43 -4
- package/dist/{create-web-notifications-BHCzaU2y.d.ts → create-web-notifications-B-xQ6R0B.d.ts} +2 -2
- package/dist/email/index.d.ts +26 -0
- package/dist/email/index.js +20 -0
- package/dist/email/index.js.map +1 -0
- package/dist/email/locales.d.ts +43 -0
- package/dist/email/locales.js +29 -0
- package/dist/email/locales.js.map +1 -0
- package/dist/email/previews/hono.d.ts +46 -0
- package/dist/email/previews/hono.js +33 -0
- package/dist/email/previews/hono.js.map +1 -0
- package/dist/email/previews/index.d.ts +67 -0
- package/dist/email/previews/index.js +12 -0
- package/dist/email/previews/index.js.map +1 -0
- package/dist/email/previews/react/index.d.ts +43 -0
- package/dist/email/previews/react/index.js +58 -0
- package/dist/email/previews/react/index.js.map +1 -0
- package/dist/{generators-B9xt3sRh.d.ts → generators-BsuXe8Mp.d.ts} +1 -1
- package/dist/hono/index.d.ts +6 -5
- package/dist/hono/index.js +4 -3
- package/dist/hono/index.js.map +1 -1
- package/dist/index.d.ts +3 -3
- package/dist/index.js +5 -5
- package/dist/{jobs-BDZ7aGHV.d.ts → jobs-D64B_bg7.d.ts} +1 -1
- package/dist/manifest/index.d.ts +39 -1
- package/dist/manifest/index.js +9 -0
- package/dist/manifest/index.js.map +1 -1
- package/dist/manifest/server.d.ts +32 -7
- package/dist/manifest/server.js +15 -4
- package/dist/manifest/server.js.map +1 -1
- package/dist/manifest/web.d.ts +18 -4
- package/dist/manifest/web.js +9 -1
- package/dist/manifest/web.js.map +1 -1
- package/dist/{preferences-screen-IOW6Y2H2.js → preferences-screen-V3ZBWP7A.js} +2 -2
- package/dist/preview-screen-DYJRAnAY.d.ts +83 -0
- package/dist/react/index.d.ts +4 -4
- package/dist/react/index.js +10 -10
- package/dist/server/index.d.ts +8 -7
- package/dist/server/index.js +5 -4
- package/dist/template-Dlt5S54z.d.ts +196 -0
- package/dist/{types-CXLAG3UU.d.ts → types-BFmAY0P5.d.ts} +39 -2
- package/dist/web-push/index.d.ts +2 -2
- package/dist/{web-push-Cs14Wp9u.d.ts → web-push-D-ysQd5h.d.ts} +1 -1
- package/dist/{wire-6dzyfDE7.d.ts → wire-_coV3tJk.d.ts} +1 -1
- package/package.json +23 -3
- package/src/email/chrome.en-US.ts +8 -0
- package/src/email/chrome.pt-BR.ts +21 -0
- package/src/email/index.ts +43 -0
- package/src/email/locales.ts +29 -0
- package/src/email/previews/catalog.ts +208 -0
- package/src/email/previews/hono.ts +69 -0
- package/src/email/previews/index.ts +26 -0
- package/src/email/previews/react/copy.en-US.ts +24 -0
- package/src/email/previews/react/copy.pt-BR.ts +24 -0
- package/src/email/previews/react/copy.ts +42 -0
- package/src/email/previews/react/index.tsx +14 -0
- package/src/email/previews/react/loadable.tsx +83 -0
- package/src/email/previews/react/message-list.tsx +133 -0
- package/src/email/previews/react/message-view.tsx +167 -0
- package/src/email/previews/react/preview-screen.tsx +342 -0
- package/src/email/previews/react/transport.ts +60 -0
- package/src/email/previews/routes.ts +111 -0
- package/src/email/template.ts +365 -0
- package/src/email/text.ts +59 -0
- package/src/email/theme.ts +77 -0
- package/src/index.ts +1 -0
- package/src/manifest/index.ts +37 -0
- package/src/manifest/server.ts +23 -0
- package/src/manifest/web.ts +13 -0
- package/src/server/db.ts +12 -1
- package/src/server/dispatch.ts +4 -0
- package/src/server/router.ts +17 -1
- package/src/server/transports/email.ts +62 -0
- package/src/types.ts +39 -1
- package/dist/chunk-WHBMPHQE.js.map +0 -1
- package/dist/chunk-XE7HZVMH.js.map +0 -1
- /package/dist/{chunk-HHMRCMQU.js.map → chunk-GGAZOLNN.js.map} +0 -0
- /package/dist/{chunk-CUZW62JS.js.map → chunk-KU4FVOFA.js.map} +0 -0
- /package/dist/{chunk-MMLV4EZT.js.map → chunk-M5XTROYM.js.map} +0 -0
- /package/dist/{preferences-screen-IOW6Y2H2.js.map → preferences-screen-V3ZBWP7A.js.map} +0 -0
|
@@ -0,0 +1,502 @@
|
|
|
1
|
+
import {
|
|
2
|
+
__name
|
|
3
|
+
} from "./chunk-7QVYU63E.js";
|
|
4
|
+
|
|
5
|
+
// src/email/previews/react/message-view.tsx
|
|
6
|
+
import { ToggleGroup } from "@12-apps/ui/form/ToggleGroup";
|
|
7
|
+
import { Box } from "@12-apps/ui/mui/Box";
|
|
8
|
+
import { Text } from "@12-apps/ui/typography/Text";
|
|
9
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
10
|
+
var MOBILE_WIDTH = 390;
|
|
11
|
+
function Monospace({ children, testId }) {
|
|
12
|
+
return /* @__PURE__ */ jsx(
|
|
13
|
+
Box,
|
|
14
|
+
{
|
|
15
|
+
component: "pre",
|
|
16
|
+
"data-testid": testId,
|
|
17
|
+
sx: {
|
|
18
|
+
m: 0,
|
|
19
|
+
p: 2,
|
|
20
|
+
borderRadius: 1.5,
|
|
21
|
+
border: "1px solid",
|
|
22
|
+
borderColor: "divider",
|
|
23
|
+
background: "background.default",
|
|
24
|
+
fontSize: 13,
|
|
25
|
+
lineHeight: 1.6,
|
|
26
|
+
whiteSpace: "pre-wrap",
|
|
27
|
+
wordBreak: "break-word",
|
|
28
|
+
maxHeight: 720,
|
|
29
|
+
overflow: "auto"
|
|
30
|
+
},
|
|
31
|
+
children
|
|
32
|
+
}
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
__name(Monospace, "Monospace");
|
|
36
|
+
function HtmlFrame({
|
|
37
|
+
detail,
|
|
38
|
+
width,
|
|
39
|
+
title
|
|
40
|
+
}) {
|
|
41
|
+
return /* @__PURE__ */ jsx(
|
|
42
|
+
Box,
|
|
43
|
+
{
|
|
44
|
+
sx: {
|
|
45
|
+
display: "flex",
|
|
46
|
+
justifyContent: "center",
|
|
47
|
+
borderRadius: 1.5,
|
|
48
|
+
border: "1px solid",
|
|
49
|
+
borderColor: "divider",
|
|
50
|
+
overflow: "hidden"
|
|
51
|
+
},
|
|
52
|
+
children: /* @__PURE__ */ jsx(
|
|
53
|
+
Box,
|
|
54
|
+
{
|
|
55
|
+
component: "iframe",
|
|
56
|
+
"data-testid": "email-preview-frame",
|
|
57
|
+
title,
|
|
58
|
+
srcDoc: detail.html,
|
|
59
|
+
sandbox: "",
|
|
60
|
+
sx: {
|
|
61
|
+
border: 0,
|
|
62
|
+
width: width === "mobile" ? MOBILE_WIDTH : "100%",
|
|
63
|
+
height: 760,
|
|
64
|
+
background: "#fff"
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
)
|
|
68
|
+
}
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
__name(HtmlFrame, "HtmlFrame");
|
|
72
|
+
function MessageView(props) {
|
|
73
|
+
const { detail, copy, tab, width, onTabChange, onWidthChange } = props;
|
|
74
|
+
return /* @__PURE__ */ jsxs(Box, { sx: { display: "flex", flexDirection: "column", gap: 1.5 }, "data-testid": "email-preview-view", children: [
|
|
75
|
+
/* @__PURE__ */ jsxs(Box, { children: [
|
|
76
|
+
/* @__PURE__ */ jsx(Text, { as: "p", size: "xs", color: "secondary", children: copy.subjectLabel }),
|
|
77
|
+
/* @__PURE__ */ jsx(Text, { as: "p", size: "md", weight: "medium", "data-testid": "email-preview-subject", children: detail.subject })
|
|
78
|
+
] }),
|
|
79
|
+
/* @__PURE__ */ jsxs(Box, { sx: { display: "flex", flexWrap: "wrap", gap: 1, alignItems: "center" }, children: [
|
|
80
|
+
/* @__PURE__ */ jsx(
|
|
81
|
+
ToggleGroup,
|
|
82
|
+
{
|
|
83
|
+
dataTestId: "email-preview-tabs",
|
|
84
|
+
exclusive: true,
|
|
85
|
+
value: tab,
|
|
86
|
+
size: "sm",
|
|
87
|
+
options: [
|
|
88
|
+
{ value: "html", label: copy.tabHtml },
|
|
89
|
+
{ value: "text", label: copy.tabText },
|
|
90
|
+
{ value: "source", label: copy.tabSource }
|
|
91
|
+
],
|
|
92
|
+
onChange: (_event, value) => {
|
|
93
|
+
if (value) onTabChange(value);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
),
|
|
97
|
+
tab === "html" ? /* @__PURE__ */ jsx(
|
|
98
|
+
ToggleGroup,
|
|
99
|
+
{
|
|
100
|
+
dataTestId: "email-preview-width",
|
|
101
|
+
exclusive: true,
|
|
102
|
+
value: width,
|
|
103
|
+
size: "sm",
|
|
104
|
+
options: [
|
|
105
|
+
{ value: "desktop", label: copy.widthDesktop },
|
|
106
|
+
{ value: "mobile", label: copy.widthMobile }
|
|
107
|
+
],
|
|
108
|
+
onChange: (_event, value) => {
|
|
109
|
+
if (value) onWidthChange(value);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
) : null
|
|
113
|
+
] }),
|
|
114
|
+
tab === "html" ? /* @__PURE__ */ jsx(HtmlFrame, { detail, width, title: copy.frameTitle }) : null,
|
|
115
|
+
tab === "text" ? /* @__PURE__ */ jsx(Monospace, { testId: "email-preview-text", children: detail.text }) : null,
|
|
116
|
+
tab === "source" ? /* @__PURE__ */ jsx(Monospace, { testId: "email-preview-source", children: detail.html }) : null
|
|
117
|
+
] });
|
|
118
|
+
}
|
|
119
|
+
__name(MessageView, "MessageView");
|
|
120
|
+
|
|
121
|
+
// src/email/previews/react/preview-screen.tsx
|
|
122
|
+
import { useCallback, useMemo, useState as useState2 } from "react";
|
|
123
|
+
import { Alert as Alert2 } from "@12-apps/ui/data-display/Alert";
|
|
124
|
+
import { Input } from "@12-apps/ui/form/Input";
|
|
125
|
+
import { ToggleGroup as ToggleGroup2 } from "@12-apps/ui/form/ToggleGroup";
|
|
126
|
+
import { Box as Box4 } from "@12-apps/ui/mui/Box";
|
|
127
|
+
import { Heading } from "@12-apps/ui/typography/Heading";
|
|
128
|
+
import { Text as Text4 } from "@12-apps/ui/typography/Text";
|
|
129
|
+
|
|
130
|
+
// src/email/previews/react/loadable.tsx
|
|
131
|
+
import { useEffect, useState } from "react";
|
|
132
|
+
import { Alert } from "@12-apps/ui/data-display/Alert";
|
|
133
|
+
import { Button } from "@12-apps/ui/form/Button";
|
|
134
|
+
import { Box as Box2 } from "@12-apps/ui/mui/Box";
|
|
135
|
+
import { Text as Text2 } from "@12-apps/ui/typography/Text";
|
|
136
|
+
import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
137
|
+
function useLoadable(load, options = {}) {
|
|
138
|
+
const [state, setState] = useState({ data: null, error: null });
|
|
139
|
+
const [nonce, setNonce] = useState(0);
|
|
140
|
+
const { keepPrevious = false } = options;
|
|
141
|
+
useEffect(() => {
|
|
142
|
+
let live = true;
|
|
143
|
+
setState((previous) => keepPrevious ? { ...previous, error: null } : { data: null, error: null });
|
|
144
|
+
load().then((data) => live && setState({ data, error: null })).catch(
|
|
145
|
+
(error) => live && setState({ data: null, error: error instanceof Error ? error.message : String(error) })
|
|
146
|
+
);
|
|
147
|
+
return () => {
|
|
148
|
+
live = false;
|
|
149
|
+
};
|
|
150
|
+
}, [load, nonce]);
|
|
151
|
+
return { ...state, reload: /* @__PURE__ */ __name(() => setNonce((n) => n + 1), "reload") };
|
|
152
|
+
}
|
|
153
|
+
__name(useLoadable, "useLoadable");
|
|
154
|
+
function Failure({
|
|
155
|
+
message,
|
|
156
|
+
copy,
|
|
157
|
+
onRetry
|
|
158
|
+
}) {
|
|
159
|
+
return /* @__PURE__ */ jsxs2(Box2, { "data-testid": "email-preview-error", sx: { display: "flex", flexDirection: "column", gap: 1, alignItems: "flex-start" }, children: [
|
|
160
|
+
/* @__PURE__ */ jsxs2(Alert, { severity: "error", children: [
|
|
161
|
+
/* @__PURE__ */ jsx2(Text2, { as: "p", size: "sm", children: copy.loadError }),
|
|
162
|
+
/* @__PURE__ */ jsx2(Text2, { as: "p", size: "sm", children: message })
|
|
163
|
+
] }),
|
|
164
|
+
/* @__PURE__ */ jsx2(Button, { size: "sm", variant: "outline", onClick: onRetry, children: copy.retry })
|
|
165
|
+
] });
|
|
166
|
+
}
|
|
167
|
+
__name(Failure, "Failure");
|
|
168
|
+
|
|
169
|
+
// src/email/previews/react/message-list.tsx
|
|
170
|
+
import { Chip } from "@12-apps/ui/data-display/Chip";
|
|
171
|
+
import { Box as Box3 } from "@12-apps/ui/mui/Box";
|
|
172
|
+
import { Text as Text3 } from "@12-apps/ui/typography/Text";
|
|
173
|
+
import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
|
|
174
|
+
function groupByOwner(rows) {
|
|
175
|
+
const groups = /* @__PURE__ */ new Map();
|
|
176
|
+
for (const row of rows) {
|
|
177
|
+
const bucket = groups.get(row.owner);
|
|
178
|
+
if (bucket) bucket.push(row);
|
|
179
|
+
else groups.set(row.owner, [row]);
|
|
180
|
+
}
|
|
181
|
+
return [...groups].map(([owner, ownerRows]) => ({ owner, rows: ownerRows }));
|
|
182
|
+
}
|
|
183
|
+
__name(groupByOwner, "groupByOwner");
|
|
184
|
+
function matchesFilter(row, filter) {
|
|
185
|
+
const needle = filter.trim().toLowerCase();
|
|
186
|
+
if (needle === "") return true;
|
|
187
|
+
return [row.subject, row.key, row.owner, row.family].some(
|
|
188
|
+
(field) => field.toLowerCase().includes(needle)
|
|
189
|
+
);
|
|
190
|
+
}
|
|
191
|
+
__name(matchesFilter, "matchesFilter");
|
|
192
|
+
function MessageRow({
|
|
193
|
+
row,
|
|
194
|
+
selected,
|
|
195
|
+
onSelect
|
|
196
|
+
}) {
|
|
197
|
+
return /* @__PURE__ */ jsxs3(
|
|
198
|
+
Box3,
|
|
199
|
+
{
|
|
200
|
+
component: "button",
|
|
201
|
+
type: "button",
|
|
202
|
+
"data-testid": `email-preview-row-${row.id}`,
|
|
203
|
+
"aria-current": selected,
|
|
204
|
+
onClick: () => onSelect(row.id),
|
|
205
|
+
sx: {
|
|
206
|
+
appearance: "none",
|
|
207
|
+
textAlign: "left",
|
|
208
|
+
width: "100%",
|
|
209
|
+
cursor: "pointer",
|
|
210
|
+
border: "1px solid",
|
|
211
|
+
borderColor: selected ? "primary.main" : "divider",
|
|
212
|
+
background: selected ? "action.selected" : "background.paper",
|
|
213
|
+
borderRadius: 1.5,
|
|
214
|
+
p: 1.25,
|
|
215
|
+
display: "flex",
|
|
216
|
+
flexDirection: "column",
|
|
217
|
+
gap: 0.5
|
|
218
|
+
},
|
|
219
|
+
children: [
|
|
220
|
+
/* @__PURE__ */ jsx3(Text3, { as: "span", size: "sm", weight: "medium", children: row.subject }),
|
|
221
|
+
/* @__PURE__ */ jsxs3(Box3, { sx: { display: "flex", alignItems: "center", gap: 0.75 }, children: [
|
|
222
|
+
/* @__PURE__ */ jsx3(Chip, { label: row.family, size: "sm", variant: "outlined", color: "neutral" }),
|
|
223
|
+
/* @__PURE__ */ jsx3(Text3, { as: "span", size: "xs", color: "secondary", children: row.key })
|
|
224
|
+
] })
|
|
225
|
+
]
|
|
226
|
+
}
|
|
227
|
+
);
|
|
228
|
+
}
|
|
229
|
+
__name(MessageRow, "MessageRow");
|
|
230
|
+
function MessageList({
|
|
231
|
+
rows,
|
|
232
|
+
selectedId,
|
|
233
|
+
copy,
|
|
234
|
+
onSelect
|
|
235
|
+
}) {
|
|
236
|
+
if (rows.length === 0) {
|
|
237
|
+
return /* @__PURE__ */ jsx3(Text3, { as: "p", size: "sm", color: "secondary", "data-testid": "email-preview-no-matches", children: copy.noMatches });
|
|
238
|
+
}
|
|
239
|
+
return /* @__PURE__ */ jsx3(Box3, { sx: { display: "flex", flexDirection: "column", gap: 2 }, children: groupByOwner(rows).map((group) => /* @__PURE__ */ jsxs3(
|
|
240
|
+
Box3,
|
|
241
|
+
{
|
|
242
|
+
"data-testid": `email-preview-owner-${group.owner}`,
|
|
243
|
+
sx: { display: "flex", flexDirection: "column", gap: 0.75 },
|
|
244
|
+
children: [
|
|
245
|
+
/* @__PURE__ */ jsx3(Text3, { as: "p", size: "xs", weight: "medium", color: "secondary", children: group.owner }),
|
|
246
|
+
group.rows.map((row) => /* @__PURE__ */ jsx3(
|
|
247
|
+
MessageRow,
|
|
248
|
+
{
|
|
249
|
+
row,
|
|
250
|
+
selected: row.id === selectedId,
|
|
251
|
+
onSelect
|
|
252
|
+
},
|
|
253
|
+
row.id
|
|
254
|
+
))
|
|
255
|
+
]
|
|
256
|
+
},
|
|
257
|
+
group.owner
|
|
258
|
+
)) });
|
|
259
|
+
}
|
|
260
|
+
__name(MessageList, "MessageList");
|
|
261
|
+
|
|
262
|
+
// src/email/previews/react/transport.ts
|
|
263
|
+
async function get(url) {
|
|
264
|
+
const response = await fetch(url, { headers: { Accept: "application/json" } });
|
|
265
|
+
let body = {};
|
|
266
|
+
try {
|
|
267
|
+
body = await response.json();
|
|
268
|
+
} catch {
|
|
269
|
+
}
|
|
270
|
+
if (!response.ok || body.data === void 0) {
|
|
271
|
+
throw new Error(body.error ?? `The request failed (${response.status}).`);
|
|
272
|
+
}
|
|
273
|
+
return body.data;
|
|
274
|
+
}
|
|
275
|
+
__name(get, "get");
|
|
276
|
+
var withLocale = /* @__PURE__ */ __name((base, locale) => `${base}?locale=${encodeURIComponent(locale)}`, "withLocale");
|
|
277
|
+
function fetchEmailPreviewIndex(apiBase, locale) {
|
|
278
|
+
return get(withLocale(apiBase, locale));
|
|
279
|
+
}
|
|
280
|
+
__name(fetchEmailPreviewIndex, "fetchEmailPreviewIndex");
|
|
281
|
+
function fetchEmailPreview(apiBase, id, locale) {
|
|
282
|
+
return get(
|
|
283
|
+
withLocale(`${apiBase}/${encodeURIComponent(id)}`, locale)
|
|
284
|
+
);
|
|
285
|
+
}
|
|
286
|
+
__name(fetchEmailPreview, "fetchEmailPreview");
|
|
287
|
+
|
|
288
|
+
// src/email/previews/react/preview-screen.tsx
|
|
289
|
+
import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
|
|
290
|
+
function searchParam(name) {
|
|
291
|
+
if (typeof window === "undefined") return null;
|
|
292
|
+
return new URLSearchParams(window.location.search).get(name);
|
|
293
|
+
}
|
|
294
|
+
__name(searchParam, "searchParam");
|
|
295
|
+
function patchSearch(patch) {
|
|
296
|
+
if (typeof window === "undefined") return;
|
|
297
|
+
const next = new URLSearchParams(window.location.search);
|
|
298
|
+
for (const [key, value] of Object.entries(patch)) next.set(key, value);
|
|
299
|
+
const { pathname, hash } = window.location;
|
|
300
|
+
window.history.replaceState({}, "", `${pathname}?${next.toString()}${hash}`);
|
|
301
|
+
}
|
|
302
|
+
__name(patchSearch, "patchSearch");
|
|
303
|
+
function CoverageNotice({
|
|
304
|
+
coverage,
|
|
305
|
+
copy
|
|
306
|
+
}) {
|
|
307
|
+
if (coverage.missing.length === 0 && coverage.orphan.length === 0) return null;
|
|
308
|
+
return /* @__PURE__ */ jsxs4(Alert2, { severity: "warning", "data-testid": "email-preview-coverage", children: [
|
|
309
|
+
/* @__PURE__ */ jsx4(Text4, { as: "p", size: "sm", weight: "medium", children: copy.coverageTitle }),
|
|
310
|
+
coverage.missing.length > 0 ? /* @__PURE__ */ jsx4(Text4, { as: "p", size: "sm", children: copy.missingSamples(coverage.missing.join(", ")) }) : null,
|
|
311
|
+
coverage.orphan.length > 0 ? /* @__PURE__ */ jsx4(Text4, { as: "p", size: "sm", children: copy.orphanSamples(coverage.orphan.join(", ")) }) : null
|
|
312
|
+
] });
|
|
313
|
+
}
|
|
314
|
+
__name(CoverageNotice, "CoverageNotice");
|
|
315
|
+
function PreviewPane({
|
|
316
|
+
apiBase,
|
|
317
|
+
id,
|
|
318
|
+
locale,
|
|
319
|
+
copy,
|
|
320
|
+
tab,
|
|
321
|
+
width,
|
|
322
|
+
onTabChange,
|
|
323
|
+
onWidthChange
|
|
324
|
+
}) {
|
|
325
|
+
const load = useCallback(
|
|
326
|
+
() => id === null ? Promise.resolve(null) : fetchEmailPreview(apiBase, id, locale),
|
|
327
|
+
[apiBase, id, locale]
|
|
328
|
+
);
|
|
329
|
+
const detail = useLoadable(load);
|
|
330
|
+
if (id === null) {
|
|
331
|
+
return /* @__PURE__ */ jsx4(Text4, { as: "p", size: "sm", color: "secondary", "data-testid": "email-preview-empty", children: copy.pickOne });
|
|
332
|
+
}
|
|
333
|
+
if (detail.error !== null) {
|
|
334
|
+
return /* @__PURE__ */ jsx4(Failure, { message: detail.error, copy, onRetry: detail.reload });
|
|
335
|
+
}
|
|
336
|
+
if (detail.data === null) {
|
|
337
|
+
return /* @__PURE__ */ jsx4(Text4, { as: "p", size: "sm", color: "secondary", "data-testid": "email-preview-detail-loading", children: copy.loading });
|
|
338
|
+
}
|
|
339
|
+
return /* @__PURE__ */ jsx4(
|
|
340
|
+
MessageView,
|
|
341
|
+
{
|
|
342
|
+
detail: detail.data,
|
|
343
|
+
copy,
|
|
344
|
+
tab,
|
|
345
|
+
width,
|
|
346
|
+
onTabChange,
|
|
347
|
+
onWidthChange
|
|
348
|
+
}
|
|
349
|
+
);
|
|
350
|
+
}
|
|
351
|
+
__name(PreviewPane, "PreviewPane");
|
|
352
|
+
function CatalogueColumn({
|
|
353
|
+
index,
|
|
354
|
+
copy,
|
|
355
|
+
selectedId,
|
|
356
|
+
onSelect
|
|
357
|
+
}) {
|
|
358
|
+
const [filter, setFilter] = useState2("");
|
|
359
|
+
const visible = useMemo(
|
|
360
|
+
() => index.items.filter((row) => matchesFilter(row, filter)),
|
|
361
|
+
[index.items, filter]
|
|
362
|
+
);
|
|
363
|
+
return (
|
|
364
|
+
/*
|
|
365
|
+
The list owns its OWN scroll rather than growing the page.
|
|
366
|
+
|
|
367
|
+
A host's console is typically a fixed-viewport shell whose centre column
|
|
368
|
+
scrolls, and a twenty-mail catalogue then scrolls the preview frame off
|
|
369
|
+
the screen — the two things this screen exists to show side by side
|
|
370
|
+
cannot both be on it. `sticky` keeps the list put while the frame is
|
|
371
|
+
read; the height is the viewport minus the chrome above it, so the column
|
|
372
|
+
ends where the window does rather than at an arbitrary pixel count.
|
|
373
|
+
*/
|
|
374
|
+
/* @__PURE__ */ jsxs4(
|
|
375
|
+
Box4,
|
|
376
|
+
{
|
|
377
|
+
sx: {
|
|
378
|
+
width: 320,
|
|
379
|
+
flexShrink: 0,
|
|
380
|
+
display: "flex",
|
|
381
|
+
flexDirection: "column",
|
|
382
|
+
gap: 1.5,
|
|
383
|
+
position: "sticky",
|
|
384
|
+
top: 0,
|
|
385
|
+
maxHeight: "calc(100dvh - 220px)",
|
|
386
|
+
minHeight: 240
|
|
387
|
+
},
|
|
388
|
+
children: [
|
|
389
|
+
/* @__PURE__ */ jsx4(
|
|
390
|
+
Input,
|
|
391
|
+
{
|
|
392
|
+
label: copy.searchLabel,
|
|
393
|
+
placeholder: copy.searchPlaceholder,
|
|
394
|
+
value: filter,
|
|
395
|
+
"data-testid": "email-preview-filter",
|
|
396
|
+
onChange: (event) => setFilter(event.target.value),
|
|
397
|
+
fullWidth: true
|
|
398
|
+
}
|
|
399
|
+
),
|
|
400
|
+
/* @__PURE__ */ jsx4(Box4, { sx: { overflowY: "auto", flex: 1, pr: 0.5 }, children: /* @__PURE__ */ jsx4(MessageList, { rows: visible, selectedId, copy, onSelect }) })
|
|
401
|
+
]
|
|
402
|
+
}
|
|
403
|
+
)
|
|
404
|
+
);
|
|
405
|
+
}
|
|
406
|
+
__name(CatalogueColumn, "CatalogueColumn");
|
|
407
|
+
function Browser({
|
|
408
|
+
apiBase,
|
|
409
|
+
index,
|
|
410
|
+
copy,
|
|
411
|
+
locale,
|
|
412
|
+
selectedId,
|
|
413
|
+
onPatch
|
|
414
|
+
}) {
|
|
415
|
+
const [tab, setTab] = useState2("html");
|
|
416
|
+
const [width, setWidth] = useState2("desktop");
|
|
417
|
+
return /* @__PURE__ */ jsxs4(Box4, { sx: { display: "flex", flexDirection: "column", gap: 2 }, children: [
|
|
418
|
+
/* @__PURE__ */ jsx4(CoverageNotice, { coverage: index.coverage, copy }),
|
|
419
|
+
/* @__PURE__ */ jsx4(
|
|
420
|
+
ToggleGroup2,
|
|
421
|
+
{
|
|
422
|
+
dataTestId: "email-preview-locale",
|
|
423
|
+
exclusive: true,
|
|
424
|
+
value: locale,
|
|
425
|
+
size: "sm",
|
|
426
|
+
options: index.locales.map((tag) => ({ value: tag, label: tag })),
|
|
427
|
+
onChange: (_event, value) => {
|
|
428
|
+
if (value) onPatch({ locale: String(value) });
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
),
|
|
432
|
+
/* @__PURE__ */ jsxs4(Box4, { sx: { display: "flex", gap: 3, alignItems: "flex-start", flexWrap: "wrap" }, children: [
|
|
433
|
+
/* @__PURE__ */ jsx4(
|
|
434
|
+
CatalogueColumn,
|
|
435
|
+
{
|
|
436
|
+
index,
|
|
437
|
+
copy,
|
|
438
|
+
selectedId,
|
|
439
|
+
onSelect: (id) => onPatch({ id })
|
|
440
|
+
}
|
|
441
|
+
),
|
|
442
|
+
/* @__PURE__ */ jsx4(Box4, { sx: { flex: 1, minWidth: 360 }, children: /* @__PURE__ */ jsx4(
|
|
443
|
+
PreviewPane,
|
|
444
|
+
{
|
|
445
|
+
apiBase,
|
|
446
|
+
id: selectedId,
|
|
447
|
+
locale,
|
|
448
|
+
copy,
|
|
449
|
+
tab,
|
|
450
|
+
width,
|
|
451
|
+
onTabChange: setTab,
|
|
452
|
+
onWidthChange: setWidth
|
|
453
|
+
}
|
|
454
|
+
) })
|
|
455
|
+
] })
|
|
456
|
+
] });
|
|
457
|
+
}
|
|
458
|
+
__name(Browser, "Browser");
|
|
459
|
+
function createEmailPreviewScreen(config) {
|
|
460
|
+
const { apiBase, copy } = config;
|
|
461
|
+
function EmailPreviewsPage() {
|
|
462
|
+
const [, setUrlNonce] = useState2(0);
|
|
463
|
+
const locale = searchParam("locale") ?? "";
|
|
464
|
+
const selectedId = searchParam("id");
|
|
465
|
+
const load = useCallback(
|
|
466
|
+
() => fetchEmailPreviewIndex(apiBase, locale),
|
|
467
|
+
[apiBase, locale]
|
|
468
|
+
);
|
|
469
|
+
const index = useLoadable(load, { keepPrevious: true });
|
|
470
|
+
const patch = /* @__PURE__ */ __name((next) => {
|
|
471
|
+
patchSearch(next);
|
|
472
|
+
setUrlNonce((n) => n + 1);
|
|
473
|
+
}, "patch");
|
|
474
|
+
return /* @__PURE__ */ jsxs4(Box4, { "data-testid": "page-email-previews", sx: { display: "flex", flexDirection: "column", gap: 2 }, children: [
|
|
475
|
+
/* @__PURE__ */ jsxs4(Box4, { children: [
|
|
476
|
+
/* @__PURE__ */ jsx4(Heading, { level: "h2", children: copy.title }),
|
|
477
|
+
/* @__PURE__ */ jsx4(Text4, { as: "p", size: "sm", color: "secondary", children: copy.description })
|
|
478
|
+
] }),
|
|
479
|
+
index.error !== null ? /* @__PURE__ */ jsx4(Failure, { message: index.error, copy, onRetry: index.reload }) : null,
|
|
480
|
+
index.error === null && index.data === null ? /* @__PURE__ */ jsx4(Text4, { as: "p", size: "sm", color: "secondary", "data-testid": "email-preview-index-loading", children: copy.loading }) : null,
|
|
481
|
+
index.data !== null ? /* @__PURE__ */ jsx4(
|
|
482
|
+
Browser,
|
|
483
|
+
{
|
|
484
|
+
apiBase,
|
|
485
|
+
index: index.data,
|
|
486
|
+
copy,
|
|
487
|
+
locale: index.data.locale,
|
|
488
|
+
selectedId,
|
|
489
|
+
onPatch: patch
|
|
490
|
+
}
|
|
491
|
+
) : null
|
|
492
|
+
] });
|
|
493
|
+
}
|
|
494
|
+
__name(EmailPreviewsPage, "EmailPreviewsPage");
|
|
495
|
+
return { page: EmailPreviewsPage };
|
|
496
|
+
}
|
|
497
|
+
__name(createEmailPreviewScreen, "createEmailPreviewScreen");
|
|
498
|
+
|
|
499
|
+
export {
|
|
500
|
+
createEmailPreviewScreen
|
|
501
|
+
};
|
|
502
|
+
//# sourceMappingURL=chunk-ZFIYBNZ7.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/email/previews/react/message-view.tsx","../src/email/previews/react/preview-screen.tsx","../src/email/previews/react/loadable.tsx","../src/email/previews/react/message-list.tsx","../src/email/previews/react/transport.ts"],"sourcesContent":["import type { JSX } from 'react';\n\nimport { ToggleGroup } from '@12-apps/ui/form/ToggleGroup';\nimport { Box } from '@12-apps/ui/mui/Box';\nimport { Text } from '@12-apps/ui/typography/Text';\n\nimport type { EmailPreviewDetail } from '../catalog';\n\nimport type { EmailPreviewScreenCopy } from './copy';\n\n/**\n * One rendered message, in the three ways it is worth looking at.\n *\n * - **HTML** — what most people will see, in a sandboxed frame at either of the\n * two widths that matter. A phone is not a nice-to-have: more than half of\n * transactional mail is opened on one, and the 600px card is exactly the\n * thing that either survives that or does not.\n * - **Text** — the plain-text twin. Worth its own tab because it is what a spam\n * filter scores, what a watch shows, and what a screen reader in plain-text\n * mode reads — and because it is the half nobody ever looks at, which is how\n * it drifts out of step with the HTML.\n * - **Source** — the markup itself, for the moment somebody is debugging why a\n * client rendered it oddly.\n *\n * ## Why an iframe, and why sandboxed\n *\n * The mail is a whole document with its own `<body>` background, and rendering\n * that inside the console's DOM would both break the mail (the console's CSS\n * reaches it) and break the console (the mail's body styles reach the page). A\n * frame is the only honest preview.\n *\n * `sandbox=\"\"` — no scripts, no forms, no top-level navigation. These documents\n * come from the host's own renderer and carry no script, so this is less a\n * containment measure than a statement that the preview is INERT: a click on a\n * CTA inside a previewed mail must never navigate the operator anywhere, least\n * of all to a sample verification link.\n */\n\n/** The two widths the HTML view renders at. */\nexport type PreviewWidth = 'desktop' | 'mobile';\n\n/** Which of the three views is showing. */\nexport type PreviewTab = 'html' | 'text' | 'source';\n\n/** A phone. 390px is a common iPhone CSS width, and among the narrowest. */\nconst MOBILE_WIDTH = 390;\n\nfunction Monospace({ children, testId }: { children: string; testId: string }): JSX.Element {\n return (\n <Box\n component=\"pre\"\n data-testid={testId}\n sx={{\n m: 0,\n p: 2,\n borderRadius: 1.5,\n border: '1px solid',\n borderColor: 'divider',\n background: 'background.default',\n fontSize: 13,\n lineHeight: 1.6,\n whiteSpace: 'pre-wrap',\n wordBreak: 'break-word',\n maxHeight: 720,\n overflow: 'auto',\n }}\n >\n {children}\n </Box>\n );\n}\n\nfunction HtmlFrame({\n detail,\n width,\n title,\n}: {\n detail: EmailPreviewDetail;\n width: PreviewWidth;\n title: string;\n}): JSX.Element {\n return (\n <Box\n sx={{\n display: 'flex',\n justifyContent: 'center',\n borderRadius: 1.5,\n border: '1px solid',\n borderColor: 'divider',\n overflow: 'hidden',\n }}\n >\n <Box\n component=\"iframe\"\n data-testid=\"email-preview-frame\"\n title={title}\n srcDoc={detail.html}\n sandbox=\"\"\n sx={{\n border: 0,\n width: width === 'mobile' ? MOBILE_WIDTH : '100%',\n height: 760,\n background: '#fff',\n }}\n />\n </Box>\n );\n}\n\ninterface MessageViewProps {\n detail: EmailPreviewDetail;\n copy: EmailPreviewScreenCopy;\n tab: PreviewTab;\n width: PreviewWidth;\n onTabChange: (tab: PreviewTab) => void;\n onWidthChange: (width: PreviewWidth) => void;\n}\n\nexport function MessageView(props: MessageViewProps): JSX.Element {\n const { detail, copy, tab, width, onTabChange, onWidthChange } = props;\n return (\n <Box sx={{ display: 'flex', flexDirection: 'column', gap: 1.5 }} data-testid=\"email-preview-view\">\n <Box>\n <Text as=\"p\" size=\"xs\" color=\"secondary\">\n {copy.subjectLabel}\n </Text>\n <Text as=\"p\" size=\"md\" weight=\"medium\" data-testid=\"email-preview-subject\">\n {detail.subject}\n </Text>\n </Box>\n <Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 1, alignItems: 'center' }}>\n <ToggleGroup\n dataTestId=\"email-preview-tabs\"\n exclusive\n value={tab}\n size=\"sm\"\n options={[\n { value: 'html', label: copy.tabHtml },\n { value: 'text', label: copy.tabText },\n { value: 'source', label: copy.tabSource },\n ]}\n onChange={(_event, value) => {\n if (value) onTabChange(value as PreviewTab);\n }}\n />\n {tab === 'html' ? (\n <ToggleGroup\n dataTestId=\"email-preview-width\"\n exclusive\n value={width}\n size=\"sm\"\n options={[\n { value: 'desktop', label: copy.widthDesktop },\n { value: 'mobile', label: copy.widthMobile },\n ]}\n onChange={(_event, value) => {\n if (value) onWidthChange(value as PreviewWidth);\n }}\n />\n ) : null}\n </Box>\n {tab === 'html' ? <HtmlFrame detail={detail} width={width} title={copy.frameTitle} /> : null}\n {tab === 'text' ? <Monospace testId=\"email-preview-text\">{detail.text}</Monospace> : null}\n {tab === 'source' ? <Monospace testId=\"email-preview-source\">{detail.html}</Monospace> : null}\n </Box>\n );\n}\n","import { useCallback, useMemo, useState, type JSX } from 'react';\n\nimport { Alert } from '@12-apps/ui/data-display/Alert';\nimport { Input } from '@12-apps/ui/form/Input';\nimport { ToggleGroup } from '@12-apps/ui/form/ToggleGroup';\nimport { Box } from '@12-apps/ui/mui/Box';\nimport { Heading } from '@12-apps/ui/typography/Heading';\nimport { Text } from '@12-apps/ui/typography/Text';\n\nimport type { EmailPreviewCoverage, EmailPreviewDetail, EmailPreviewIndex } from '../catalog';\n\nimport type { EmailPreviewScreenCopy } from './copy';\nimport { Failure, useLoadable } from './loadable';\nimport { MessageList, matchesFilter } from './message-list';\nimport { MessageView, type PreviewTab, type PreviewWidth } from './message-view';\nimport { fetchEmailPreview, fetchEmailPreviewIndex } from './transport';\n\n/**\n * The operator screen over the `./email/previews` catalogue.\n *\n * ## What a host supplies, and what it does not\n *\n * `apiBase` — where the routes are mounted — and `copy`, by name. Nothing else:\n * the owners, the languages, the messages and their subjects all arrive from\n * the surface, because they are facts about the host's own mail that no prop\n * could usefully restate.\n *\n * ## The selection lives in the URL, without a router\n *\n * `?id=` and `?locale=`, read and written through `history.replaceState`. A\n * link to one mail in one language is the actual workflow this screen serves —\n * \"look at what the reset mail says now\" — and local state would make every\n * such conversation a set of instructions instead of a link.\n *\n * Deliberately NOT a router integration: this package cannot know whether a\n * host runs react-router, TanStack Router or a framework's own, and a screen\n * that imported one would be unmountable in the other two. `replaceState` is\n * the one API all of them are built on, and `replace` rather than `push` so\n * browsing twenty mails is not twenty back-button steps.\n */\n\nexport interface EmailPreviewScreenConfig {\n /** Where the routes are mounted, e.g. `/api/platform/email-previews`. */\n readonly apiBase: string;\n /** The screen's words. REQUIRED — see `./copy`. */\n readonly copy: EmailPreviewScreenCopy;\n}\n\n/** Read one search param without assuming a router owns the URL. */\nfunction searchParam(name: string): string | null {\n if (typeof window === 'undefined') return null;\n return new URLSearchParams(window.location.search).get(name);\n}\n\n/**\n * Patch the query string in place, keeping everything else about the URL.\n *\n * The HASH is carried over deliberately, and it is not defensive coding: a host\n * that routes on `location.hash` — the consumer harness does, and so does any\n * SPA served from a static file — would otherwise be navigated off this screen\n * by its own locale switch, because rebuilding the URL from `pathname` alone\n * silently drops the fragment that says which page this is.\n *\n * `replaceState` rather than `pushState` for the reason the screen exists:\n * browsing twenty messages is one place to come back from, not twenty\n * back-button steps.\n */\nfunction patchSearch(patch: Record<string, string>): void {\n if (typeof window === 'undefined') return;\n const next = new URLSearchParams(window.location.search);\n for (const [key, value] of Object.entries(patch)) next.set(key, value);\n const { pathname, hash } = window.location;\n window.history.replaceState({}, '', `${pathname}?${next.toString()}${hash}`);\n}\n\n/** The surface's honest report about what it cannot show. */\nfunction CoverageNotice({\n coverage,\n copy,\n}: {\n coverage: EmailPreviewCoverage;\n copy: EmailPreviewScreenCopy;\n}): JSX.Element | null {\n if (coverage.missing.length === 0 && coverage.orphan.length === 0) return null;\n return (\n <Alert severity=\"warning\" data-testid=\"email-preview-coverage\">\n <Text as=\"p\" size=\"sm\" weight=\"medium\">\n {copy.coverageTitle}\n </Text>\n {coverage.missing.length > 0 ? (\n <Text as=\"p\" size=\"sm\">\n {copy.missingSamples(coverage.missing.join(', '))}\n </Text>\n ) : null}\n {coverage.orphan.length > 0 ? (\n <Text as=\"p\" size=\"sm\">\n {copy.orphanSamples(coverage.orphan.join(', '))}\n </Text>\n ) : null}\n </Alert>\n );\n}\n\n/** The right-hand pane: the selected message, or an invitation to pick one. */\nfunction PreviewPane({\n apiBase,\n id,\n locale,\n copy,\n tab,\n width,\n onTabChange,\n onWidthChange,\n}: {\n apiBase: string;\n id: string | null;\n locale: string;\n copy: EmailPreviewScreenCopy;\n tab: PreviewTab;\n width: PreviewWidth;\n onTabChange: (tab: PreviewTab) => void;\n onWidthChange: (width: PreviewWidth) => void;\n}): JSX.Element {\n const load = useCallback(\n () =>\n id === null\n ? Promise.resolve(null as EmailPreviewDetail | null)\n : fetchEmailPreview(apiBase, id, locale),\n [apiBase, id, locale],\n );\n const detail = useLoadable(load);\n\n if (id === null) {\n return (\n <Text as=\"p\" size=\"sm\" color=\"secondary\" data-testid=\"email-preview-empty\">\n {copy.pickOne}\n </Text>\n );\n }\n if (detail.error !== null) {\n return <Failure message={detail.error} copy={copy} onRetry={detail.reload} />;\n }\n if (detail.data === null) {\n return (\n <Text as=\"p\" size=\"sm\" color=\"secondary\" data-testid=\"email-preview-detail-loading\">\n {copy.loading}\n </Text>\n );\n }\n return (\n <MessageView\n detail={detail.data}\n copy={copy}\n tab={tab}\n width={width}\n onTabChange={onTabChange}\n onWidthChange={onWidthChange}\n />\n );\n}\n\n/** The left column: the filter, and the rows under their owners. */\nfunction CatalogueColumn({\n index,\n copy,\n selectedId,\n onSelect,\n}: {\n index: EmailPreviewIndex;\n copy: EmailPreviewScreenCopy;\n selectedId: string | null;\n onSelect: (id: string) => void;\n}): JSX.Element {\n const [filter, setFilter] = useState('');\n const visible = useMemo(\n () => index.items.filter((row) => matchesFilter(row, filter)),\n [index.items, filter],\n );\n return (\n /*\n The list owns its OWN scroll rather than growing the page.\n\n A host's console is typically a fixed-viewport shell whose centre column\n scrolls, and a twenty-mail catalogue then scrolls the preview frame off\n the screen — the two things this screen exists to show side by side\n cannot both be on it. `sticky` keeps the list put while the frame is\n read; the height is the viewport minus the chrome above it, so the column\n ends where the window does rather than at an arbitrary pixel count.\n */\n <Box\n sx={{\n width: 320,\n flexShrink: 0,\n display: 'flex',\n flexDirection: 'column',\n gap: 1.5,\n position: 'sticky',\n top: 0,\n maxHeight: 'calc(100dvh - 220px)',\n minHeight: 240,\n }}\n >\n <Input\n label={copy.searchLabel}\n placeholder={copy.searchPlaceholder}\n value={filter}\n data-testid=\"email-preview-filter\"\n onChange={(event) => setFilter(event.target.value)}\n fullWidth\n />\n {/* Only the ROWS scroll — the filter field stays reachable. */}\n <Box sx={{ overflowY: 'auto', flex: 1, pr: 0.5 }}>\n <MessageList rows={visible} selectedId={selectedId} copy={copy} onSelect={onSelect} />\n </Box>\n </Box>\n );\n}\n\n/** The catalogue and the preview, once the index has loaded. */\nfunction Browser({\n apiBase,\n index,\n copy,\n locale,\n selectedId,\n onPatch,\n}: {\n apiBase: string;\n index: EmailPreviewIndex;\n copy: EmailPreviewScreenCopy;\n locale: string;\n selectedId: string | null;\n onPatch: (patch: Record<string, string>) => void;\n}): JSX.Element {\n const [tab, setTab] = useState<PreviewTab>('html');\n const [width, setWidth] = useState<PreviewWidth>('desktop');\n\n return (\n <Box sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}>\n <CoverageNotice coverage={index.coverage} copy={copy} />\n <ToggleGroup\n dataTestId=\"email-preview-locale\"\n exclusive\n value={locale}\n size=\"sm\"\n options={index.locales.map((tag) => ({ value: tag, label: tag }))}\n onChange={(_event, value) => {\n if (value) onPatch({ locale: String(value) });\n }}\n />\n <Box sx={{ display: 'flex', gap: 3, alignItems: 'flex-start', flexWrap: 'wrap' }}>\n <CatalogueColumn\n index={index}\n copy={copy}\n selectedId={selectedId}\n onSelect={(id) => onPatch({ id })}\n />\n <Box sx={{ flex: 1, minWidth: 360 }}>\n <PreviewPane\n apiBase={apiBase}\n id={selectedId}\n locale={locale}\n copy={copy}\n tab={tab}\n width={width}\n onTabChange={setTab}\n onWidthChange={setWidth}\n />\n </Box>\n </Box>\n </Box>\n );\n}\n\n/**\n * Build the screen. One call, one config object — the shape every factory in\n * this estate has.\n */\nexport function createEmailPreviewScreen(config: EmailPreviewScreenConfig): {\n page: () => JSX.Element;\n} {\n const { apiBase, copy } = config;\n\n function EmailPreviewsPage(): JSX.Element {\n // The VALUE is deliberately discarded: nothing reads the counter, and the\n // only thing it has to do is change, so React re-renders and the URL is\n // re-read below. It used to be a dependency of the catalogue fetch, which\n // is what made every row click refetch the list.\n const [, setUrlNonce] = useState(0);\n const locale = searchParam('locale') ?? '';\n const selectedId = searchParam('id');\n // The catalogue depends on the LANGUAGE and on nothing else. `urlNonce` is\n // deliberately absent: it counts every URL patch, selection included, and\n // including it here refetched the whole catalogue on each row click — for a\n // list whose contents cannot have changed. What the click actually needs is\n // a re-RENDER, so `selectedId` is re-read, and `setUrlNonce` already does\n // that on its own.\n const load = useCallback(\n () => fetchEmailPreviewIndex(apiBase, locale),\n [apiBase, locale],\n );\n const index = useLoadable(load, { keepPrevious: true });\n\n const patch = (next: Record<string, string>): void => {\n patchSearch(next);\n // `replaceState` does not notify React, so the screen re-reads the URL\n // through this counter rather than through a router's own subscription.\n setUrlNonce((n) => n + 1);\n };\n\n return (\n <Box data-testid=\"page-email-previews\" sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}>\n <Box>\n <Heading level=\"h2\">{copy.title}</Heading>\n <Text as=\"p\" size=\"sm\" color=\"secondary\">\n {copy.description}\n </Text>\n </Box>\n {index.error !== null ? (\n <Failure message={index.error} copy={copy} onRetry={index.reload} />\n ) : null}\n {index.error === null && index.data === null ? (\n <Text as=\"p\" size=\"sm\" color=\"secondary\" data-testid=\"email-preview-index-loading\">\n {copy.loading}\n </Text>\n ) : null}\n {index.data !== null ? (\n <Browser\n apiBase={apiBase}\n index={index.data}\n copy={copy}\n locale={index.data.locale}\n selectedId={selectedId}\n onPatch={patch}\n />\n ) : null}\n </Box>\n );\n }\n\n return { page: EmailPreviewsPage };\n}\n","import type { JSX } from 'react';\nimport { useEffect, useState } from 'react';\n\nimport { Alert } from '@12-apps/ui/data-display/Alert';\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 { EmailPreviewScreenCopy } from './copy';\n\n/**\n * The screen's async plumbing: one hook, and the state it cannot render itself.\n *\n * Its own module because it is the half with no opinion about e-mail. What is\n * left in `preview-screen.tsx` is composition — which columns, which panes,\n * what the URL says — and this is the machinery underneath all of it.\n *\n * `keepPrevious` is the load-bearing option and its docblock says why: a\n * consumer that renders children only while `data` is non-null gets those\n * children UNMOUNTED by a blanking reload, which silently discards whatever\n * state they held.\n */\n\n/** A load that can fail, in the two states a screen has to render. */\ninterface Loadable<T> {\n data: T | null;\n error: string | null;\n}\n\nexport function useLoadable<T>(\n load: () => Promise<T>,\n options: { keepPrevious?: boolean } = {},\n): Loadable<T> & { reload: () => void } {\n const [state, setState] = useState<Loadable<T>>({ data: null, error: null });\n const [nonce, setNonce] = useState(0);\n const { keepPrevious = false } = options;\n useEffect(() => {\n let live = true;\n // `keepPrevious` holds the last good answer on screen while the next one is\n // in flight, and it is not a nicety. The consumer of this hook renders its\n // children only while `data` is non-null, so blanking here UNMOUNTS them —\n // taking the filter text, the open tab and the chosen width with it. On a\n // fast connection the refetch lands before anyone notices; on a slow one\n // the operator watches what they just typed disappear.\n setState((previous) => (keepPrevious ? { ...previous, error: null } : { data: null, error: null }));\n load()\n .then((data) => live && setState({ data, error: null }))\n .catch(\n (error: unknown) =>\n live &&\n setState({ data: null, error: error instanceof Error ? error.message : String(error) }),\n );\n return () => {\n // A language switched twice in a second must not let the FIRST answer\n // land last — the screen would show a document the operator did not ask\n // for, with the toggle disagreeing.\n live = false;\n };\n }, [load, nonce]);\n return { ...state, reload: () => setNonce((n) => n + 1) };\n}\n\nexport function Failure({\n message,\n copy,\n onRetry,\n}: {\n message: string;\n copy: EmailPreviewScreenCopy;\n onRetry: () => void;\n}): JSX.Element {\n return (\n <Box data-testid=\"email-preview-error\" sx={{ display: 'flex', flexDirection: 'column', gap: 1, alignItems: 'flex-start' }}>\n <Alert severity=\"error\">\n <Text as=\"p\" size=\"sm\">{copy.loadError}</Text>\n <Text as=\"p\" size=\"sm\">{message}</Text>\n </Alert>\n <Button size=\"sm\" variant=\"outline\" onClick={onRetry}>\n {copy.retry}\n </Button>\n </Box>\n );\n}\n","import type { JSX } from 'react';\n\nimport { Chip } from '@12-apps/ui/data-display/Chip';\nimport { Box } from '@12-apps/ui/mui/Box';\nimport { Text } from '@12-apps/ui/typography/Text';\n\nimport type { EmailPreviewRow } from '../catalog';\n\nimport type { EmailPreviewScreenCopy } from './copy';\n\n/**\n * The catalogue, grouped by the PACKAGE that owns each message.\n *\n * Grouping by owner rather than by family is the whole answer to \"which parts\n * of this system send mail\": the section headers ARE that list, derived from\n * the rows the surface sent rather than written down anywhere, so a package\n * that starts sending mail appears the day it does.\n *\n * The family stays visible as a chip on each row, because it is the other\n * question an operator asks — and one owner can span two families.\n */\n\ninterface OwnerGroup {\n owner: string;\n rows: EmailPreviewRow[];\n}\n\n/** Group in FIRST-SEEN order, so the list does not reshuffle between renders. */\nfunction groupByOwner(rows: readonly EmailPreviewRow[]): OwnerGroup[] {\n const groups = new Map<string, EmailPreviewRow[]>();\n for (const row of rows) {\n const bucket = groups.get(row.owner);\n if (bucket) bucket.push(row);\n else groups.set(row.owner, [row]);\n }\n return [...groups].map(([owner, ownerRows]) => ({ owner, rows: ownerRows }));\n}\n\n/** Does this row match what was typed? Subject, key, owner and family all count. */\nexport function matchesFilter(row: EmailPreviewRow, filter: string): boolean {\n const needle = filter.trim().toLowerCase();\n if (needle === '') return true;\n return [row.subject, row.key, row.owner, row.family].some((field) =>\n field.toLowerCase().includes(needle),\n );\n}\n\nfunction MessageRow({\n row,\n selected,\n onSelect,\n}: {\n row: EmailPreviewRow;\n selected: boolean;\n onSelect: (id: string) => void;\n}): JSX.Element {\n return (\n <Box\n component=\"button\"\n type=\"button\"\n data-testid={`email-preview-row-${row.id}`}\n aria-current={selected}\n onClick={() => onSelect(row.id)}\n sx={{\n appearance: 'none',\n textAlign: 'left',\n width: '100%',\n cursor: 'pointer',\n border: '1px solid',\n borderColor: selected ? 'primary.main' : 'divider',\n background: selected ? 'action.selected' : 'background.paper',\n borderRadius: 1.5,\n p: 1.25,\n display: 'flex',\n flexDirection: 'column',\n gap: 0.5,\n }}\n >\n <Text as=\"span\" size=\"sm\" weight=\"medium\">\n {row.subject}\n </Text>\n <Box sx={{ display: 'flex', alignItems: 'center', gap: 0.75 }}>\n <Chip label={row.family} size=\"sm\" variant=\"outlined\" color=\"neutral\" />\n <Text as=\"span\" size=\"xs\" color=\"secondary\">\n {row.key}\n </Text>\n </Box>\n </Box>\n );\n}\n\nexport function MessageList({\n rows,\n selectedId,\n copy,\n onSelect,\n}: {\n rows: readonly EmailPreviewRow[];\n selectedId: string | null;\n copy: EmailPreviewScreenCopy;\n onSelect: (id: string) => void;\n}): JSX.Element {\n if (rows.length === 0) {\n return (\n <Text as=\"p\" size=\"sm\" color=\"secondary\" data-testid=\"email-preview-no-matches\">\n {copy.noMatches}\n </Text>\n );\n }\n return (\n <Box sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}>\n {groupByOwner(rows).map((group) => (\n <Box\n key={group.owner}\n data-testid={`email-preview-owner-${group.owner}`}\n sx={{ display: 'flex', flexDirection: 'column', gap: 0.75 }}\n >\n <Text as=\"p\" size=\"xs\" weight=\"medium\" color=\"secondary\">\n {group.owner}\n </Text>\n {group.rows.map((row) => (\n <MessageRow\n key={row.id}\n row={row}\n selected={row.id === selectedId}\n onSelect={onSelect}\n />\n ))}\n </Box>\n ))}\n </Box>\n );\n}\n","import type { EmailPreviewDetail, EmailPreviewIndex } from '../catalog';\n\n/**\n * How the screen reaches its own endpoints.\n *\n * Plain `fetch` rather than a data library: this package cannot know whether a\n * host runs react-query, SWR or nothing at all, and a screen that dragged one\n * in would put a second cache beside whichever the host already has. The two\n * calls here are a list and a document — neither needs invalidation, retries or\n * shared state, which is most of what a data library is for.\n *\n * Both unwrap the `{ data }` envelope the routes write, and both surface a\n * non-2xx as a thrown `Error` carrying whatever the surface said, so the screen\n * can show the operator the real refusal (an unknown locale, a 403 from the\n * host's own gate) rather than a generic failure.\n */\n\n/** The envelope every route in this package answers with. */\ninterface Envelope<T> {\n data?: T;\n error?: string;\n}\n\nasync function get<T>(url: string): Promise<T> {\n const response = await fetch(url, { headers: { Accept: 'application/json' } });\n let body: Envelope<T> = {};\n try {\n body = (await response.json()) as Envelope<T>;\n } catch {\n // A gate that refuses before the router runs may answer HTML, not JSON.\n // Falling through to the status line below is more useful than a parse\n // error naming a character offset.\n }\n if (!response.ok || body.data === undefined) {\n throw new Error(body.error ?? `The request failed (${response.status}).`);\n }\n return body.data;\n}\n\nconst withLocale = (base: string, locale: string): string =>\n `${base}?locale=${encodeURIComponent(locale)}`;\n\n/** The catalogue, with every subject rendered in `locale`. */\nexport function fetchEmailPreviewIndex(\n apiBase: string,\n locale: string,\n): Promise<EmailPreviewIndex> {\n return get<EmailPreviewIndex>(withLocale(apiBase, locale));\n}\n\n/** One rendered message. */\nexport function fetchEmailPreview(\n apiBase: string,\n id: string,\n locale: string,\n): Promise<EmailPreviewDetail> {\n return get<EmailPreviewDetail>(\n withLocale(`${apiBase}/${encodeURIComponent(id)}`, locale),\n );\n}\n"],"mappings":"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","names":["useState","Alert","ToggleGroup","Box","Text","Box","Text","jsx","jsxs","Box","Text","Box","Text","jsx","jsxs","Box","Text","jsx","jsxs","Alert","Text","useState","Box","ToggleGroup"]}
|
package/dist/{create-api-notifications-B3u6Kx3x.d.ts → create-api-notifications-B-VMezVB.d.ts}
RENAMED
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import { N as NotificationGeneratorRegistry } from './generators-
|
|
2
|
-
import { b as NotificationRow, C as ChannelMatrix, a as ChannelRow, L as ListNotificationsResult, h as NotificationsCopySource, c as NotificationWireMessages } from './wire-
|
|
3
|
-
import {
|
|
4
|
-
import { e as WebPushSubscriptionSource, D as DriverDeclarationBase, a as WebPushDriverDeclaration, c as WebPushSender } from './web-push-
|
|
1
|
+
import { N as NotificationGeneratorRegistry } from './generators-BsuXe8Mp.js';
|
|
2
|
+
import { b as NotificationRow, C as ChannelMatrix, a as ChannelRow, L as ListNotificationsResult, h as NotificationsCopySource, c as NotificationWireMessages } from './wire-_coV3tJk.js';
|
|
3
|
+
import { c as NotificationChannel, D as DeliveryStatus, g as NotificationLogger, j as NotificationTransport, d as NotificationContent, b as NotificationCategory, e as NotificationEvent, N as NotificationGenerator } from './types-BFmAY0P5.js';
|
|
4
|
+
import { e as WebPushSubscriptionSource, D as DriverDeclarationBase, a as WebPushDriverDeclaration, c as WebPushSender } from './web-push-D-ysQd5h.js';
|
|
5
|
+
import { d as EmailChromeCopy, f as EmailTheme } from './template-Dlt5S54z.js';
|
|
5
6
|
|
|
6
7
|
/**
|
|
7
8
|
* The database seam (12-15) — the exact, narrow slice of a Prisma-shaped
|
|
@@ -306,11 +307,19 @@ type NotificationsDbProvider = () => Promise<NotificationsDb>;
|
|
|
306
307
|
* Returning `null` for a user id means "no such recipient", which `notify`
|
|
307
308
|
* treats as a caller bug and throws on — a notification addressed to nobody is
|
|
308
309
|
* never silently dropped.
|
|
310
|
+
*
|
|
311
|
+
* `locale` is the recipient's own language, and it is here rather than on the
|
|
312
|
+
* event because it is a fact about the PERSON, not about what happened
|
|
313
|
+
* The host owns it for the same reason it owns the address: this
|
|
314
|
+
* package has no user table to read it from. Absent — the field omitted, or
|
|
315
|
+
* `null` for a host that stores no language yet — means "nobody said", and
|
|
316
|
+
* every generator answers with its default exactly as before.
|
|
309
317
|
*/
|
|
310
318
|
interface NotificationContactDirectory {
|
|
311
319
|
getContact(userId: string): Promise<{
|
|
312
320
|
email: string | null;
|
|
313
321
|
phone: string | null;
|
|
322
|
+
locale?: string | null;
|
|
314
323
|
} | null>;
|
|
315
324
|
}
|
|
316
325
|
|
|
@@ -399,6 +408,31 @@ interface EmailDriverDeclaration extends DriverDeclarationBase {
|
|
|
399
408
|
* `from` address.
|
|
400
409
|
*/
|
|
401
410
|
linkLabel: string;
|
|
411
|
+
/**
|
|
412
|
+
* Render through `../../email` — the ONE layout — instead of the three bare
|
|
413
|
+
* `<p>` tags below.
|
|
414
|
+
*
|
|
415
|
+
* OPTIONAL, and its absence is the pre-layout behaviour verbatim. That is
|
|
416
|
+
* deliberate rather than timid: `brand` and `chrome` are REQUIRED with no
|
|
417
|
+
* default anywhere in the layout (a package that defaulted them would sign
|
|
418
|
+
* another company's mail, in a language nobody chose), so a required field
|
|
419
|
+
* here would break every host that already declares EMAIL — at runtime, on
|
|
420
|
+
* the first send, which is the worst place to find out.
|
|
421
|
+
*
|
|
422
|
+
* So the seam is opt-in and the ad-hoc path is what remains for a host that
|
|
423
|
+
* has not taken it. `layout.theme` may still be omitted; `../../email/theme`
|
|
424
|
+
* argues why that one asymmetry is allowed.
|
|
425
|
+
*/
|
|
426
|
+
layout?: {
|
|
427
|
+
/** The product name in the header and the footer. */
|
|
428
|
+
brand: string;
|
|
429
|
+
/** The layout's own words. `@12-apps/notifications/email/locales` ships packs. */
|
|
430
|
+
chrome: EmailChromeCopy;
|
|
431
|
+
/** The recipient's language, for the document's `lang` attribute. */
|
|
432
|
+
locale: string;
|
|
433
|
+
/** Defaults to the layout's neutral palette. */
|
|
434
|
+
theme?: EmailTheme;
|
|
435
|
+
};
|
|
402
436
|
logger?: NotificationLogger;
|
|
403
437
|
}
|
|
404
438
|
/** The built-in email vendors. A host adds one by extending this table. */
|
|
@@ -406,6 +440,11 @@ declare const EMAIL_DRIVERS: Record<string, (declaration: EmailDriverDeclaration
|
|
|
406
440
|
/**
|
|
407
441
|
* Agnostic content → subject/text/html. The link becomes a trailing CTA, only
|
|
408
442
|
* when an app base URL is configured.
|
|
443
|
+
*
|
|
444
|
+
* Two renderings, and which one runs is the host's choice: with `layout`
|
|
445
|
+
* declared this is the shared document (`../../email`), and without it the
|
|
446
|
+
* three bare `<p>` tags that predate it. See `EmailDriverDeclaration.layout`
|
|
447
|
+
* for why the new path could not simply replace the old one.
|
|
409
448
|
*/
|
|
410
449
|
declare function formatEmail(content: NotificationContent, declaration: EmailDriverDeclaration): EmailMessage;
|
|
411
450
|
declare function emailTransport(declaration: EmailDriverDeclaration, extraDrivers?: Record<string, (d: EmailDriverDeclaration) => EmailDriver>): NotificationTransport<EmailMessage>;
|