@voyantjs/notifications-ui 0.28.3

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 (45) hide show
  1. package/README.md +21 -0
  2. package/dist/components/notification-settings-form.d.ts +2 -0
  3. package/dist/components/notification-settings-form.d.ts.map +1 -0
  4. package/dist/components/notification-settings-form.js +66 -0
  5. package/dist/components/reminders-preview-list.d.ts +6 -0
  6. package/dist/components/reminders-preview-list.d.ts.map +1 -0
  7. package/dist/components/reminders-preview-list.js +19 -0
  8. package/dist/components/stage-channel-editor-dialog.d.ts +11 -0
  9. package/dist/components/stage-channel-editor-dialog.d.ts.map +1 -0
  10. package/dist/components/stage-channel-editor-dialog.js +77 -0
  11. package/dist/components/stage-channel-list.d.ts +6 -0
  12. package/dist/components/stage-channel-list.d.ts.map +1 -0
  13. package/dist/components/stage-channel-list.js +20 -0
  14. package/dist/components/stage-editor-dialog.d.ts +10 -0
  15. package/dist/components/stage-editor-dialog.d.ts.map +1 -0
  16. package/dist/components/stage-editor-dialog.js +104 -0
  17. package/dist/components/stage-list.d.ts +5 -0
  18. package/dist/components/stage-list.d.ts.map +1 -0
  19. package/dist/components/stage-list.js +34 -0
  20. package/dist/components/template-picker.d.ts +19 -0
  21. package/dist/components/template-picker.d.ts.map +1 -0
  22. package/dist/components/template-picker.js +24 -0
  23. package/dist/components/timezone-combobox.d.ts +9 -0
  24. package/dist/components/timezone-combobox.d.ts.map +1 -0
  25. package/dist/components/timezone-combobox.js +65 -0
  26. package/dist/i18n/en.d.ts +3 -0
  27. package/dist/i18n/en.d.ts.map +1 -0
  28. package/dist/i18n/en.js +138 -0
  29. package/dist/i18n/index.d.ts +5 -0
  30. package/dist/i18n/index.d.ts.map +1 -0
  31. package/dist/i18n/index.js +3 -0
  32. package/dist/i18n/messages.d.ts +139 -0
  33. package/dist/i18n/messages.d.ts.map +1 -0
  34. package/dist/i18n/messages.js +1 -0
  35. package/dist/i18n/provider.d.ts +26 -0
  36. package/dist/i18n/provider.d.ts.map +1 -0
  37. package/dist/i18n/provider.js +44 -0
  38. package/dist/i18n/ro.d.ts +3 -0
  39. package/dist/i18n/ro.d.ts.map +1 -0
  40. package/dist/i18n/ro.js +138 -0
  41. package/dist/index.d.ts +9 -0
  42. package/dist/index.d.ts.map +1 -0
  43. package/dist/index.js +8 -0
  44. package/package.json +93 -0
  45. package/src/styles.css +2 -0
