@voyantjs/notifications-react 0.106.0 → 0.108.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (93) hide show
  1. package/README.md +30 -0
  2. package/dist/admin/index.d.ts +80 -0
  3. package/dist/admin/index.d.ts.map +1 -0
  4. package/dist/admin/index.js +102 -0
  5. package/dist/admin/notification-deliveries-host.d.ts +7 -0
  6. package/dist/admin/notification-deliveries-host.d.ts.map +1 -0
  7. package/dist/admin/notification-deliveries-host.js +92 -0
  8. package/dist/admin/notification-delivery-detail-dialog.d.ts +8 -0
  9. package/dist/admin/notification-delivery-detail-dialog.d.ts.map +1 -0
  10. package/dist/admin/notification-delivery-detail-dialog.js +30 -0
  11. package/dist/admin/notification-reminder-rule-detail-host.d.ts +12 -0
  12. package/dist/admin/notification-reminder-rule-detail-host.d.ts.map +1 -0
  13. package/dist/admin/notification-reminder-rule-detail-host.js +23 -0
  14. package/dist/admin/notification-reminder-rule-dialog.d.ts +10 -0
  15. package/dist/admin/notification-reminder-rule-dialog.d.ts.map +1 -0
  16. package/dist/admin/notification-reminder-rule-dialog.js +122 -0
  17. package/dist/admin/notification-reminder-rules-host.d.ts +8 -0
  18. package/dist/admin/notification-reminder-rules-host.d.ts.map +1 -0
  19. package/dist/admin/notification-reminder-rules-host.js +57 -0
  20. package/dist/admin/notification-reminder-runs-host.d.ts +7 -0
  21. package/dist/admin/notification-reminder-runs-host.d.ts.map +1 -0
  22. package/dist/admin/notification-reminder-runs-host.js +28 -0
  23. package/dist/admin/notification-settings-host.d.ts +7 -0
  24. package/dist/admin/notification-settings-host.d.ts.map +1 -0
  25. package/dist/admin/notification-settings-host.js +11 -0
  26. package/dist/admin/notification-template-authoring-help.d.ts +25 -0
  27. package/dist/admin/notification-template-authoring-help.d.ts.map +1 -0
  28. package/dist/admin/notification-template-authoring-help.js +8 -0
  29. package/dist/admin/notification-template-detail-host.d.ts +11 -0
  30. package/dist/admin/notification-template-detail-host.d.ts.map +1 -0
  31. package/dist/admin/notification-template-detail-host.js +159 -0
  32. package/dist/admin/notification-template-dialog.d.ts +10 -0
  33. package/dist/admin/notification-template-dialog.d.ts.map +1 -0
  34. package/dist/admin/notification-template-dialog.js +364 -0
  35. package/dist/admin/notification-templates-host.d.ts +9 -0
  36. package/dist/admin/notification-templates-host.d.ts.map +1 -0
  37. package/dist/admin/notification-templates-host.js +52 -0
  38. package/dist/admin/notifications-admin-shared.d.ts +14 -0
  39. package/dist/admin/notifications-admin-shared.d.ts.map +1 -0
  40. package/dist/admin/notifications-admin-shared.js +17 -0
  41. package/dist/admin/pages/notification-reminder-rule-detail-page.d.ts +8 -0
  42. package/dist/admin/pages/notification-reminder-rule-detail-page.d.ts.map +1 -0
  43. package/dist/admin/pages/notification-reminder-rule-detail-page.js +10 -0
  44. package/dist/admin/pages/notification-template-detail-page.d.ts +7 -0
  45. package/dist/admin/pages/notification-template-detail-page.d.ts.map +1 -0
  46. package/dist/admin/pages/notification-template-detail-page.js +9 -0
  47. package/dist/admin/reminders-preview-host.d.ts +7 -0
  48. package/dist/admin/reminders-preview-host.d.ts.map +1 -0
  49. package/dist/admin/reminders-preview-host.js +13 -0
  50. package/dist/components/notification-settings-form.d.ts +2 -0
  51. package/dist/components/notification-settings-form.d.ts.map +1 -0
  52. package/dist/components/notification-settings-form.js +66 -0
  53. package/dist/components/reminders-preview-list.d.ts +6 -0
  54. package/dist/components/reminders-preview-list.d.ts.map +1 -0
  55. package/dist/components/reminders-preview-list.js +19 -0
  56. package/dist/components/stage-channel-editor-dialog.d.ts +11 -0
  57. package/dist/components/stage-channel-editor-dialog.d.ts.map +1 -0
  58. package/dist/components/stage-channel-editor-dialog.js +77 -0
  59. package/dist/components/stage-channel-list.d.ts +6 -0
  60. package/dist/components/stage-channel-list.d.ts.map +1 -0
  61. package/dist/components/stage-channel-list.js +20 -0
  62. package/dist/components/stage-editor-dialog.d.ts +10 -0
  63. package/dist/components/stage-editor-dialog.d.ts.map +1 -0
  64. package/dist/components/stage-editor-dialog.js +104 -0
  65. package/dist/components/stage-list.d.ts +5 -0
  66. package/dist/components/stage-list.d.ts.map +1 -0
  67. package/dist/components/stage-list.js +34 -0
  68. package/dist/components/template-picker.d.ts +19 -0
  69. package/dist/components/template-picker.d.ts.map +1 -0
  70. package/dist/components/template-picker.js +26 -0
  71. package/dist/components/timezone-combobox.d.ts +9 -0
  72. package/dist/components/timezone-combobox.d.ts.map +1 -0
  73. package/dist/components/timezone-combobox.js +67 -0
  74. package/dist/i18n/en.d.ts +3 -0
  75. package/dist/i18n/en.d.ts.map +1 -0
  76. package/dist/i18n/en.js +385 -0
  77. package/dist/i18n/index.d.ts +5 -0
  78. package/dist/i18n/index.d.ts.map +1 -0
  79. package/dist/i18n/index.js +3 -0
  80. package/dist/i18n/messages.d.ts +386 -0
  81. package/dist/i18n/messages.d.ts.map +1 -0
  82. package/dist/i18n/messages.js +1 -0
  83. package/dist/i18n/provider.d.ts +26 -0
  84. package/dist/i18n/provider.d.ts.map +1 -0
  85. package/dist/i18n/provider.js +44 -0
  86. package/dist/i18n/ro.d.ts +3 -0
  87. package/dist/i18n/ro.d.ts.map +1 -0
  88. package/dist/i18n/ro.js +385 -0
  89. package/dist/ui.d.ts +9 -0
  90. package/dist/ui.d.ts.map +1 -0
  91. package/dist/ui.js +8 -0
  92. package/package.json +71 -10
  93. package/src/styles.css +2 -0