package/README.md ADDED
@@ -0,0 +1,21 @@
1
+ # @voyantjs/notifications-ui
2
+
3
+ React components for Voyant notifications: reminder sequence editor (stages + channels), notification settings, and preview.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ pnpm add @voyantjs/notifications-ui @voyantjs/notifications-react @voyantjs/ui @tanstack/react-query react react-dom
9
+ ```
10
+
11
+ ```ts
12
+ import "@voyantjs/notifications-ui/styles.css"
13
+ ```
14
+
15
+ ## Components
16
+
17
+ - `<StageList />` — reminder rule's ordered stages with reorder + delete.
18
+ - `<StageEditorDialog />` — create / edit a stage (anchor, window, cadence).
19
+ - `<StageChannelEditorDialog />` — create / edit a channel under a stage.
20
+ - `<NotificationSettingsForm />` — quiet hours, blackout dates, weekend skip, recipient rate limit, suppression window.
21
+ - `<RemindersPreviewList />` — what would fire on a given date with reasoning.
@@ -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":"AA0DA,wBAAgB,wBAAwB,4CAgNvC"}
@@ -0,0 +1,66 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { useNotificationSettings, useNotificationSettingsMutation, } from "@voyantjs/notifications-react";
4
+ import { Button, Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle, Input, } from "@voyantjs/ui/components";
5
+ import { DatePicker } from "@voyantjs/ui/components/date-picker";
6
+ import { Field, FieldDescription, FieldGroup, FieldLabel, FieldLegend, FieldSet, FieldTitle, } from "@voyantjs/ui/components/field";
7
+ import { Switch } from "@voyantjs/ui/components/switch";
8
+ import { Loader2, Plus, Trash2 } from "lucide-react";
9
+ import { useEffect, useState } from "react";
10
+ import { useNotificationsUiMessagesOrDefault } from "../i18n/index.js";
11
+ import { TimezoneCombobox } from "./timezone-combobox.js";
12
+ let blackoutSeq = 0;
13
+ const nextBlackoutKey = () => `bo-${++blackoutSeq}`;
14
+ const emptyForm = {
15
+ quietHoursStart: "",
16
+ quietHoursEnd: "",
17
+ quietHoursTz: "UTC",
18
+ blackoutDates: [],
19
+ skipWeekends: false,
20
+ recipientRateLimitPerDay: "",
21
+ suppressionWindowHours: 24,
22
+ };
23
+ export function NotificationSettingsForm() {
24
+ const messages = useNotificationsUiMessagesOrDefault();
25
+ const { data: settings, isLoading } = useNotificationSettings();
26
+ const mutation = useNotificationSettingsMutation();
27
+ const [form, setForm] = useState(emptyForm);
28
+ useEffect(() => {
29
+ if (!settings)
30
+ return;
31
+ setForm({
32
+ quietHoursStart: settings.quietHoursLocal?.start ?? "",
33
+ quietHoursEnd: settings.quietHoursLocal?.end ?? "",
34
+ quietHoursTz: settings.quietHoursLocal?.tz ?? "UTC",
35
+ blackoutDates: (settings.blackoutDates ?? []).map((date) => ({
36
+ rowKey: nextBlackoutKey(),
37
+ date,
38
+ })),
39
+ skipWeekends: settings.skipWeekends,
40
+ recipientRateLimitPerDay: settings.recipientRateLimitPerDay?.toString() ?? "",
41
+ suppressionWindowHours: settings.suppressionWindowHours,
42
+ });
43
+ }, [settings]);
44
+ const setField = (key, value) => setForm((prev) => ({ ...prev, [key]: value }));
45
+ const addBlackout = () => setField("blackoutDates", [...form.blackoutDates, { rowKey: nextBlackoutKey(), date: null }]);
46
+ const removeBlackout = (rowKey) => setField("blackoutDates", form.blackoutDates.filter((row) => row.rowKey !== rowKey));
47
+ const updateBlackout = (rowKey, date) => setField("blackoutDates", form.blackoutDates.map((row) => (row.rowKey === rowKey ? { ...row, date } : row)));
48
+ const handleSubmit = async () => {
49
+ const dates = form.blackoutDates
50
+ .map((row) => row.date)
51
+ .filter((d) => Boolean(d && /^\d{4}-\d{2}-\d{2}$/.test(d)));
52
+ const quiet = form.quietHoursStart && form.quietHoursEnd
53
+ ? { start: form.quietHoursStart, end: form.quietHoursEnd, tz: form.quietHoursTz || "UTC" }
54
+ : null;
55
+ await mutation.mutateAsync({
56
+ quietHoursLocal: quiet,
57
+ blackoutDates: dates.length > 0 ? dates : null,
58
+ skipWeekends: form.skipWeekends,
59
+ recipientRateLimitPerDay: form.recipientRateLimitPerDay
60
+ ? Number(form.recipientRateLimitPerDay)
61
+ : null,
62
+ suppressionWindowHours: form.suppressionWindowHours,
63
+ });
64
+ };
65
+ return (_jsxs(Card, { children: [_jsxs(CardHeader, { children: [_jsx(CardTitle, { children: messages.settings.heading }), _jsx(CardDescription, { children: messages.settings.description })] }), _jsxs(CardContent, { children: [isLoading && _jsx("p", { className: "text-sm text-muted-foreground", children: messages.common.loading }), _jsxs(FieldGroup, { children: [_jsxs(FieldSet, { children: [_jsx(FieldLegend, { children: messages.settings.sections.quietHours }), _jsx(FieldDescription, { children: messages.settings.sections.quietHoursDesc }), _jsxs(FieldGroup, { className: "gap-4", children: [_jsxs(Field, { orientation: "horizontal", children: [_jsx(Switch, { id: "skipWeekends", checked: form.skipWeekends, onCheckedChange: (value) => setField("skipWeekends", value) }), _jsxs(FieldLabel, { htmlFor: "skipWeekends", className: "!w-auto !flex-row", children: [_jsx(FieldTitle, { children: messages.settings.fields.skipWeekends }), _jsx(FieldDescription, { children: messages.settings.fields.skipWeekendsDesc })] })] }), _jsxs("div", { className: "grid grid-cols-1 gap-4 md:grid-cols-3", children: [_jsxs(Field, { children: [_jsx(FieldLabel, { htmlFor: "quietHoursStart", children: messages.settings.fields.quietHoursStart }), _jsx(Input, { id: "quietHoursStart", type: "time", value: form.quietHoursStart, onChange: (e) => setField("quietHoursStart", e.target.value) })] }), _jsxs(Field, { children: [_jsx(FieldLabel, { htmlFor: "quietHoursEnd", children: messages.settings.fields.quietHoursEnd }), _jsx(Input, { id: "quietHoursEnd", type: "time", value: form.quietHoursEnd, onChange: (e) => setField("quietHoursEnd", e.target.value) })] }), _jsxs(Field, { children: [_jsx(FieldLabel, { children: messages.settings.fields.quietHoursTz }), _jsx(TimezoneCombobox, { value: form.quietHoursTz, onChange: (value) => setField("quietHoursTz", value ?? "UTC"), placeholder: messages.settings.placeholders.tz })] })] })] })] }), _jsxs(FieldSet, { children: [_jsx(FieldLegend, { children: messages.settings.sections.blackouts }), _jsx(FieldDescription, { children: messages.settings.sections.blackoutsDesc }), _jsxs("div", { className: "space-y-2", children: [form.blackoutDates.length === 0 && (_jsx("p", { className: "text-sm text-muted-foreground", children: messages.settings.placeholders.noBlackouts })), form.blackoutDates.map((row) => (_jsxs("div", { className: "flex items-center gap-2", children: [_jsx("div", { className: "flex-1", children: _jsx(DatePicker, { value: row.date, onChange: (value) => updateBlackout(row.rowKey, value) }) }), _jsx(Button, { type: "button", variant: "ghost", size: "icon", onClick: () => removeBlackout(row.rowKey), "aria-label": messages.settings.actions.removeBlackoutDate, children: _jsx(Trash2, { className: "size-4" }) })] }, row.rowKey))), _jsxs(Button, { type: "button", variant: "outline", size: "sm", onClick: addBlackout, children: [_jsx(Plus, { className: "size-4" }), " ", messages.settings.actions.addBlackoutDate] })] })] }), _jsxs(FieldSet, { children: [_jsx(FieldLegend, { children: messages.settings.sections.rateLimits }), _jsx(FieldDescription, { children: messages.settings.sections.rateLimitsDesc }), _jsxs("div", { className: "grid grid-cols-1 gap-4 md:grid-cols-2", children: [_jsxs(Field, { children: [_jsx(FieldLabel, { htmlFor: "recipientRateLimitPerDay", children: messages.settings.fields.recipientRateLimitPerDay }), _jsx(Input, { id: "recipientRateLimitPerDay", type: "number", min: 1, value: form.recipientRateLimitPerDay, onChange: (e) => setField("recipientRateLimitPerDay", e.target.value), placeholder: messages.common.optionalPlaceholder }), _jsx(FieldDescription, { children: messages.settings.helpers.recipientRateLimitPerDay })] }), _jsxs(Field, { children: [_jsx(FieldLabel, { htmlFor: "suppressionWindowHours", children: messages.settings.fields.suppressionWindowHours }), _jsx(Input, { id: "suppressionWindowHours", type: "number", min: 0, value: form.suppressionWindowHours, onChange: (e) => setField("suppressionWindowHours", Number(e.target.value)) }), _jsx(FieldDescription, { children: messages.settings.helpers.suppressionWindowHours })] })] })] })] })] }), _jsx(CardFooter, { className: "justify-end", children: _jsxs(Button, { onClick: handleSubmit, disabled: mutation.isPending, children: [mutation.isPending ? _jsx(Loader2, { className: "size-4 animate-spin" }) : null, messages.common.save] }) })] }));
66
+ }
@@ -0,0 +1,6 @@
1
+ export interface RemindersPreviewListProps {
2
+ ruleId?: string;
3
+ targetId?: string;
4
+ }
5
+ export declare function RemindersPreviewList({ ruleId, targetId }: RemindersPreviewListProps): import("react/jsx-runtime").JSX.Element;
6
+ //# sourceMappingURL=reminders-preview-list.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"reminders-preview-list.d.ts","sourceRoot":"","sources":["../../src/components/reminders-preview-list.tsx"],"names":[],"mappings":"AAuBA,MAAM,WAAW,yBAAyB;IACxC,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,QAAQ,CAAC,EAAE,MAAM,CAAA;CAClB;AAED,wBAAgB,oBAAoB,CAAC,EAAE,MAAM,EAAE,QAAQ,EAAE,EAAE,yBAAyB,2CAyDnF"}
@@ -0,0 +1,19 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { useRemindersPreview } from "@voyantjs/notifications-react";
4
+ import { Card, CardContent } from "@voyantjs/ui/components";
5
+ import { DatePicker } from "@voyantjs/ui/components/date-picker";
6
+ import { Field, FieldLabel } from "@voyantjs/ui/components/field";
7
+ import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from "@voyantjs/ui/components/table";
8
+ import { Loader2 } from "lucide-react";
9
+ import { useState } from "react";
10
+ import { useNotificationsUiMessagesOrDefault } from "../i18n/index.js";
11
+ function todayIso() {
12
+ return new Date().toISOString().slice(0, 10);
13
+ }
14
+ export function RemindersPreviewList({ ruleId, targetId }) {
15
+ const messages = useNotificationsUiMessagesOrDefault();
16
+ const [date, setDate] = useState(todayIso());
17
+ const { data, isFetching } = useRemindersPreview({ date, ruleId, targetId });
18
+ return (_jsx(Card, { children: _jsxs(CardContent, { className: "space-y-4", children: [_jsxs("div", { className: "flex items-center justify-between gap-3", children: [_jsxs(Field, { className: "max-w-xs", children: [_jsx(FieldLabel, { htmlFor: "preview-date", children: messages.preview.dateLabel }), _jsx(DatePicker, { value: date, onChange: (next) => setDate(next ?? todayIso()), clearable: false })] }), isFetching ? (_jsx(Loader2, { className: "size-4 animate-spin text-muted-foreground", "aria-hidden": true })) : null] }), data && data.length === 0 ? (_jsx("p", { className: "text-sm text-muted-foreground", children: messages.preview.empty })) : (_jsxs(Table, { children: [_jsx(TableHeader, { children: _jsxs(TableRow, { children: [_jsx(TableHead, { children: messages.preview.columns.rule }), _jsx(TableHead, { children: messages.preview.columns.stage }), _jsx(TableHead, { children: messages.preview.columns.target }), _jsx(TableHead, { children: messages.preview.columns.anchor }), _jsx(TableHead, { children: messages.preview.columns.scheduledAt }), _jsx(TableHead, { children: messages.preview.columns.reasoning })] }) }), _jsx(TableBody, { children: data?.map((row) => (_jsxs(TableRow, { children: [_jsx(TableCell, { className: "font-medium", children: row.ruleName }), _jsxs(TableCell, { children: ["#", row.stageOrderIndex, " ", row.stageName ?? ""] }), _jsx(TableCell, { className: "font-mono text-xs", children: row.targetId }), _jsx(TableCell, { children: row.anchor }), _jsx(TableCell, { className: "font-mono text-xs", children: new Date(row.scheduledAt).toLocaleString() }), _jsx(TableCell, { className: "text-xs text-muted-foreground", children: row.reasoning })] }, `${row.ruleId}:${row.targetId}:${row.stageId}`))) })] }))] }) }));
19
+ }
@@ -0,0 +1,11 @@
1
+ import { type ReminderStageChannelRecord } from "@voyantjs/notifications-react";
2
+ export interface StageChannelEditorDialogProps {
3
+ reminderRuleId: string;
4
+ stageId: string;
5
+ channel: ReminderStageChannelRecord | null;
6
+ defaultOrderIndex?: number;
7
+ open: boolean;
8
+ onOpenChange: (open: boolean) => void;
9
+ }
10
+ export declare function StageChannelEditorDialog({ reminderRuleId, stageId, channel, defaultOrderIndex, open, onOpenChange, }: StageChannelEditorDialogProps): import("react/jsx-runtime").JSX.Element;
11
+ //# sourceMappingURL=stage-channel-editor-dialog.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"stage-channel-editor-dialog.d.ts","sourceRoot":"","sources":["../../src/components/stage-channel-editor-dialog.tsx"],"names":[],"mappings":"AAEA,OAAO,EACL,KAAK,0BAA0B,EAEhC,MAAM,+BAA+B,CAAA;AAwDtC,MAAM,WAAW,6BAA6B;IAC5C,cAAc,EAAE,MAAM,CAAA;IACtB,OAAO,EAAE,MAAM,CAAA;IACf,OAAO,EAAE,0BAA0B,GAAG,IAAI,CAAA;IAC1C,iBAAiB,CAAC,EAAE,MAAM,CAAA;IAC1B,IAAI,EAAE,OAAO,CAAA;IACb,YAAY,EAAE,CAAC,IAAI,EAAE,OAAO,KAAK,IAAI,CAAA;CACtC;AAED,wBAAgB,wBAAwB,CAAC,EACvC,cAAc,EACd,OAAO,EACP,OAAO,EACP,iBAAqB,EACrB,IAAI,EACJ,YAAY,GACb,EAAE,6BAA6B,2CA+J/B"}
@@ -0,0 +1,77 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { useReminderStageChannelMutation, } from "@voyantjs/notifications-react";
4
+ import { Button, Dialog, DialogBody, DialogContent, DialogFooter, DialogHeader, DialogTitle, Input, Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@voyantjs/ui/components";
5
+ import { Field, FieldDescription, FieldGroup, FieldLabel } from "@voyantjs/ui/components/field";
6
+ import { Loader2 } from "lucide-react";
7
+ import { useEffect, useState } from "react";
8
+ import { useNotificationsUiMessagesOrDefault } from "../i18n/index.js";
9
+ import { TemplatePicker } from "./template-picker.js";
10
+ function fromRecord(channel, orderIndex) {
11
+ if (!channel) {
12
+ return {
13
+ orderIndex,
14
+ channel: "email",
15
+ provider: "automatic",
16
+ templateId: null,
17
+ recipientKind: "primary",
18
+ };
19
+ }
20
+ const provider = channel.provider === "resend" || channel.provider === "twilio" ? channel.provider : "automatic";
21
+ return {
22
+ orderIndex: channel.orderIndex,
23
+ channel: channel.channel,
24
+ provider,
25
+ templateId: channel.templateId ?? null,
26
+ recipientKind: channel.recipientKind,
27
+ };
28
+ }
29
+ export function StageChannelEditorDialog({ reminderRuleId, stageId, channel, defaultOrderIndex = 0, open, onOpenChange, }) {
30
+ const messages = useNotificationsUiMessagesOrDefault();
31
+ const { create, update } = useReminderStageChannelMutation(reminderRuleId, stageId);
32
+ const [form, setForm] = useState(() => fromRecord(channel, defaultOrderIndex));
33
+ const isEdit = Boolean(channel);
34
+ const isPending = create.isPending || update.isPending;
35
+ useEffect(() => {
36
+ if (open)
37
+ setForm(fromRecord(channel, defaultOrderIndex));
38
+ }, [open, channel, defaultOrderIndex]);
39
+ const setField = (key, value) => setForm((prev) => ({ ...prev, [key]: value }));
40
+ const handleSubmit = async () => {
41
+ const input = {
42
+ orderIndex: form.orderIndex,
43
+ channel: form.channel,
44
+ provider: form.provider === "automatic" ? null : form.provider,
45
+ templateId: form.templateId,
46
+ templateSlug: null,
47
+ recipientKind: form.recipientKind,
48
+ recipientRole: null,
49
+ };
50
+ if (isEdit && channel) {
51
+ await update.mutateAsync({ channelId: channel.id, input });
52
+ }
53
+ else {
54
+ await create.mutateAsync(input);
55
+ }
56
+ onOpenChange(false);
57
+ };
58
+ return (_jsx(Dialog, { open: open, onOpenChange: onOpenChange, children: _jsxs(DialogContent, { children: [_jsx(DialogHeader, { children: _jsx(DialogTitle, { children: isEdit ? messages.channel.titles.edit : messages.channel.titles.create }) }), _jsx(DialogBody, { children: _jsxs(FieldGroup, { children: [_jsxs("div", { className: "grid grid-cols-1 gap-4 md:grid-cols-[1fr_8rem]", children: [_jsxs(Field, { children: [_jsx(FieldLabel, { htmlFor: "channel-channel", children: messages.channel.fields.channel }), _jsxs(Select, { value: form.channel, onValueChange: (v) => {
59
+ if (!v)
60
+ return;
61
+ const next = v;
62
+ setForm((prev) => ({
63
+ ...prev,
64
+ channel: next,
65
+ // Picked template no longer matches the new channel — clear it.
66
+ templateId: null,
67
+ }));
68
+ }, children: [_jsx(SelectTrigger, { id: "channel-channel", children: _jsx(SelectValue, {}) }), _jsxs(SelectContent, { children: [_jsx(SelectItem, { value: "email", children: messages.channel.channels.email }), _jsx(SelectItem, { value: "sms", children: messages.channel.channels.sms })] })] })] }), _jsxs(Field, { children: [_jsx(FieldLabel, { htmlFor: "channel-order", children: messages.channel.fields.orderIndex }), _jsx(Input, { id: "channel-order", type: "number", value: form.orderIndex, onChange: (e) => setField("orderIndex", Number(e.target.value)) })] })] }), _jsxs(Field, { children: [_jsx(FieldLabel, { children: messages.channel.fields.template }), _jsx(TemplatePicker, { value: form.templateId, onChange: (value) => setField("templateId", value), channel: form.channel, placeholder: messages.channel.placeholders.template }), _jsx(FieldDescription, { children: "Filtered by the channel above. Resolved at send time so editing the template doesn't need a rule update." })] }), _jsxs("div", { className: "grid grid-cols-1 gap-4 md:grid-cols-2", children: [_jsxs(Field, { children: [_jsx(FieldLabel, { htmlFor: "channel-recipient-kind", children: messages.channel.fields.recipientKind }), _jsxs(Select, { value: form.recipientKind, onValueChange: (v) => {
69
+ if (!v)
70
+ return;
71
+ setField("recipientKind", v);
72
+ }, children: [_jsx(SelectTrigger, { id: "channel-recipient-kind", children: _jsx(SelectValue, {}) }), _jsxs(SelectContent, { children: [_jsx(SelectItem, { value: "primary", children: messages.channel.recipientKinds.primary }), _jsx(SelectItem, { value: "cc", children: messages.channel.recipientKinds.cc }), _jsx(SelectItem, { value: "bcc", children: messages.channel.recipientKinds.bcc })] })] })] }), _jsxs(Field, { children: [_jsx(FieldLabel, { htmlFor: "channel-provider", children: messages.channel.fields.provider }), _jsxs(Select, { value: form.provider, onValueChange: (v) => {
73
+ if (!v)
74
+ return;
75
+ setField("provider", v);
76
+ }, children: [_jsx(SelectTrigger, { id: "channel-provider", children: _jsx(SelectValue, {}) }), _jsxs(SelectContent, { children: [_jsx(SelectItem, { value: "automatic", children: messages.channel.providers.automatic }), _jsx(SelectItem, { value: "resend", children: messages.channel.providers.resend }), _jsx(SelectItem, { value: "twilio", children: messages.channel.providers.twilio })] })] }), _jsx(FieldDescription, { children: "Use Automatic to fall back to the deployment default for this channel." })] })] })] }) }), _jsxs(DialogFooter, { children: [_jsx(Button, { variant: "outline", onClick: () => onOpenChange(false), disabled: isPending, children: messages.common.cancel }), _jsxs(Button, { onClick: handleSubmit, disabled: isPending, children: [isPending ? _jsx(Loader2, { className: "size-4 animate-spin" }) : null, isEdit ? messages.common.save : messages.common.create] })] })] }) }));
77
+ }
@@ -0,0 +1,6 @@
1
+ export interface StageChannelListProps {
2
+ reminderRuleId: string;
3
+ stageId: string;
4
+ }
5
+ export declare function StageChannelList({ reminderRuleId, stageId }: StageChannelListProps): import("react/jsx-runtime").JSX.Element;
6
+ //# sourceMappingURL=stage-channel-list.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"stage-channel-list.d.ts","sourceRoot":"","sources":["../../src/components/stage-channel-list.tsx"],"names":[],"mappings":"AAcA,MAAM,WAAW,qBAAqB;IACpC,cAAc,EAAE,MAAM,CAAA;IACtB,OAAO,EAAE,MAAM,CAAA;CAChB;AAED,wBAAgB,gBAAgB,CAAC,EAAE,cAAc,EAAE,OAAO,EAAE,EAAE,qBAAqB,2CAyElF"}
@@ -0,0 +1,20 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { useReminderStageChannelMutation, useReminderStageChannels, } from "@voyantjs/notifications-react";
4
+ import { Badge, Button, Card, CardContent, CardHeader, CardTitle } from "@voyantjs/ui/components";
5
+ import { Pencil, Plus, Trash2 } from "lucide-react";
6
+ import { useState } from "react";
7
+ import { useNotificationsUiMessagesOrDefault } from "../i18n/index.js";
8
+ import { StageChannelEditorDialog } from "./stage-channel-editor-dialog.js";
9
+ export function StageChannelList({ reminderRuleId, stageId }) {
10
+ const messages = useNotificationsUiMessagesOrDefault();
11
+ const { data: channels, isLoading } = useReminderStageChannels(reminderRuleId, stageId);
12
+ const { remove } = useReminderStageChannelMutation(reminderRuleId, stageId);
13
+ const [editing, setEditing] = useState(null);
14
+ const [creating, setCreating] = useState(false);
15
+ return (_jsxs(Card, { className: "border-dashed", children: [_jsxs(CardHeader, { className: "flex flex-row items-center justify-between space-y-0 pb-2", children: [_jsx(CardTitle, { className: "text-sm font-medium", children: messages.channel.listHeading }), _jsxs(Button, { size: "sm", variant: "outline", onClick: () => setCreating(true), children: [_jsx(Plus, { className: "size-4" }), " ", messages.channel.addChannel] })] }), _jsxs(CardContent, { className: "space-y-2", children: [isLoading && _jsx("p", { className: "text-sm text-muted-foreground", children: messages.common.loading }), channels && channels.length === 0 && (_jsx("p", { className: "text-sm text-muted-foreground", children: messages.channel.listEmpty })), channels?.map((channel) => (_jsxs("div", { className: "flex items-center justify-between rounded-md border px-3 py-2 text-sm", children: [_jsxs("div", { className: "flex items-center gap-3", children: [_jsx(Badge, { variant: "outline", children: messages.channel.channels[channel.channel] }), _jsx(Badge, { variant: "secondary", children: messages.channel.recipientKinds[channel.recipientKind] })] }), _jsxs("div", { className: "flex items-center gap-1", children: [_jsx(Button, { size: "icon", variant: "ghost", onClick: () => setEditing(channel), children: _jsx(Pencil, { className: "size-4" }) }), _jsx(Button, { size: "icon", variant: "ghost", onClick: () => {
16
+ if (window.confirm(messages.channel.deleteConfirm)) {
17
+ void remove.mutateAsync(channel.id);
18
+ }
19
+ }, children: _jsx(Trash2, { className: "size-4" }) })] })] }, channel.id)))] }), creating && (_jsx(StageChannelEditorDialog, { reminderRuleId: reminderRuleId, stageId: stageId, channel: null, defaultOrderIndex: channels?.length ?? 0, open: creating, onOpenChange: setCreating })), editing && (_jsx(StageChannelEditorDialog, { reminderRuleId: reminderRuleId, stageId: stageId, channel: editing, open: Boolean(editing), onOpenChange: (open) => !open && setEditing(null) }))] }));
20
+ }
@@ -0,0 +1,10 @@
1
+ import { type ReminderRuleStageRecord } from "@voyantjs/notifications-react";
2
+ export interface StageEditorDialogProps {
3
+ reminderRuleId: string;
4
+ stage: ReminderRuleStageRecord | null;
5
+ defaultOrderIndex?: number;
6
+ open: boolean;
7
+ onOpenChange: (open: boolean) => void;
8
+ }
9
+ export declare function StageEditorDialog({ reminderRuleId, stage, defaultOrderIndex, open, onOpenChange, }: StageEditorDialogProps): import("react/jsx-runtime").JSX.Element;
10
+ //# sourceMappingURL=stage-editor-dialog.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"stage-editor-dialog.d.ts","sourceRoot":"","sources":["../../src/components/stage-editor-dialog.tsx"],"names":[],"mappings":"AAEA,OAAO,EACL,KAAK,uBAAuB,EAE7B,MAAM,+BAA+B,CAAA;AAoGtC,MAAM,WAAW,sBAAsB;IACrC,cAAc,EAAE,MAAM,CAAA;IACtB,KAAK,EAAE,uBAAuB,GAAG,IAAI,CAAA;IACrC,iBAAiB,CAAC,EAAE,MAAM,CAAA;IAC1B,IAAI,EAAE,OAAO,CAAA;IACb,YAAY,EAAE,CAAC,IAAI,EAAE,OAAO,KAAK,IAAI,CAAA;CACtC;AAED,wBAAgB,iBAAiB,CAAC,EAChC,cAAc,EACd,KAAK,EACL,iBAAqB,EACrB,IAAI,EACJ,YAAY,GACb,EAAE,sBAAsB,2CAuTxB"}
@@ -0,0 +1,104 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { useReminderRuleStageMutation, } from "@voyantjs/notifications-react";
4
+ import { Button, Dialog, DialogBody, DialogContent, DialogFooter, DialogHeader, DialogTitle, Input, Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@voyantjs/ui/components";
5
+ import { Field, FieldDescription, FieldGroup, FieldLabel, FieldLegend, FieldSet, FieldTitle, } from "@voyantjs/ui/components/field";
6
+ import { Switch } from "@voyantjs/ui/components/switch";
7
+ import { Loader2, Plus, Trash2 } from "lucide-react";
8
+ import { useEffect, useState } from "react";
9
+ import { useNotificationsUiMessagesOrDefault } from "../i18n/index.js";
10
+ let intervalRowSeq = 0;
11
+ const nextIntervalRowKey = () => `iv-${++intervalRowSeq}`;
12
+ const ANCHORS = [
13
+ "due_date",
14
+ "booking_created_at",
15
+ "departure_date",
16
+ "invoice_issued_at",
17
+ "last_send_at",
18
+ ];
19
+ const CADENCES = ["once", "every_n_days", "escalating"];
20
+ function fromRecord(stage, orderIndex) {
21
+ if (!stage) {
22
+ return {
23
+ name: "",
24
+ orderIndex,
25
+ anchor: "due_date",
26
+ windowStartDays: -7,
27
+ windowEndDays: 0,
28
+ cadenceKind: "once",
29
+ cadenceEveryDays: null,
30
+ cadenceIntervals: [],
31
+ maxSendsInStage: null,
32
+ respectQuietHours: true,
33
+ };
34
+ }
35
+ return {
36
+ name: stage.name ?? "",
37
+ orderIndex: stage.orderIndex,
38
+ anchor: stage.anchor,
39
+ windowStartDays: stage.windowStartDays,
40
+ windowEndDays: stage.windowEndDays,
41
+ cadenceKind: stage.cadenceKind,
42
+ cadenceEveryDays: stage.cadenceEveryDays,
43
+ cadenceIntervals: stage.cadenceIntervals?.map((i) => ({
44
+ rowKey: nextIntervalRowKey(),
45
+ whenDaysUntilDueGT: i.whenDaysUntilDueGT ?? null,
46
+ repeatEveryDays: i.repeatEveryDays,
47
+ })) ?? [],
48
+ maxSendsInStage: stage.maxSendsInStage,
49
+ respectQuietHours: stage.respectQuietHours,
50
+ };
51
+ }
52
+ export function StageEditorDialog({ reminderRuleId, stage, defaultOrderIndex = 0, open, onOpenChange, }) {
53
+ const messages = useNotificationsUiMessagesOrDefault();
54
+ const { create, update } = useReminderRuleStageMutation(reminderRuleId);
55
+ const [form, setForm] = useState(() => fromRecord(stage, defaultOrderIndex));
56
+ const isEdit = Boolean(stage);
57
+ const isPending = create.isPending || update.isPending;
58
+ useEffect(() => {
59
+ if (open)
60
+ setForm(fromRecord(stage, defaultOrderIndex));
61
+ }, [open, stage, defaultOrderIndex]);
62
+ const setField = (key, value) => setForm((prev) => ({ ...prev, [key]: value }));
63
+ const addInterval = () => setField("cadenceIntervals", [
64
+ ...form.cadenceIntervals,
65
+ { rowKey: nextIntervalRowKey(), whenDaysUntilDueGT: null, repeatEveryDays: 7 },
66
+ ]);
67
+ const removeInterval = (rowKey) => setField("cadenceIntervals", form.cadenceIntervals.filter((row) => row.rowKey !== rowKey));
68
+ const handleSubmit = async () => {
69
+ const input = {
70
+ name: form.name || null,
71
+ orderIndex: form.orderIndex,
72
+ anchor: form.anchor,
73
+ windowStartDays: form.windowStartDays,
74
+ windowEndDays: form.windowEndDays,
75
+ cadenceKind: form.cadenceKind,
76
+ cadenceEveryDays: form.cadenceKind === "every_n_days" ? form.cadenceEveryDays : null,
77
+ cadenceIntervals: form.cadenceKind === "escalating"
78
+ ? form.cadenceIntervals.map((row) => ({
79
+ whenDaysUntilDueGT: row.whenDaysUntilDueGT,
80
+ repeatEveryDays: row.repeatEveryDays,
81
+ }))
82
+ : null,
83
+ maxSendsInStage: form.maxSendsInStage,
84
+ respectQuietHours: form.respectQuietHours,
85
+ };
86
+ if (isEdit && stage) {
87
+ await update.mutateAsync({ stageId: stage.id, input });
88
+ }
89
+ else {
90
+ await create.mutateAsync(input);
91
+ }
92
+ onOpenChange(false);
93
+ };
94
+ return (_jsx(Dialog, { open: open, onOpenChange: onOpenChange, children: _jsxs(DialogContent, { className: "max-w-2xl", children: [_jsx(DialogHeader, { children: _jsx(DialogTitle, { children: isEdit ? messages.stage.titles.edit : messages.stage.titles.create }) }), _jsx(DialogBody, { children: _jsxs(FieldGroup, { children: [_jsxs("div", { className: "grid grid-cols-1 gap-4 md:grid-cols-[1fr_8rem]", children: [_jsxs(Field, { children: [_jsx(FieldLabel, { htmlFor: "stage-name", children: messages.stage.fields.name }), _jsx(Input, { id: "stage-name", value: form.name, onChange: (e) => setField("name", e.target.value), placeholder: messages.stage.placeholders.name })] }), _jsxs(Field, { children: [_jsx(FieldLabel, { htmlFor: "stage-order", children: messages.stage.fields.orderIndex }), _jsx(Input, { id: "stage-order", type: "number", value: form.orderIndex, onChange: (e) => setField("orderIndex", Number(e.target.value)) })] })] }), _jsxs(FieldSet, { children: [_jsx(FieldLegend, { variant: "label", children: messages.stage.fields.anchor }), _jsx(FieldDescription, { children: "When the eligibility window opens, relative to the chosen anchor." }), _jsxs(FieldGroup, { className: "gap-4", children: [_jsx(Field, { children: _jsxs(Select, { value: form.anchor, onValueChange: (v) => setField("anchor", v), children: [_jsx(SelectTrigger, { children: _jsx(SelectValue, {}) }), _jsx(SelectContent, { children: ANCHORS.map((a) => (_jsx(SelectItem, { value: a, children: messages.stage.anchors[a] }, a))) })] }) }), _jsxs("div", { className: "grid grid-cols-1 gap-4 md:grid-cols-2", children: [_jsxs(Field, { children: [_jsx(FieldLabel, { htmlFor: "window-start", children: messages.stage.fields.windowStartDays }), _jsx(Input, { id: "window-start", type: "number", value: form.windowStartDays, onChange: (e) => setField("windowStartDays", Number(e.target.value)) })] }), _jsxs(Field, { children: [_jsx(FieldLabel, { htmlFor: "window-end", children: messages.stage.fields.windowEndDays }), _jsx(Input, { id: "window-end", type: "number", value: form.windowEndDays, onChange: (e) => setField("windowEndDays", Number(e.target.value)) })] })] })] })] }), _jsxs(FieldSet, { children: [_jsx(FieldLegend, { variant: "label", children: messages.stage.fields.cadenceKind }), _jsx(FieldDescription, { children: "How often this stage may fire while inside the window." }), _jsxs(FieldGroup, { className: "gap-4", children: [_jsx(Field, { children: _jsxs(Select, { value: form.cadenceKind, onValueChange: (v) => setField("cadenceKind", v), children: [_jsx(SelectTrigger, { children: _jsx(SelectValue, {}) }), _jsx(SelectContent, { children: CADENCES.map((c) => (_jsx(SelectItem, { value: c, children: messages.stage.cadences[c] }, c))) })] }) }), form.cadenceKind === "every_n_days" && (_jsxs(Field, { children: [_jsx(FieldLabel, { htmlFor: "cadence-every", children: messages.stage.fields.cadenceEveryDays }), _jsx(Input, { id: "cadence-every", type: "number", min: 1, value: form.cadenceEveryDays ?? "", onChange: (e) => setField("cadenceEveryDays", e.target.value ? Number(e.target.value) : null) })] })), form.cadenceKind === "escalating" && (_jsxs(Field, { children: [_jsxs("div", { className: "flex items-center justify-between", children: [_jsx(FieldLabel, { children: messages.stage.fields.cadenceIntervals }), _jsxs(Button, { type: "button", size: "sm", variant: "outline", onClick: addInterval, children: [_jsx(Plus, { className: "size-4" }), messages.stage.intervalRow.addInterval] })] }), form.cadenceIntervals.length === 0 ? (_jsx(FieldDescription, { children: "Add buckets keyed on days-until-due to scale cadence as the deadline approaches." })) : null, _jsx("div", { className: "space-y-2", children: form.cadenceIntervals.map((interval) => (_jsxs("div", { className: "grid grid-cols-[1fr_1fr_auto] items-end gap-2", children: [_jsxs(Field, { children: [_jsx(FieldLabel, { className: "text-xs", children: messages.stage.intervalRow.whenDaysUntilDueGT }), _jsx(Input, { type: "number", value: interval.whenDaysUntilDueGT ?? "", onChange: (e) => setField("cadenceIntervals", form.cadenceIntervals.map((row) => row.rowKey === interval.rowKey
95
+ ? {
96
+ ...row,
97
+ whenDaysUntilDueGT: e.target.value
98
+ ? Number(e.target.value)
99
+ : null,
100
+ }
101
+ : row)) })] }), _jsxs(Field, { children: [_jsx(FieldLabel, { className: "text-xs", children: messages.stage.intervalRow.repeatEveryDays }), _jsx(Input, { type: "number", min: 1, value: interval.repeatEveryDays, onChange: (e) => setField("cadenceIntervals", form.cadenceIntervals.map((row) => row.rowKey === interval.rowKey
102
+ ? { ...row, repeatEveryDays: Number(e.target.value) }
103
+ : row)) })] }), _jsx(Button, { type: "button", size: "icon", variant: "ghost", onClick: () => removeInterval(interval.rowKey), "aria-label": messages.stage.intervalRow.removeInterval, children: _jsx(Trash2, { className: "size-4" }) })] }, interval.rowKey))) })] }))] })] }), _jsxs(FieldSet, { children: [_jsx(FieldLegend, { variant: "label", children: "Stop conditions" }), _jsxs(FieldGroup, { className: "gap-4", children: [_jsxs(Field, { children: [_jsx(FieldLabel, { htmlFor: "max-sends", children: messages.stage.fields.maxSendsInStage }), _jsx(Input, { id: "max-sends", type: "number", min: 1, value: form.maxSendsInStage ?? "", onChange: (e) => setField("maxSendsInStage", e.target.value ? Number(e.target.value) : null), placeholder: messages.common.optionalPlaceholder }), _jsx(FieldDescription, { children: "Leave blank for no limit. When reached, the next stage takes over." })] }), _jsxs(Field, { orientation: "horizontal", children: [_jsx(Switch, { id: "respect-quiet-hours", checked: form.respectQuietHours, onCheckedChange: (v) => setField("respectQuietHours", Boolean(v)) }), _jsxs(FieldLabel, { htmlFor: "respect-quiet-hours", className: "!w-auto !flex-row", children: [_jsx(FieldTitle, { children: messages.stage.fields.respectQuietHours }), _jsx(FieldDescription, { children: "Defer fires that would land inside the tenant's quiet-hours window." })] })] })] })] })] }) }), _jsxs(DialogFooter, { children: [_jsx(Button, { variant: "outline", onClick: () => onOpenChange(false), disabled: isPending, children: messages.common.cancel }), _jsxs(Button, { onClick: handleSubmit, disabled: isPending, children: [isPending ? _jsx(Loader2, { className: "size-4 animate-spin" }) : null, isEdit ? messages.common.save : messages.common.create] })] })] }) }));
104
+ }
@@ -0,0 +1,5 @@
1
+ export interface StageListProps {
2
+ reminderRuleId: string;
3
+ }
4
+ export declare function StageList({ reminderRuleId }: StageListProps): import("react/jsx-runtime").JSX.Element;
5
+ //# sourceMappingURL=stage-list.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"stage-list.d.ts","sourceRoot":"","sources":["../../src/components/stage-list.tsx"],"names":[],"mappings":"AAeA,MAAM,WAAW,cAAc;IAC7B,cAAc,EAAE,MAAM,CAAA;CACvB;AAED,wBAAgB,SAAS,CAAC,EAAE,cAAc,EAAE,EAAE,cAAc,2CAqH3D"}
@@ -0,0 +1,34 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { useReminderRuleStageMutation, useReminderRuleStages, } from "@voyantjs/notifications-react";
4
+ import { Badge, Button, Card, CardContent, CardHeader, CardTitle } from "@voyantjs/ui/components";
5
+ import { ArrowDown, ArrowUp, Pencil, Plus, Trash2 } from "lucide-react";
6
+ import { useState } from "react";
7
+ import { useNotificationsUiMessagesOrDefault } from "../i18n/index.js";
8
+ import { StageChannelList } from "./stage-channel-list.js";
9
+ import { StageEditorDialog } from "./stage-editor-dialog.js";
10
+ export function StageList({ reminderRuleId }) {
11
+ const messages = useNotificationsUiMessagesOrDefault();
12
+ const { data: stages, isLoading } = useReminderRuleStages(reminderRuleId);
13
+ const { remove, reorder } = useReminderRuleStageMutation(reminderRuleId);
14
+ const [editing, setEditing] = useState(null);
15
+ const [creating, setCreating] = useState(false);
16
+ const move = async (index, direction) => {
17
+ if (!stages)
18
+ return;
19
+ const target = index + direction;
20
+ if (target < 0 || target >= stages.length)
21
+ return;
22
+ const next = [...stages];
23
+ const [item] = next.splice(index, 1);
24
+ if (!item)
25
+ return;
26
+ next.splice(target, 0, item);
27
+ await reorder.mutateAsync({ stageIds: next.map((s) => s.id) });
28
+ };
29
+ return (_jsxs("div", { className: "space-y-4", children: [_jsxs("div", { className: "flex items-center justify-between", children: [_jsx("h3", { className: "text-base font-semibold", children: messages.stage.listHeading }), _jsxs(Button, { size: "sm", onClick: () => setCreating(true), children: [_jsx(Plus, { className: "size-4" }), " ", messages.stage.addStage] })] }), isLoading && _jsx("p", { className: "text-sm text-muted-foreground", children: messages.common.loading }), stages && stages.length === 0 && (_jsx("p", { className: "text-sm text-muted-foreground", children: messages.stage.listEmpty })), stages?.map((stage, index) => (_jsxs(Card, { children: [_jsxs(CardHeader, { className: "flex flex-row items-center justify-between space-y-0 pb-3", children: [_jsxs(CardTitle, { className: "flex items-center gap-2 text-sm font-medium", children: [_jsxs(Badge, { variant: "outline", children: ["#", stage.orderIndex] }), stage.name ?? messages.stage.placeholders.name, _jsx(Badge, { variant: "secondary", children: messages.stage.anchors[stage.anchor] }), _jsx(Badge, { children: messages.stage.cadences[stage.cadenceKind] })] }), _jsxs("div", { className: "flex items-center gap-1", children: [_jsx(Button, { size: "icon", variant: "ghost", onClick: () => move(index, -1), disabled: index === 0, children: _jsx(ArrowUp, { className: "size-4" }) }), _jsx(Button, { size: "icon", variant: "ghost", onClick: () => move(index, 1), disabled: index === stages.length - 1, children: _jsx(ArrowDown, { className: "size-4" }) }), _jsx(Button, { size: "icon", variant: "ghost", onClick: () => setEditing(stage), children: _jsx(Pencil, { className: "size-4" }) }), _jsx(Button, { size: "icon", variant: "ghost", onClick: () => {
30
+ if (window.confirm(messages.stage.deleteConfirm)) {
31
+ void remove.mutateAsync(stage.id);
32
+ }
33
+ }, children: _jsx(Trash2, { className: "size-4" }) })] })] }), _jsxs(CardContent, { className: "space-y-3 text-sm", children: [_jsxs("div", { className: "flex flex-wrap gap-x-6 gap-y-1 text-muted-foreground", children: [_jsxs("span", { children: [messages.stage.fields.windowStartDays, ": ", stage.windowStartDays] }), _jsxs("span", { children: [messages.stage.fields.windowEndDays, ": ", stage.windowEndDays] }), stage.cadenceKind === "every_n_days" && (_jsxs("span", { children: [messages.stage.fields.cadenceEveryDays, ": ", stage.cadenceEveryDays] })), stage.maxSendsInStage != null && (_jsxs("span", { children: [messages.stage.fields.maxSendsInStage, ": ", stage.maxSendsInStage] }))] }), _jsx(StageChannelList, { reminderRuleId: reminderRuleId, stageId: stage.id })] })] }, stage.id))), creating && (_jsx(StageEditorDialog, { reminderRuleId: reminderRuleId, stage: null, defaultOrderIndex: stages?.length ?? 0, open: creating, onOpenChange: setCreating })), editing && (_jsx(StageEditorDialog, { reminderRuleId: reminderRuleId, stage: editing, open: Boolean(editing), onOpenChange: (open) => !open && setEditing(null) }))] }));
34
+ }
@@ -0,0 +1,19 @@
1
+ import { type NotificationTemplateRecord } from "@voyantjs/notifications-react";
2
+ export interface TemplatePickerProps {
3
+ /** Currently selected template id (or null when nothing is picked). */
4
+ value: string | null;
5
+ onChange: (value: string | null) => void;
6
+ /** Restrict results to templates registered for this channel. */
7
+ channel?: NotificationTemplateRecord["channel"];
8
+ placeholder?: string;
9
+ emptyText?: string;
10
+ disabled?: boolean;
11
+ }
12
+ /**
13
+ * Async-friendly template picker. Searches active templates by name / slug
14
+ * filtered by the chosen channel and resolves to the template id. The
15
+ * currently selected template is fetched separately so its label keeps
16
+ * rendering even after the search list filters it out.
17
+ */
18
+ export declare function TemplatePicker({ value, onChange, channel, placeholder, emptyText, disabled, }: TemplatePickerProps): import("react/jsx-runtime").JSX.Element;
19
+ //# sourceMappingURL=template-picker.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"template-picker.d.ts","sourceRoot":"","sources":["../../src/components/template-picker.tsx"],"names":[],"mappings":"AAEA,OAAO,EACL,KAAK,0BAA0B,EAGhC,MAAM,+BAA+B,CAAA;AAItC,MAAM,WAAW,mBAAmB;IAClC,uEAAuE;IACvE,KAAK,EAAE,MAAM,GAAG,IAAI,CAAA;IACpB,QAAQ,EAAE,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,KAAK,IAAI,CAAA;IACxC,iEAAiE;IACjE,OAAO,CAAC,EAAE,0BAA0B,CAAC,SAAS,CAAC,CAAA;IAC/C,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,QAAQ,CAAC,EAAE,OAAO,CAAA;CACnB;AAED;;;;;GAKG;AACH,wBAAgB,cAAc,CAAC,EAC7B,KAAK,EACL,QAAQ,EACR,OAAO,EACP,WAAiC,EACjC,SAAiC,EACjC,QAAQ,GACT,EAAE,mBAAmB,2CA6BrB"}
@@ -0,0 +1,24 @@
1
+ "use client";
2
+ import { jsx as _jsx } from "react/jsx-runtime";
3
+ import { useNotificationTemplate, useNotificationTemplates, } from "@voyantjs/notifications-react";
4
+ import { AsyncCombobox } from "@voyantjs/ui/components/async-combobox";
5
+ import * as React from "react";
6
+ /**
7
+ * Async-friendly template picker. Searches active templates by name / slug
8
+ * filtered by the chosen channel and resolves to the template id. The
9
+ * currently selected template is fetched separately so its label keeps
10
+ * rendering even after the search list filters it out.
11
+ */
12
+ export function TemplatePicker({ value, onChange, channel, placeholder = "Search templates…", emptyText = "No templates found.", disabled, }) {
13
+ const [search, setSearch] = React.useState("");
14
+ const { data } = useNotificationTemplates({
15
+ channel,
16
+ status: "active",
17
+ search: search || undefined,
18
+ limit: 20,
19
+ offset: 0,
20
+ });
21
+ const { data: selected } = useNotificationTemplate(value ?? "", { enabled: Boolean(value) });
22
+ const items = data?.data ?? [];
23
+ return (_jsx(AsyncCombobox, { value: value, onChange: onChange, items: items, selectedItem: selected ?? null, getKey: (template) => template.id, getLabel: (template) => template.name, getSecondary: (template) => template.slug, onSearchChange: setSearch, placeholder: placeholder, emptyText: emptyText, disabled: disabled, clearable: true }));
24
+ }
@@ -0,0 +1,9 @@
1
+ export interface TimezoneComboboxProps {
2
+ value: string | null | undefined;
3
+ onChange: (value: string | null) => void;
4
+ placeholder?: string;
5
+ emptyText?: string;
6
+ disabled?: boolean;
7
+ }
8
+ export declare function TimezoneCombobox({ value, onChange, placeholder, emptyText, disabled, }: TimezoneComboboxProps): import("react/jsx-runtime").JSX.Element;
9
+ //# sourceMappingURL=timezone-combobox.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"timezone-combobox.d.ts","sourceRoot":"","sources":["../../src/components/timezone-combobox.tsx"],"names":[],"mappings":"AA6DA,MAAM,WAAW,qBAAqB;IACpC,KAAK,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,CAAA;IAChC,QAAQ,EAAE,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,KAAK,IAAI,CAAA;IACxC,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,QAAQ,CAAC,EAAE,OAAO,CAAA;CACnB;AAED,wBAAgB,gBAAgB,CAAC,EAC/B,KAAK,EACL,QAAQ,EACR,WAAiC,EACjC,SAAiC,EACjC,QAAQ,GACT,EAAE,qBAAqB,2CAwCvB"}
@@ -0,0 +1,65 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { Combobox, ComboboxCollection, ComboboxContent, ComboboxEmpty, ComboboxInput, ComboboxItem, ComboboxList, } from "@voyantjs/ui/components/combobox";
4
+ import * as React from "react";
5
+ const TIMEZONES = (() => {
6
+ const intl = Intl;
7
+ if (typeof intl.supportedValuesOf === "function") {
8
+ try {
9
+ return [...intl.supportedValuesOf("timeZone")].sort();
10
+ }
11
+ catch {
12
+ // fall through
13
+ }
14
+ }
15
+ return [
16
+ "UTC",
17
+ "Africa/Cairo",
18
+ "Africa/Johannesburg",
19
+ "Africa/Lagos",
20
+ "America/Chicago",
21
+ "America/Denver",
22
+ "America/Los_Angeles",
23
+ "America/Mexico_City",
24
+ "America/New_York",
25
+ "America/Sao_Paulo",
26
+ "Asia/Bangkok",
27
+ "Asia/Dubai",
28
+ "Asia/Hong_Kong",
29
+ "Asia/Jakarta",
30
+ "Asia/Kolkata",
31
+ "Asia/Manila",
32
+ "Asia/Seoul",
33
+ "Asia/Shanghai",
34
+ "Asia/Singapore",
35
+ "Asia/Tokyo",
36
+ "Australia/Sydney",
37
+ "Europe/Amsterdam",
38
+ "Europe/Berlin",
39
+ "Europe/Bucharest",
40
+ "Europe/Istanbul",
41
+ "Europe/Lisbon",
42
+ "Europe/London",
43
+ "Europe/Madrid",
44
+ "Europe/Moscow",
45
+ "Europe/Paris",
46
+ "Europe/Rome",
47
+ "Europe/Vienna",
48
+ "Pacific/Auckland",
49
+ ];
50
+ })();
51
+ export function TimezoneCombobox({ value, onChange, placeholder = "Search timezones…", emptyText = "No timezones found.", disabled, }) {
52
+ const [inputValue, setInputValue] = React.useState(value ?? "");
53
+ React.useEffect(() => {
54
+ setInputValue(value ?? "");
55
+ }, [value]);
56
+ return (_jsxs(Combobox, { items: TIMEZONES, value: value ?? null, inputValue: inputValue, autoHighlight: true, disabled: disabled, itemToStringLabel: (item) => item, itemToStringValue: (item) => item, onInputValueChange: (next) => {
57
+ setInputValue(next);
58
+ if (!next)
59
+ onChange(null);
60
+ }, onValueChange: (next) => {
61
+ const tz = next ?? null;
62
+ onChange(tz);
63
+ setInputValue(tz ?? "");
64
+ }, children: [_jsx(ComboboxInput, { placeholder: placeholder, showClear: Boolean(value) }), _jsxs(ComboboxContent, { children: [_jsx(ComboboxEmpty, { children: emptyText }), _jsx(ComboboxList, { children: _jsx(ComboboxCollection, { children: (tz) => (_jsx(ComboboxItem, { value: tz, children: tz }, tz)) }) })] })] }));
65
+ }
@@ -0,0 +1,3 @@
1
+ import type { NotificationsUiMessages } from "./messages.js";
2
+ export declare const notificationsUiEn: NotificationsUiMessages;
3
+ //# sourceMappingURL=en.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"en.d.ts","sourceRoot":"","sources":["../../src/i18n/en.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,eAAe,CAAA;AAE5D,eAAO,MAAM,iBAAiB,EAAE,uBA0I/B,CAAA"}