@@ -0,0 +1,364 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
3
+ import { formatMessage } from "@voyantjs/i18n";
4
+ import { Button, Checkbox, Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle, Input, Label, ScrollArea, Switch, Tabs, TabsContent, TabsList, TabsTrigger, Textarea, } from "@voyantjs/ui/components";
5
+ import { RichTextEditor } from "@voyantjs/ui/components/rich-text-editor";
6
+ import { insertPlainText, insertVariableToken, } from "@voyantjs/ui/components/rich-text-variable-extension";
7
+ import { zodResolver } from "@voyantjs/ui/lib/zod-resolver";
8
+ import { Loader2 } from "lucide-react";
9
+ import { useEffect, useMemo, useRef, useState } from "react";
10
+ import { useForm } from "react-hook-form";
11
+ import { toast } from "sonner";
12
+ import { z } from "zod/v4";
13
+ import { useNotificationsUiMessagesOrDefault } from "../i18n/index.js";
14
+ import { useNotificationTemplateAuthoring, useNotificationTemplateMutation, useNotificationTemplateTools, } from "../index.js";
15
+ import { NotificationTemplateAuthoringHelp } from "./notification-template-authoring-help.js";
16
+ const CHANNEL_VALUES = ["email", "sms"];
17
+ const STATUS_VALUES = ["draft", "active", "archived"];
18
+ const ATTACHMENT_VALUES = ["contract", "invoice", "brochure"];
19
+ const channelItemLabel = (t, value) => (value === "email" ? t.channelEmail : t.channelSms);
20
+ const statusItemLabel = (t, value) => (value === "draft" ? t.statusDraft : value === "active" ? t.statusActive : t.statusArchived);
21
+ const attachmentItemLabel = (t, value) => value === "contract"
22
+ ? t.attachmentContract
23
+ : value === "invoice"
24
+ ? t.attachmentInvoice
25
+ : t.attachmentBrochure;
26
+ const nativeSelectClassName = "h-9 w-full rounded-md border border-input bg-transparent px-3 py-2 text-sm shadow-xs outline-none transition-[color,box-shadow] focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50 dark:bg-input/30";
27
+ const templateAttachmentSchema = z.enum(["contract", "invoice", "brochure"]);
28
+ const templateFormSchema = z.object({
29
+ name: z.string().min(1, "Name is required"),
30
+ slug: z
31
+ .string()
32
+ .min(1, "Slug is required")
33
+ .regex(/^[a-z0-9]+(?:-[a-z0-9]+)*$/, "Must be kebab-case"),
34
+ channel: z.enum(["email", "sms"]),
35
+ status: z.enum(["draft", "active", "archived"]).default("draft"),
36
+ subjectTemplate: z.string().optional(),
37
+ htmlTemplate: z.string().optional(),
38
+ textTemplate: z.string().optional(),
39
+ fromAddress: z.string().optional(),
40
+ attachments: z.array(templateAttachmentSchema).default([]),
41
+ active: z.boolean(),
42
+ });
43
+ function parsePath(path) {
44
+ return path
45
+ .replace(/\[(\d+)\]/g, ".$1")
46
+ .split(".")
47
+ .filter(Boolean);
48
+ }
49
+ function setDeepValue(target, path, value) {
50
+ const segments = parsePath(path);
51
+ let current = target;
52
+ for (let index = 0; index < segments.length; index += 1) {
53
+ const segment = segments[index];
54
+ const isLast = index === segments.length - 1;
55
+ const nextSegment = segments[index + 1];
56
+ const nextIsIndex = nextSegment ? /^\d+$/.test(nextSegment) : false;
57
+ if (Array.isArray(current)) {
58
+ const arrayIndex = Number(segment);
59
+ if (Number.isNaN(arrayIndex))
60
+ return;
61
+ if (isLast) {
62
+ current[arrayIndex] = value;
63
+ return;
64
+ }
65
+ if (current[arrayIndex] == null) {
66
+ current[arrayIndex] = nextIsIndex ? [] : {};
67
+ }
68
+ current = current[arrayIndex];
69
+ continue;
70
+ }
71
+ if (typeof current !== "object" || current == null)
72
+ return;
73
+ const record = current;
74
+ if (isLast) {
75
+ record[segment] = value;
76
+ return;
77
+ }
78
+ if (record[segment] == null) {
79
+ record[segment] = nextIsIndex ? [] : {};
80
+ }
81
+ current = record[segment];
82
+ }
83
+ }
84
+ function buildSamplePayload(variableGroups) {
85
+ const sample = {};
86
+ for (const group of variableGroups) {
87
+ for (const variable of group.variables) {
88
+ setDeepValue(sample, variable.key, variable.example);
89
+ }
90
+ }
91
+ return sample;
92
+ }
93
+ function appendTemplateValue(current, addition) {
94
+ if (!current?.trim())
95
+ return addition;
96
+ return `${current}${current.endsWith("\n") ? "" : "\n"}${addition}`;
97
+ }
98
+ function variableReference(key) {
99
+ return `{{ ${key} }}`;
100
+ }
101
+ function getMetadataRecord(value) {
102
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
103
+ return null;
104
+ }
105
+ return value;
106
+ }
107
+ function readTemplateAttachments(metadata) {
108
+ const record = getMetadataRecord(metadata);
109
+ const value = record?.attachments;
110
+ if (!Array.isArray(value)) {
111
+ return [];
112
+ }
113
+ const allowed = new Set(ATTACHMENT_VALUES);
114
+ return ATTACHMENT_VALUES.filter((attachment) => allowed.has(attachment) && value.includes(attachment));
115
+ }
116
+ function buildTemplateMetadata(metadata, attachments) {
117
+ const current = getMetadataRecord(metadata);
118
+ const next = current ? { ...current } : {};
119
+ if (attachments.length > 0) {
120
+ next.attachments = [...attachments];
121
+ }
122
+ else {
123
+ delete next.attachments;
124
+ }
125
+ return Object.keys(next).length > 0 ? next : null;
126
+ }
127
+ export function NotificationTemplateDialog(props) {
128
+ if (!props.open) {
129
+ return null;
130
+ }
131
+ return _jsx(NotificationTemplateDialogInner, { ...props });
132
+ }
133
+ function NotificationTemplateDialogInner({ open, onOpenChange, template, onSuccess, }) {
134
+ const isEditing = Boolean(template);
135
+ const messages = useNotificationsUiMessagesOrDefault();
136
+ const t = messages.admin.templateDialog;
137
+ const common = messages.admin.common;
138
+ const { create, update } = useNotificationTemplateMutation();
139
+ const { preview, testSend } = useNotificationTemplateTools();
140
+ const previewResetRef = useRef(preview.reset);
141
+ const testSendResetRef = useRef(testSend.reset);
142
+ const { variableCatalog, liquidSnippets } = useNotificationTemplateAuthoring();
143
+ const [editorInstance, setEditorInstance] = useState(null);
144
+ const [insertionTarget, setInsertionTarget] = useState("body");
145
+ const [previewDataInput, setPreviewDataInput] = useState("{}");
146
+ const [testRecipient, setTestRecipient] = useState("");
147
+ const variableGroups = useMemo(() => variableCatalog.map((group) => ({
148
+ ...group,
149
+ variables: group.variables.map((variable) => ({
150
+ ...variable,
151
+ example: String(variable.example),
152
+ })),
153
+ })), [variableCatalog]);
154
+ const defaultPreviewData = useMemo(() => JSON.stringify(buildSamplePayload(variableGroups), null, 2), [variableGroups]);
155
+ const form = useForm({
156
+ resolver: zodResolver(templateFormSchema),
157
+ defaultValues: {
158
+ name: "",
159
+ slug: "",
160
+ channel: "email",
161
+ status: "draft",
162
+ subjectTemplate: "",
163
+ htmlTemplate: "",
164
+ textTemplate: "",
165
+ fromAddress: "",
166
+ attachments: [],
167
+ active: true,
168
+ },
169
+ });
170
+ const channel = form.watch("channel");
171
+ const attachments = form.watch("attachments") ?? [];
172
+ previewResetRef.current = preview.reset;
173
+ testSendResetRef.current = testSend.reset;
174
+ useEffect(() => {
175
+ if (open && template) {
176
+ form.reset({
177
+ name: template.name,
178
+ slug: template.slug,
179
+ channel: template.channel,
180
+ status: template.status,
181
+ subjectTemplate: template.subjectTemplate ?? "",
182
+ htmlTemplate: template.htmlTemplate ?? "",
183
+ textTemplate: template.textTemplate ?? "",
184
+ fromAddress: template.fromAddress ?? "",
185
+ attachments: template.channel === "email" ? readTemplateAttachments(template.metadata) : [],
186
+ active: template.status === "active",
187
+ });
188
+ return;
189
+ }
190
+ if (open) {
191
+ form.reset();
192
+ }
193
+ }, [open, template, form]);
194
+ useEffect(() => {
195
+ if (!open)
196
+ return;
197
+ setInsertionTarget((current) => {
198
+ const next = channel === "sms" ? "text" : current === "text" ? "body" : current;
199
+ return next === current ? current : next;
200
+ });
201
+ }, [channel, open]);
202
+ useEffect(() => {
203
+ if (!open || channel === "email" || (form.getValues("attachments") ?? []).length === 0)
204
+ return;
205
+ form.setValue("attachments", [], {
206
+ shouldDirty: true,
207
+ shouldTouch: true,
208
+ shouldValidate: true,
209
+ });
210
+ }, [channel, form, open]);
211
+ useEffect(() => {
212
+ if (!open)
213
+ return;
214
+ setPreviewDataInput(defaultPreviewData);
215
+ setTestRecipient("");
216
+ previewResetRef.current();
217
+ testSendResetRef.current();
218
+ }, [defaultPreviewData, open]);
219
+ const onSubmit = async (values) => {
220
+ const payload = {
221
+ name: values.name,
222
+ slug: values.slug,
223
+ channel: values.channel,
224
+ provider: null,
225
+ status: values.active ? (values.status === "archived" ? "active" : values.status) : "draft",
226
+ subjectTemplate: values.channel === "email" ? values.subjectTemplate || null : null,
227
+ htmlTemplate: values.channel === "email" ? values.htmlTemplate || null : null,
228
+ textTemplate: values.channel === "sms" ? values.textTemplate || null : null,
229
+ fromAddress: values.channel === "email" ? values.fromAddress || null : null,
230
+ isSystem: template?.isSystem ?? false,
231
+ metadata: values.channel === "email"
232
+ ? buildTemplateMetadata(template?.metadata, values.attachments)
233
+ : buildTemplateMetadata(template?.metadata, []),
234
+ };
235
+ if (isEditing && template) {
236
+ await update.mutateAsync({ id: template.id, input: payload });
237
+ }
238
+ else {
239
+ await create.mutateAsync(payload);
240
+ }
241
+ onSuccess();
242
+ };
243
+ const isPending = create.isPending || update.isPending;
244
+ const parsePreviewData = () => {
245
+ try {
246
+ const parsed = previewDataInput.trim() ? JSON.parse(previewDataInput) : {};
247
+ if (typeof parsed !== "object" || parsed == null || Array.isArray(parsed)) {
248
+ throw new Error(common.previewDataNotObject);
249
+ }
250
+ return parsed;
251
+ }
252
+ catch (error) {
253
+ throw new Error(error instanceof Error ? error.message : common.previewInvalidJson);
254
+ }
255
+ };
256
+ const insertIntoTarget = (content, kind) => {
257
+ if (insertionTarget === "body" && channel === "email" && editorInstance) {
258
+ if (kind === "variable") {
259
+ insertVariableToken(editorInstance, content);
260
+ }
261
+ else {
262
+ insertPlainText(editorInstance, content);
263
+ }
264
+ return;
265
+ }
266
+ const fieldName = insertionTarget === "subject" ? "subjectTemplate" : "textTemplate";
267
+ const current = form.getValues(fieldName) ?? "";
268
+ const nextValue = kind === "variable"
269
+ ? // i18n-literal-ok single-space joiner between Liquid tokens, not user-facing copy.
270
+ `${current}${current ? " " : ""}${variableReference(content)}`
271
+ : appendTemplateValue(current, content);
272
+ form.setValue(fieldName, nextValue, {
273
+ shouldDirty: true,
274
+ shouldTouch: true,
275
+ shouldValidate: true,
276
+ });
277
+ };
278
+ const handlePreview = async () => {
279
+ try {
280
+ const data = parsePreviewData();
281
+ await preview.mutateAsync({
282
+ channel,
283
+ provider: null,
284
+ fromAddress: channel === "email" ? form.getValues("fromAddress") || null : null,
285
+ subjectTemplate: channel === "email" ? form.getValues("subjectTemplate") || null : null,
286
+ htmlTemplate: channel === "email" ? form.getValues("htmlTemplate") || null : null,
287
+ textTemplate: channel === "sms" ? form.getValues("textTemplate") || null : null,
288
+ data,
289
+ });
290
+ }
291
+ catch (error) {
292
+ toast.error(error instanceof Error ? error.message : common.previewFailed);
293
+ }
294
+ };
295
+ const handleTestSend = async () => {
296
+ if (!testRecipient.trim()) {
297
+ toast.error(channel === "email" ? t.recipientEmailRequired : t.recipientPhoneRequired);
298
+ return;
299
+ }
300
+ try {
301
+ const data = parsePreviewData();
302
+ await testSend.mutateAsync({
303
+ to: testRecipient.trim(),
304
+ channel,
305
+ provider: null,
306
+ from: channel === "email" ? form.getValues("fromAddress") || null : null,
307
+ subject: channel === "email" ? form.getValues("subjectTemplate") || null : null,
308
+ html: channel === "email" ? form.getValues("htmlTemplate") || null : null,
309
+ text: channel === "sms" ? form.getValues("textTemplate") || null : null,
310
+ data,
311
+ targetType: "other",
312
+ });
313
+ toast.success(channel === "email" ? t.testQueuedEmail : t.testQueuedSms);
314
+ }
315
+ catch (error) {
316
+ toast.error(error instanceof Error ? error.message : t.testSendFailed);
317
+ }
318
+ };
319
+ const setAttachmentSelected = (attachment, checked) => {
320
+ const current = form.getValues("attachments") ?? [];
321
+ const next = checked
322
+ ? [...current, attachment].filter((value, index, values) => values.indexOf(value) === index)
323
+ : current.filter((value) => value !== attachment);
324
+ form.setValue("attachments", next, {
325
+ shouldDirty: true,
326
+ shouldTouch: true,
327
+ shouldValidate: true,
328
+ });
329
+ };
330
+ return (_jsx(Dialog, { open: open, onOpenChange: onOpenChange, children: _jsx(DialogContent, { size: "xl", className: "h-[calc(100vh-2rem)]", children: _jsxs("form", { onSubmit: form.handleSubmit(onSubmit), className: "grid min-h-0 flex-1 grid-rows-[auto_minmax(0,1fr)_auto] overflow-hidden", children: [_jsx(DialogHeader, { children: _jsx(DialogTitle, { children: isEditing ? t.editTitle : t.createTitle }) }), _jsx(ScrollArea, { className: "min-h-0 flex-1", children: _jsxs("div", { className: "grid gap-4 py-4 pr-4", children: [_jsxs("div", { className: "grid grid-cols-2 gap-4", children: [_jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Label, { children: t.nameLabel }), _jsx(Input, { ...form.register("name"), placeholder: t.namePlaceholder }), form.formState.errors.name ? (_jsx("p", { className: "text-xs text-destructive", children: form.formState.errors.name.message })) : null] }), _jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Label, { children: t.slugLabel }), _jsx(Input, { ...form.register("slug"), placeholder: t.slugPlaceholder }), form.formState.errors.slug ? (_jsx("p", { className: "text-xs text-destructive", children: form.formState.errors.slug.message })) : null] })] }), _jsxs("div", { className: "grid grid-cols-2 gap-4", children: [_jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Label, { children: t.channelLabel }), _jsx("select", { className: nativeSelectClassName, value: form.watch("channel"), onChange: (event) => {
331
+ const nextChannel = event.target.value;
332
+ if (form.getValues("channel") === nextChannel)
333
+ return;
334
+ form.setValue("channel", nextChannel, {
335
+ shouldDirty: true,
336
+ shouldTouch: true,
337
+ shouldValidate: true,
338
+ });
339
+ }, children: CHANNEL_VALUES.map((value) => (_jsx("option", { value: value, children: channelItemLabel(common, value) }, value))) })] }), _jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Label, { children: t.statusLabel }), _jsx("select", { className: nativeSelectClassName, value: form.watch("status"), onChange: (event) => {
340
+ const nextStatus = event.target.value;
341
+ if (form.getValues("status") === nextStatus)
342
+ return;
343
+ form.setValue("status", nextStatus, {
344
+ shouldDirty: true,
345
+ shouldTouch: true,
346
+ shouldValidate: true,
347
+ });
348
+ }, children: STATUS_VALUES.map((value) => (_jsx("option", { value: value, children: statusItemLabel(common, value) }, value))) })] })] }), channel === "email" ? (_jsxs(_Fragment, { children: [_jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Label, { children: t.attachmentsLabel }), _jsx("div", { className: "flex flex-wrap gap-3", children: ATTACHMENT_VALUES.map((value) => (_jsxs("div", { className: "flex h-9 items-center gap-2 rounded-md border px-3 text-sm", children: [_jsx(Checkbox, { id: `notification-template-attachment-${value}`, checked: attachments.includes(value), onCheckedChange: (checked) => setAttachmentSelected(value, checked === true) }), _jsx(Label, { htmlFor: `notification-template-attachment-${value}`, className: "cursor-pointer text-sm font-normal", children: attachmentItemLabel(t, value) })] }, value))) })] }), _jsxs("div", { className: "grid grid-cols-2 gap-4", children: [_jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Label, { children: t.fromAddressLabel }), _jsx(Input, { ...form.register("fromAddress"), placeholder: t.fromAddressPlaceholder })] }), _jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Label, { children: t.subjectLabel }), _jsx(Input, { ...form.register("subjectTemplate"), placeholder: t.subjectPlaceholder })] })] }), _jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Label, { children: t.htmlBodyLabel }), _jsx(RichTextEditor, { value: form.watch("htmlTemplate") ?? "", onChange: (value) => form.setValue("htmlTemplate", value, {
349
+ shouldDirty: true,
350
+ shouldTouch: true,
351
+ shouldValidate: true,
352
+ }), placeholder: t.htmlBodyPlaceholder, enableVariables: true, onEditorReady: setEditorInstance })] })] })) : null, channel === "sms" ? (_jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Label, { children: t.smsBodyLabel }), _jsx(Textarea, { ...form.register("textTemplate"), placeholder: t.smsBodyPlaceholder, rows: 6, className: "font-mono text-xs" })] })) : null, _jsxs(Tabs, { defaultValue: "authoring", children: [_jsxs(TabsList, { className: "w-full", children: [_jsx(TabsTrigger, { value: "authoring", children: t.tabAuthoring }), _jsx(TabsTrigger, { value: "preview", children: t.tabPreview })] }), _jsxs(TabsContent, { value: "authoring", className: "mt-4 space-y-4", children: [_jsxs("div", { className: "grid grid-cols-1 gap-3 sm:grid-cols-[180px_1fr] sm:items-center", children: [_jsxs("div", { className: "flex flex-col gap-1.5", children: [_jsx(Label, { children: t.insertIntoLabel }), _jsxs("select", { className: nativeSelectClassName, value: insertionTarget, onChange: (event) => {
353
+ const nextTarget = event.target.value;
354
+ if (nextTarget === insertionTarget)
355
+ return;
356
+ setInsertionTarget(nextTarget);
357
+ }, children: [channel === "email" ? (_jsx("option", { value: "subject", children: t.insertTargetSubject })) : null, channel === "email" ? (_jsx("option", { value: "body", children: t.insertTargetHtmlBody })) : null, channel === "sms" ? (_jsx("option", { value: "text", children: t.insertTargetSmsBody })) : null] })] }), _jsx("p", { className: "text-xs text-muted-foreground", children: t.insertHint })] }), _jsx(NotificationTemplateAuthoringHelp, { variableGroups: variableGroups, snippets: liquidSnippets, onInsertVariable: (variable) => insertIntoTarget(variable.key, "variable"), onInsertSnippet: (snippet) => insertIntoTarget(snippet.code, "snippet") })] }), _jsx(TabsContent, { value: "preview", className: "mt-4 space-y-4", children: _jsxs("div", { className: "grid gap-4 lg:grid-cols-[minmax(0,1fr)_320px]", children: [_jsxs("div", { className: "space-y-4", children: [_jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Label, { children: t.previewDataLabel }), _jsx(Textarea, { value: previewDataInput, onChange: (event) => setPreviewDataInput(event.target.value), rows: 14, className: "font-mono text-xs", placeholder: t.previewDataPlaceholder }), _jsx("p", { className: "text-xs text-muted-foreground", children: t.previewDataHint })] }), _jsx("div", { className: "flex gap-2", children: _jsxs(Button, { type: "button", variant: "outline", onClick: handlePreview, disabled: preview.isPending, children: [preview.isPending ? (_jsx(Loader2, { className: "mr-2 h-4 w-4 animate-spin" })) : null, t.refreshPreview] }) }), _jsxs("div", { className: "space-y-3 rounded-md border p-4", children: [_jsx("div", { className: "text-sm font-medium", children: t.renderedPreviewTitle }), channel === "email" ? (_jsxs("div", { className: "space-y-3", children: [_jsxs("div", { className: "space-y-1", children: [_jsx("div", { className: "text-xs uppercase tracking-wide text-muted-foreground", children: t.renderedSubjectLabel }), _jsx("div", { className: "rounded-md border bg-muted/20 px-3 py-2 text-sm", children: preview.data?.subject || t.noSubjectRendered })] }), _jsxs("div", { className: "space-y-1", children: [_jsx("div", { className: "text-xs uppercase tracking-wide text-muted-foreground", children: t.renderedHtmlLabel }), _jsx("div", { className: "rounded-md border bg-background", children: preview.data?.html ? (_jsx("div", { className: "prose prose-sm max-w-none px-3 py-3 dark:prose-invert",
358
+ // biome-ignore lint/security/noDangerouslySetInnerHtml: Preview HTML is generated server-side for template preview.
359
+ dangerouslySetInnerHTML: { __html: preview.data.html } })) : (_jsx("div", { className: "px-3 py-3 text-sm text-muted-foreground", children: t.noHtmlRendered })) })] })] })) : (_jsxs("div", { className: "space-y-1", children: [_jsx("div", { className: "text-xs uppercase tracking-wide text-muted-foreground", children: t.renderedSmsLabel }), _jsx("pre", { className: "whitespace-pre-wrap rounded-md border bg-muted/20 px-3 py-3 text-xs", children: preview.data?.text || t.noSmsRendered })] }))] })] }), _jsxs("div", { className: "space-y-4 rounded-md border p-4", children: [_jsxs("div", { className: "space-y-1", children: [_jsx("div", { className: "text-sm font-medium", children: t.testSendTitle }), _jsx("p", { className: "text-xs text-muted-foreground", children: t.testSendDescription })] }), _jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Label, { children: channel === "email" ? t.recipientEmailLabel : t.recipientPhoneLabel }), _jsx(Input, { value: testRecipient, onChange: (event) => setTestRecipient(event.target.value), placeholder: channel === "email"
360
+ ? t.recipientEmailPlaceholder
361
+ : t.recipientPhonePlaceholder })] }), _jsxs("div", { className: "space-y-1 text-xs text-muted-foreground", children: [_jsx("div", { children: t.providerAutoNote }), channel === "email" ? (_jsx("div", { children: formatMessage(t.fromNote, {
362
+ sender: form.watch("fromAddress") || common.defaultSender,
363
+ }) })) : null] }), _jsxs(Button, { type: "button", className: "w-full", onClick: handleTestSend, disabled: testSend.isPending, children: [testSend.isPending ? (_jsx(Loader2, { className: "mr-2 h-4 w-4 animate-spin" })) : null, channel === "email" ? t.sendTestEmail : t.sendTestSms] }), testSend.data ? (_jsxs("div", { className: "rounded-md border border-emerald-500/30 bg-emerald-500/10 px-3 py-2 text-xs text-emerald-200", children: ["Delivery queued with status ", _jsx("strong", { children: testSend.data.status }), testSend.data.provider ? ` via ${testSend.data.provider}` : "", "."] })) : null] })] }) })] }), _jsxs("div", { className: "flex items-center gap-3", children: [_jsx(Switch, { checked: form.watch("active"), onCheckedChange: (checked) => form.setValue("active", checked) }), _jsx(Label, { className: "cursor-pointer", children: t.markActiveLabel })] })] }) }), _jsxs(DialogFooter, { className: "mt-0", children: [_jsx(Button, { type: "button", variant: "ghost", onClick: () => onOpenChange(false), children: messages.common.cancel }), _jsxs(Button, { type: "submit", disabled: isPending, children: [isPending ? _jsx(Loader2, { className: "mr-2 h-4 w-4 animate-spin" }) : null, isEditing ? common.saveChanges : t.createTemplate] })] })] }) }) }));
364
+ }
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Packaged admin host for the notification templates list page
3
+ * (packaged-admin RFC Phase 3). Zero-prop: list/filter state stays
4
+ * component-local, row clicks resolve through the
5
+ * `notificationTemplate.detail` semantic destination, and the create/edit
6
+ * dialog stays lazily loaded inside the package.
7
+ */
8
+ export declare function NotificationTemplatesHost(): import("react/jsx-runtime").JSX.Element;
9
+ //# sourceMappingURL=notification-templates-host.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"notification-templates-host.d.ts","sourceRoot":"","sources":["../../src/admin/notification-templates-host.tsx"],"names":[],"mappings":"AAgCA;;;;;;GAMG;AACH,wBAAgB,yBAAyB,4CAuJxC"}
@@ -0,0 +1,52 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { useAdminHref, useAdminNavigate } from "@voyantjs/admin";
4
+ import { Badge, Button, Input, Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@voyantjs/ui/components";
5
+ import { Loader2, Pencil, Plus, Search } from "lucide-react";
6
+ import { lazy, Suspense, useState } from "react";
7
+ import { useNotificationsUiMessagesOrDefault } from "../i18n/index.js";
8
+ import { useNotificationTemplates, } from "../index.js";
9
+ import { DestinationLink } from "./notifications-admin-shared.js";
10
+ // Lazy-load: the template dialog pulls the rich-text editor (tiptap +
11
+ // prosemirror). Keeping it out of the list-page chunk means those modules
12
+ // only download when the user opens the create/edit dialog.
13
+ const NotificationTemplateDialog = lazy(() => import("./notification-template-dialog.js").then((m) => ({
14
+ default: m.NotificationTemplateDialog,
15
+ })));
16
+ /**
17
+ * Packaged admin host for the notification templates list page
18
+ * (packaged-admin RFC Phase 3). Zero-prop: list/filter state stays
19
+ * component-local, row clicks resolve through the
20
+ * `notificationTemplate.detail` semantic destination, and the create/edit
21
+ * dialog stays lazily loaded inside the package.
22
+ */
23
+ export function NotificationTemplatesHost() {
24
+ const messages = useNotificationsUiMessagesOrDefault();
25
+ const t = messages.admin.templatesPage;
26
+ const common = messages.admin.common;
27
+ const [search, setSearch] = useState("");
28
+ const [channel, setChannel] = useState("all");
29
+ const [status, setStatus] = useState("all");
30
+ const [dialogOpen, setDialogOpen] = useState(false);
31
+ const [editing, setEditing] = useState();
32
+ const resolveHref = useAdminHref();
33
+ const navigateTo = useAdminNavigate();
34
+ const { data, isPending, refetch } = useNotificationTemplates({
35
+ search,
36
+ channel: channel === "all" ? undefined : channel,
37
+ status: status === "all" ? undefined : status,
38
+ });
39
+ return (_jsxs("div", { className: "flex flex-col gap-6 p-6", children: [_jsxs("div", { className: "flex items-center justify-between", children: [_jsxs("div", { children: [_jsx("h1", { className: "text-2xl font-bold tracking-tight", children: t.title }), _jsx("p", { className: "text-sm text-muted-foreground", children: t.description })] }), _jsxs(Button, { onClick: () => {
40
+ setEditing(undefined);
41
+ setDialogOpen(true);
42
+ }, children: [_jsx(Plus, { className: "mr-2 h-4 w-4" }), t.newTemplate] })] }), _jsxs("div", { className: "flex items-center gap-3", children: [_jsxs("div", { className: "relative max-w-sm flex-1", children: [_jsx(Search, { className: "absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" }), _jsx(Input, { placeholder: t.searchPlaceholder, value: search, onChange: (event) => setSearch(event.target.value), className: "pl-9" })] }), _jsxs(Select, { value: channel, onValueChange: (value) => setChannel(value ?? "all"), children: [_jsx(SelectTrigger, { className: "w-[140px]", children: _jsx(SelectValue, { placeholder: common.channelFilterPlaceholder }) }), _jsxs(SelectContent, { children: [_jsx(SelectItem, { value: "all", children: common.allChannels }), _jsx(SelectItem, { value: "email", children: common.channelEmail }), _jsx(SelectItem, { value: "sms", children: common.channelSms })] })] }), _jsxs(Select, { value: status, onValueChange: (value) => setStatus(value ?? "all"), children: [_jsx(SelectTrigger, { className: "w-[140px]", children: _jsx(SelectValue, { placeholder: common.statusFilterPlaceholder }) }), _jsxs(SelectContent, { children: [_jsx(SelectItem, { value: "all", children: common.allStatuses }), _jsx(SelectItem, { value: "draft", children: common.statusDraft }), _jsx(SelectItem, { value: "active", children: common.statusActive }), _jsx(SelectItem, { value: "archived", children: common.statusArchived })] })] })] }), isPending ? (_jsx("div", { className: "flex items-center justify-center py-12", children: _jsx(Loader2, { className: "h-6 w-6 animate-spin text-muted-foreground" }) })) : null, !isPending && (!data?.data || data.data.length === 0) ? (_jsx("div", { className: "rounded-md border border-dashed p-8 text-center", children: _jsx("p", { className: "text-sm text-muted-foreground", children: t.empty }) })) : null, !isPending && data?.data && data.data.length > 0 ? (_jsx("div", { className: "rounded-md border", children: _jsxs("table", { className: "w-full text-sm", children: [_jsx("thead", { className: "bg-muted/40 text-left text-xs uppercase tracking-wide text-muted-foreground", children: _jsxs("tr", { children: [_jsx("th", { className: "px-4 py-3", children: "Template" }), _jsx("th", { className: "px-4 py-3", children: "Channel" }), _jsx("th", { className: "px-4 py-3", children: "Status" }), _jsx("th", { className: "px-4 py-3", children: "Updated" }), _jsx("th", { className: "px-4 py-3 text-right", children: "Actions" })] }) }), _jsx("tbody", { children: data.data.map((template) => (_jsxs("tr", { className: "border-t", children: [_jsx("td", { className: "px-4 py-3", children: _jsxs(DestinationLink, { href: resolveHref("notificationTemplate.detail", {
43
+ templateId: template.id,
44
+ }), onNavigate: () => navigateTo("notificationTemplate.detail", { templateId: template.id }), className: "block rounded-sm outline-none transition-colors hover:text-primary focus-visible:text-primary", children: [_jsx("div", { className: "font-medium", children: template.name }), _jsx("div", { className: "font-mono text-xs text-muted-foreground", children: template.slug })] }) }), _jsx("td", { className: "px-4 py-3", children: _jsx(Badge, { variant: "outline", children: template.channel }) }), _jsx("td", { className: "px-4 py-3", children: _jsx(Badge, { variant: template.status === "active" ? "default" : "secondary", children: template.status }) }), _jsx("td", { className: "px-4 py-3", children: new Date(template.updatedAt).toLocaleString() }), _jsx("td", { className: "px-4 py-3 text-right", children: _jsx(Button, { variant: "ghost", size: "sm", onClick: () => {
45
+ setEditing(template);
46
+ setDialogOpen(true);
47
+ }, children: _jsx(Pencil, { className: "h-4 w-4" }) }) })] }, template.id))) })] }) })) : null, _jsx(Suspense, { fallback: null, children: _jsx(NotificationTemplateDialog, { open: dialogOpen, onOpenChange: setDialogOpen, template: editing, onSuccess: () => {
48
+ setDialogOpen(false);
49
+ setEditing(undefined);
50
+ void refetch();
51
+ } }) })] }));
52
+ }
@@ -0,0 +1,14 @@
1
+ import type { ReactNode } from "react";
2
+ /**
3
+ * SPA-friendly destination link shared by the packaged notifications admin
4
+ * hosts: real href for a11y / middle-click, host-router navigation on plain
5
+ * left click (packaged-admin RFC §4.7 — hrefs come from `useAdminHref`,
6
+ * clicks go through `useAdminNavigate`).
7
+ */
8
+ export declare function DestinationLink({ href, onNavigate, className, children, }: {
9
+ href: string;
10
+ onNavigate: () => void;
11
+ className?: string;
12
+ children: ReactNode;
13
+ }): import("react/jsx-runtime").JSX.Element;
14
+ //# sourceMappingURL=notifications-admin-shared.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"notifications-admin-shared.d.ts","sourceRoot":"","sources":["../../src/admin/notifications-admin-shared.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAc,SAAS,EAAE,MAAM,OAAO,CAAA;AAElD;;;;;GAKG;AACH,wBAAgB,eAAe,CAAC,EAC9B,IAAI,EACJ,UAAU,EACV,SAAS,EACT,QAAQ,GACT,EAAE;IACD,IAAI,EAAE,MAAM,CAAA;IACZ,UAAU,EAAE,MAAM,IAAI,CAAA;IACtB,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,QAAQ,EAAE,SAAS,CAAA;CACpB,2CAWA"}
@@ -0,0 +1,17 @@
1
+ "use client";
2
+ import { jsx as _jsx } from "react/jsx-runtime";
3
+ /**
4
+ * SPA-friendly destination link shared by the packaged notifications admin
5
+ * hosts: real href for a11y / middle-click, host-router navigation on plain
6
+ * left click (packaged-admin RFC §4.7 — hrefs come from `useAdminHref`,
7
+ * clicks go through `useAdminNavigate`).
8
+ */
9
+ export function DestinationLink({ href, onNavigate, className, children, }) {
10
+ const handleClick = (event) => {
11
+ if (event.defaultPrevented || event.metaKey || event.ctrlKey || event.shiftKey)
12
+ return;
13
+ event.preventDefault();
14
+ onNavigate();
15
+ };
16
+ return (_jsx("a", { href: href, onClick: handleClick, className: className, children: children }));
17
+ }
@@ -0,0 +1,8 @@
1
+ import type { AdminRoutePageProps } from "@voyantjs/admin";
2
+ /**
3
+ * Route page for the `notifications-reminder-rules-detail` contribution:
4
+ * binds the matched route's `$id` param onto
5
+ * {@link NotificationReminderRuleDetailHost}.
6
+ */
7
+ export default function NotificationReminderRuleDetailPage({ params }: AdminRoutePageProps): import("react/jsx-runtime").JSX.Element;
8
+ //# sourceMappingURL=notification-reminder-rule-detail-page.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"notification-reminder-rule-detail-page.d.ts","sourceRoot":"","sources":["../../../src/admin/pages/notification-reminder-rule-detail-page.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,iBAAiB,CAAA;AAI1D;;;;GAIG;AACH,MAAM,CAAC,OAAO,UAAU,kCAAkC,CAAC,EAAE,MAAM,EAAE,EAAE,mBAAmB,2CAEzF"}
@@ -0,0 +1,10 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { NotificationReminderRuleDetailHost } from "../notification-reminder-rule-detail-host.js";
3
+ /**
4
+ * Route page for the `notifications-reminder-rules-detail` contribution:
5
+ * binds the matched route's `$id` param onto
6
+ * {@link NotificationReminderRuleDetailHost}.
7
+ */
8
+ export default function NotificationReminderRuleDetailPage({ params }) {
9
+ return _jsx(NotificationReminderRuleDetailHost, { id: params.id ?? "" });
10
+ }
@@ -0,0 +1,7 @@
1
+ import type { AdminRoutePageProps } from "@voyantjs/admin";
2
+ /**
3
+ * Route page for the `notifications-templates-detail` contribution: binds
4
+ * the matched route's `$id` param onto {@link NotificationTemplateDetailHost}.
5
+ */
6
+ export default function NotificationTemplateDetailPage({ params }: AdminRoutePageProps): import("react/jsx-runtime").JSX.Element;
7
+ //# sourceMappingURL=notification-template-detail-page.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"notification-template-detail-page.d.ts","sourceRoot":"","sources":["../../../src/admin/pages/notification-template-detail-page.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,iBAAiB,CAAA;AAI1D;;;GAGG;AACH,MAAM,CAAC,OAAO,UAAU,8BAA8B,CAAC,EAAE,MAAM,EAAE,EAAE,mBAAmB,2CAErF"}
@@ -0,0 +1,9 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { NotificationTemplateDetailHost } from "../notification-template-detail-host.js";
3
+ /**
4
+ * Route page for the `notifications-templates-detail` contribution: binds
5
+ * the matched route's `$id` param onto {@link NotificationTemplateDetailHost}.
6
+ */
7
+ export default function NotificationTemplateDetailPage({ params }) {
8
+ return _jsx(NotificationTemplateDetailHost, { id: params.id ?? "" });
9
+ }
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Packaged admin host for the read-only reminders preview page
3
+ * (packaged-admin RFC Phase 3). Zero-prop: the preview list owns its data
4
+ * wiring through `@voyantjs/notifications-react`.
5
+ */
6
+ export declare function RemindersPreviewHost(): import("react/jsx-runtime").JSX.Element;
7
+ //# sourceMappingURL=reminders-preview-host.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"reminders-preview-host.d.ts","sourceRoot":"","sources":["../../src/admin/reminders-preview-host.tsx"],"names":[],"mappings":"AAKA;;;;GAIG;AACH,wBAAgB,oBAAoB,4CAYnC"}
@@ -0,0 +1,13 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { RemindersPreviewList } from "../components/reminders-preview-list.js";
4
+ import { useNotificationsUiMessagesOrDefault } from "../i18n/index.js";
5
+ /**
6
+ * Packaged admin host for the read-only reminders preview page
7
+ * (packaged-admin RFC Phase 3). Zero-prop: the preview list owns its data
8
+ * wiring through `@voyantjs/notifications-react`.
9
+ */
10
+ export function RemindersPreviewHost() {
11
+ const t = useNotificationsUiMessagesOrDefault().admin.previewPage;
12
+ return (_jsxs("div", { className: "container mx-auto space-y-6 p-6", children: [_jsxs("div", { children: [_jsx("h1", { className: "text-2xl font-semibold", children: t.title }), _jsx("p", { className: "text-sm text-muted-foreground", children: t.description })] }), _jsx(RemindersPreviewList, {})] }));
13
+ }
@@ -0,0 +1,2 @@
1
+ export declare function NotificationSettingsForm(): import("react/jsx-runtime").JSX.Element;
2
+ //# sourceMappingURL=notification-settings-form.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"notification-settings-form.d.ts","sourceRoot":"","sources":["../../src/components/notification-settings-form.tsx"],"names":[],"mappings":"AAsDA,wBAAgB,wBAAwB,4CAqNvC"}