@vertebr/react 0.4.0 → 0.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (82) hide show
  1. package/dist/components/admin/AdminContent.d.ts +36 -0
  2. package/dist/components/admin/AdminContent.d.ts.map +1 -0
  3. package/dist/components/admin/AdminContent.js +107 -0
  4. package/dist/components/admin/AdminHeader.d.ts +13 -0
  5. package/dist/components/admin/AdminHeader.d.ts.map +1 -0
  6. package/dist/components/admin/AdminHeader.js +32 -0
  7. package/dist/components/admin/AdminSidebar.d.ts +25 -0
  8. package/dist/components/admin/AdminSidebar.d.ts.map +1 -0
  9. package/dist/components/admin/AdminSidebar.js +31 -0
  10. package/dist/components/admin/VertebrAdmin.d.ts +3 -18
  11. package/dist/components/admin/VertebrAdmin.d.ts.map +1 -1
  12. package/dist/components/admin/VertebrAdmin.js +130 -120
  13. package/dist/components/admin/sidebar.d.ts +3 -0
  14. package/dist/components/admin/sidebar.d.ts.map +1 -0
  15. package/dist/components/admin/sidebar.js +13 -0
  16. package/dist/components/admin/storage.d.ts +4 -0
  17. package/dist/components/admin/storage.d.ts.map +1 -0
  18. package/dist/components/admin/storage.js +24 -0
  19. package/dist/components/admin/types.d.ts +53 -0
  20. package/dist/components/admin/types.d.ts.map +1 -0
  21. package/dist/components/admin/types.js +1 -0
  22. package/dist/components/config/form/ConfigForm.d.ts +1 -1
  23. package/dist/components/config/form/ConfigForm.d.ts.map +1 -1
  24. package/dist/components/config/form/ConfigForm.js +5 -1
  25. package/dist/components/config/form/RecordConfigForm.d.ts +1 -0
  26. package/dist/components/config/form/RecordConfigForm.d.ts.map +1 -1
  27. package/dist/components/config/form/SettingsConfigForm.d.ts +2 -1
  28. package/dist/components/config/form/SettingsConfigForm.d.ts.map +1 -1
  29. package/dist/components/config/form/SettingsConfigForm.js +2 -2
  30. package/dist/components/config/form/helpers/fields.js +1 -1
  31. package/dist/components/config/form/types.d.ts +4 -0
  32. package/dist/components/config/form/types.d.ts.map +1 -1
  33. package/dist/components/emails/EmailRecipientSelect.d.ts +16 -0
  34. package/dist/components/emails/EmailRecipientSelect.d.ts.map +1 -0
  35. package/dist/components/emails/EmailRecipientSelect.js +10 -0
  36. package/dist/components/emails/EmailsActivityPanel.d.ts +11 -0
  37. package/dist/components/emails/EmailsActivityPanel.d.ts.map +1 -0
  38. package/dist/components/emails/EmailsActivityPanel.js +139 -0
  39. package/dist/components/emails/EmailsBoard.d.ts +47 -0
  40. package/dist/components/emails/EmailsBoard.d.ts.map +1 -0
  41. package/dist/components/emails/EmailsBoard.js +594 -0
  42. package/dist/components/emails/EmailsLibraryPanel.d.ts +25 -0
  43. package/dist/components/emails/EmailsLibraryPanel.d.ts.map +1 -0
  44. package/dist/components/emails/EmailsLibraryPanel.js +379 -0
  45. package/dist/components/emails/EmailsNavSelect.d.ts +18 -0
  46. package/dist/components/emails/EmailsNavSelect.d.ts.map +1 -0
  47. package/dist/components/emails/EmailsNavSelect.js +37 -0
  48. package/dist/components/emails/MultiCheckSelect.d.ts +18 -0
  49. package/dist/components/emails/MultiCheckSelect.d.ts.map +1 -0
  50. package/dist/components/emails/MultiCheckSelect.js +74 -0
  51. package/dist/components/metrics/MetricsBoard.d.ts +8 -2
  52. package/dist/components/metrics/MetricsBoard.d.ts.map +1 -1
  53. package/dist/components/metrics/MetricsBoard.js +55 -2
  54. package/dist/components/metrics/charts.d.ts +14 -0
  55. package/dist/components/metrics/charts.d.ts.map +1 -0
  56. package/dist/components/metrics/charts.js +212 -0
  57. package/dist/components/resources/ResourceActionModal.d.ts +14 -0
  58. package/dist/components/resources/ResourceActionModal.d.ts.map +1 -0
  59. package/dist/components/resources/ResourceActionModal.js +64 -0
  60. package/dist/components/resources/ResourceCell.d.ts.map +1 -1
  61. package/dist/components/resources/ResourceCell.js +25 -9
  62. package/dist/components/resources/ResourceMailModal.d.ts +12 -0
  63. package/dist/components/resources/ResourceMailModal.d.ts.map +1 -0
  64. package/dist/components/resources/ResourceMailModal.js +196 -0
  65. package/dist/components/resources/ResourceTable.d.ts +7 -3
  66. package/dist/components/resources/ResourceTable.d.ts.map +1 -1
  67. package/dist/components/resources/ResourceTable.js +204 -34
  68. package/dist/index.d.ts +7 -1
  69. package/dist/index.d.ts.map +1 -1
  70. package/dist/index.js +1 -0
  71. package/dist/lib/metricsRanges.d.ts +20 -0
  72. package/dist/lib/metricsRanges.d.ts.map +1 -0
  73. package/dist/lib/metricsRanges.js +14 -0
  74. package/dist/styles/cells.css +42 -0
  75. package/dist/styles/emails.css +632 -0
  76. package/dist/styles/index.css +3 -0
  77. package/dist/styles/mail.css +240 -0
  78. package/dist/styles/metrics.css +633 -0
  79. package/dist/styles/nav.css +103 -1
  80. package/dist/styles/panel.css +2 -68
  81. package/dist/styles/table.css +26 -3
  82. package/package.json +1 -1
@@ -0,0 +1,379 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { useCallback, useEffect, useMemo, useState } from "react";
3
+ import { EmailsNavSelect } from "./EmailsNavSelect";
4
+ import { MultiCheckSelect } from "./MultiCheckSelect";
5
+ const FREEFORM_PRESETS = new Set(["custom", "promo", "custom_discount"]);
6
+ function humanize(value) {
7
+ return value
8
+ .replace(/([a-z])([A-Z])/g, "$1 $2")
9
+ .replace(/[_-]+/g, " ")
10
+ .trim();
11
+ }
12
+ export function systemPresetsFromTemplates(templates) {
13
+ const seen = new Set();
14
+ const out = [];
15
+ for (const template of templates) {
16
+ const raw = template.input?.preset;
17
+ if (!raw || typeof raw === "string")
18
+ continue;
19
+ if (raw.type !== "enum" || !raw.options?.length)
20
+ continue;
21
+ for (const option of raw.options) {
22
+ if (!option || FREEFORM_PRESETS.has(option) || option.startsWith("library:")) {
23
+ continue;
24
+ }
25
+ if (seen.has(option))
26
+ continue;
27
+ seen.add(option);
28
+ out.push({
29
+ id: option,
30
+ label: raw.optionLabels?.[option]?.trim() || humanize(option),
31
+ });
32
+ }
33
+ }
34
+ return out;
35
+ }
36
+ function emptyEditor(from) {
37
+ return {
38
+ id: from?.id ?? `lib_${crypto.randomUUID().replace(/-/g, "").slice(0, 12)}`,
39
+ label: from?.label ?? "",
40
+ subject: from?.subject ?? "",
41
+ body: from?.body ?? "",
42
+ productIds: from?.productIds ?? [],
43
+ discountId: from?.discountId,
44
+ discountCode: from?.discountCode ?? "",
45
+ discountOffer: from?.discountOffer ?? "",
46
+ discountExpires: from?.discountExpires ?? "",
47
+ fromPreset: from?.fromPreset,
48
+ kind: from?.kind,
49
+ eyebrow: from?.eyebrow ?? "",
50
+ headline: from?.headline ?? "",
51
+ confirmationLabel: from?.confirmationLabel ?? "",
52
+ shippingNote: from?.shippingNote ?? "",
53
+ footer: from?.footer ?? "",
54
+ signOff: from?.signOff ?? "",
55
+ createdAt: from?.createdAt,
56
+ };
57
+ }
58
+ function kindFromPreset(presetId) {
59
+ if (/order_?confirm|receipt|invoice/i.test(presetId))
60
+ return "order";
61
+ if (/discount|offer|sale|flash/i.test(presetId))
62
+ return "discount";
63
+ if (/promo|drop|restock/i.test(presetId))
64
+ return "promo";
65
+ return "account";
66
+ }
67
+ function formatRecipients(to) {
68
+ return Array.isArray(to) ? to.join(", ") : to;
69
+ }
70
+ function previewInput(editor, saved) {
71
+ const preset = saved
72
+ ? `library:${editor.id}`
73
+ : editor.kind === "discount"
74
+ ? "custom_discount"
75
+ : editor.kind === "promo"
76
+ ? "promo"
77
+ : "custom";
78
+ return {
79
+ preset,
80
+ kind: editor.kind,
81
+ subject: editor.subject,
82
+ body: editor.body,
83
+ productIds: editor.productIds,
84
+ discountId: editor.discountId,
85
+ discountCode: editor.discountCode,
86
+ discountOffer: editor.discountOffer,
87
+ discountExpires: editor.discountExpires,
88
+ eyebrow: editor.eyebrow,
89
+ headline: editor.headline,
90
+ confirmationLabel: editor.confirmationLabel,
91
+ shippingNote: editor.shippingNote,
92
+ footer: editor.footer,
93
+ signOff: editor.signOff,
94
+ };
95
+ }
96
+ function canPreviewDraft(editor) {
97
+ if (editor.kind === "order")
98
+ return true;
99
+ if (!editor.subject.trim() || !editor.body.trim())
100
+ return false;
101
+ if (editor.kind === "discount") {
102
+ return Boolean(editor.discountCode.trim() && editor.discountOffer.trim());
103
+ }
104
+ return true;
105
+ }
106
+ export function EmailsLibraryPanel({ templates, library, products, discounts = [], canLibrary, busy, previewResourceId, previewTemplateId, onPreview, onSaveLibrary, onDeleteLibrary, }) {
107
+ const systemPresets = useMemo(() => systemPresetsFromTemplates(templates), [templates]);
108
+ const [editor, setEditor] = useState(null);
109
+ const [saving, setSaving] = useState(false);
110
+ const [error, setError] = useState(null);
111
+ const [section, setSection] = useState("all");
112
+ const [preview, setPreview] = useState(null);
113
+ const [previewing, setPreviewing] = useState(false);
114
+ const savedEditor = Boolean(editor && library.some((item) => item.id === editor.id));
115
+ const productOptions = useMemo(() => products.map((item) => ({
116
+ id: item.id,
117
+ label: item.label,
118
+ secondary: item.priceLabel,
119
+ imageUrl: item.imageUrl,
120
+ })), [products]);
121
+ const loadPreview = useCallback(async () => {
122
+ if (!editor || !onPreview || !previewResourceId || !previewTemplateId) {
123
+ setPreview(null);
124
+ return;
125
+ }
126
+ if (!canPreviewDraft(editor)) {
127
+ setPreview(null);
128
+ return;
129
+ }
130
+ setPreviewing(true);
131
+ try {
132
+ const next = await onPreview({
133
+ resourceId: previewResourceId,
134
+ templateId: previewTemplateId,
135
+ input: previewInput(editor, savedEditor),
136
+ });
137
+ setPreview(next);
138
+ }
139
+ catch {
140
+ setPreview(null);
141
+ }
142
+ finally {
143
+ setPreviewing(false);
144
+ }
145
+ }, [
146
+ editor,
147
+ onPreview,
148
+ previewResourceId,
149
+ previewTemplateId,
150
+ savedEditor,
151
+ ]);
152
+ useEffect(() => {
153
+ const timer = window.setTimeout(() => {
154
+ void loadPreview();
155
+ }, 280);
156
+ return () => window.clearTimeout(timer);
157
+ }, [loadPreview]);
158
+ const openDuplicate = (preset) => {
159
+ setError(null);
160
+ setEditor(emptyEditor({
161
+ label: `${preset.label} copy`,
162
+ fromPreset: preset.id,
163
+ kind: kindFromPreset(preset.id),
164
+ }));
165
+ };
166
+ const openEdit = (item) => {
167
+ setError(null);
168
+ setEditor(emptyEditor({
169
+ id: item.id,
170
+ label: item.label,
171
+ subject: item.subject,
172
+ body: item.body,
173
+ productIds: item.productIds ?? [],
174
+ discountId: item.discountId,
175
+ discountCode: item.discountCode ?? "",
176
+ discountOffer: item.discountOffer ?? "",
177
+ discountExpires: item.discountExpires ?? "",
178
+ fromPreset: item.fromPreset,
179
+ kind: item.kind,
180
+ eyebrow: item.eyebrow ?? "",
181
+ headline: item.headline ?? "",
182
+ confirmationLabel: item.confirmationLabel ?? "",
183
+ shippingNote: item.shippingNote ?? "",
184
+ footer: item.footer ?? "",
185
+ signOff: item.signOff ?? "",
186
+ createdAt: item.createdAt,
187
+ }));
188
+ };
189
+ const openCreate = () => {
190
+ setError(null);
191
+ setEditor(emptyEditor({ kind: "promo" }));
192
+ };
193
+ const save = async () => {
194
+ if (!editor || !onSaveLibrary)
195
+ return;
196
+ const label = editor.label.trim();
197
+ const subject = editor.subject.trim();
198
+ const body = editor.body.trim();
199
+ if (!label || !subject || !body) {
200
+ setError("Label, subject, and body are required.");
201
+ return;
202
+ }
203
+ if (editor.kind === "discount" &&
204
+ (!editor.discountCode.trim() || !editor.discountOffer.trim())) {
205
+ setError("Discount templates need a code and offer label.");
206
+ return;
207
+ }
208
+ setSaving(true);
209
+ setError(null);
210
+ try {
211
+ const now = new Date().toISOString();
212
+ await onSaveLibrary({
213
+ id: editor.id,
214
+ label,
215
+ subject,
216
+ body,
217
+ productIds: editor.productIds.length ? editor.productIds : undefined,
218
+ discountId: editor.kind === "discount" ?
219
+ editor.discountId || undefined :
220
+ undefined,
221
+ discountCode: editor.kind === "discount" ?
222
+ editor.discountCode.trim() || undefined :
223
+ undefined,
224
+ discountOffer: editor.kind === "discount" ?
225
+ editor.discountOffer.trim() || undefined :
226
+ undefined,
227
+ discountExpires: editor.kind === "discount" ?
228
+ editor.discountExpires.trim() || undefined :
229
+ undefined,
230
+ fromPreset: editor.fromPreset,
231
+ kind: editor.kind,
232
+ eyebrow: editor.kind === "order" ?
233
+ editor.eyebrow.trim() || undefined :
234
+ undefined,
235
+ headline: editor.kind === "order" ?
236
+ editor.headline.trim() || undefined :
237
+ undefined,
238
+ confirmationLabel: editor.kind === "order" ?
239
+ editor.confirmationLabel.trim() || undefined :
240
+ undefined,
241
+ shippingNote: editor.kind === "order" ?
242
+ editor.shippingNote.trim() || undefined :
243
+ undefined,
244
+ footer: editor.kind === "order" ?
245
+ editor.footer.trim() || undefined :
246
+ undefined,
247
+ signOff: editor.kind === "order" ?
248
+ editor.signOff.trim() || undefined :
249
+ undefined,
250
+ createdAt: editor.createdAt ?? now,
251
+ updatedAt: now,
252
+ });
253
+ setEditor(null);
254
+ }
255
+ catch (err) {
256
+ setError(err instanceof Error ? err.message : "Save failed");
257
+ }
258
+ finally {
259
+ setSaving(false);
260
+ }
261
+ };
262
+ const remove = async (item) => {
263
+ if (!onDeleteLibrary)
264
+ return;
265
+ if (!window.confirm(`Delete template “${item.label}”?`))
266
+ return;
267
+ setSaving(true);
268
+ setError(null);
269
+ try {
270
+ await onDeleteLibrary(item.id);
271
+ if (editor?.id === item.id)
272
+ setEditor(null);
273
+ }
274
+ catch (err) {
275
+ setError(err instanceof Error ? err.message : "Delete failed");
276
+ }
277
+ finally {
278
+ setSaving(false);
279
+ }
280
+ };
281
+ const disabled = busy || saving || !canLibrary || !onSaveLibrary;
282
+ return (_jsx("div", { className: "vertebr-emails-library", "data-editing": editor ? "true" : undefined, children: editor ? (_jsxs("div", { className: "vertebr-emails-library-editor", children: [_jsxs("header", { children: [_jsx("h3", { children: savedEditor ? "Edit template" : "Save as custom" }), _jsx("button", { type: "button", className: "vertebr-btn vertebr-btn-quiet", disabled: saving, onClick: () => {
283
+ setEditor(null);
284
+ setPreview(null);
285
+ }, children: "Close" })] }), _jsxs("div", { className: "vertebr-emails-layout", children: [_jsxs("div", { className: "vertebr-emails-compose", children: [_jsxs("div", { className: "vertebr-emails-fields", children: [_jsxs("label", { className: "vertebr-mail-field", children: [_jsx("span", { children: "Label" }), _jsx("input", { type: "text", value: editor.label, disabled: saving, onChange: (event) => setEditor((current) => current
286
+ ? { ...current, label: event.target.value }
287
+ : current) })] }), _jsx("div", { className: "vertebr-emails-segment", role: "group", "aria-label": "Kind", children: [
288
+ ["account", "Account"],
289
+ ["promo", "Promo"],
290
+ ["discount", "Discount"],
291
+ ["order", "Order"],
292
+ ].map(([id, label]) => (_jsx("button", { type: "button", "data-active": editor.kind === id ? "true" : undefined, disabled: saving || editor.kind === "order", onClick: () => setEditor((current) => current ? { ...current, kind: id } : current), children: label }, id))) }), _jsxs("label", { className: "vertebr-mail-field", children: [_jsx("span", { children: "Subject" }), _jsx("input", { type: "text", value: editor.subject, disabled: saving, onChange: (event) => setEditor((current) => current
293
+ ? { ...current, subject: event.target.value }
294
+ : current) })] }), _jsxs("label", { className: "vertebr-mail-field", children: [_jsx("span", { children: editor.kind === "order" ? "Intro" : "Body" }), _jsx("textarea", { rows: editor.kind === "order" ? 4 : 7, value: editor.body, disabled: saving, onChange: (event) => setEditor((current) => current
295
+ ? { ...current, body: event.target.value }
296
+ : current) })] }), editor.kind === "order" ? (_jsxs(_Fragment, { children: [_jsxs("label", { className: "vertebr-mail-field", children: [_jsx("span", { children: "Eyebrow" }), _jsx("input", { type: "text", value: editor.eyebrow, disabled: saving, onChange: (event) => setEditor((current) => current
297
+ ? { ...current, eyebrow: event.target.value }
298
+ : current) })] }), _jsxs("label", { className: "vertebr-mail-field", children: [_jsx("span", { children: "Headline" }), _jsx("input", { type: "text", value: editor.headline, disabled: saving, onChange: (event) => setEditor((current) => current
299
+ ? { ...current, headline: event.target.value }
300
+ : current) })] }), _jsxs("label", { className: "vertebr-mail-field", children: [_jsx("span", { children: "Confirmation label" }), _jsx("input", { type: "text", value: editor.confirmationLabel, disabled: saving, onChange: (event) => setEditor((current) => current
301
+ ? {
302
+ ...current,
303
+ confirmationLabel: event.target.value,
304
+ }
305
+ : current) })] }), _jsxs("label", { className: "vertebr-mail-field", children: [_jsx("span", { children: "Shipping note" }), _jsx("textarea", { rows: 2, value: editor.shippingNote, disabled: saving, onChange: (event) => setEditor((current) => current
306
+ ? {
307
+ ...current,
308
+ shippingNote: event.target.value,
309
+ }
310
+ : current) })] }), _jsxs("label", { className: "vertebr-mail-field", children: [_jsx("span", { children: "Footer" }), _jsx("textarea", { rows: 2, value: editor.footer, disabled: saving, onChange: (event) => setEditor((current) => current
311
+ ? { ...current, footer: event.target.value }
312
+ : current) })] }), _jsxs("label", { className: "vertebr-mail-field", children: [_jsx("span", { children: "Sign-off" }), _jsx("input", { type: "text", value: editor.signOff, disabled: saving, onChange: (event) => setEditor((current) => current
313
+ ? { ...current, signOff: event.target.value }
314
+ : current) })] }), _jsxs("p", { className: "vertebr-mail-compose-hint", children: ["Tokens: ", "{{confirmationNumber}}", ", ", "{{email}}", ",", " ", "{{total}}"] })] })) : null, editor.kind === "discount" ? (_jsxs(_Fragment, { children: [discounts.length > 0 ? (_jsxs("label", { className: "vertebr-mail-field", children: [_jsx("span", { children: "Discount" }), _jsxs("select", { value: editor.discountId ?? "", disabled: saving, onChange: (event) => {
315
+ const id = event.target.value;
316
+ const hit = discounts.find((item) => item.id === id);
317
+ setEditor((current) => current
318
+ ? {
319
+ ...current,
320
+ discountId: id || undefined,
321
+ discountCode: hit?.code ?? current.discountCode,
322
+ discountOffer: hit?.offerLabel ??
323
+ hit?.label ??
324
+ current.discountOffer,
325
+ discountExpires: hit?.expiresLabel ??
326
+ current.discountExpires,
327
+ }
328
+ : current);
329
+ }, children: [_jsx("option", { value: "", children: "Manual code\u2026" }), discounts.map((item) => (_jsxs("option", { value: item.id, children: [item.label, item.code ? ` (${item.code})` : ""] }, item.id)))] })] })) : null, _jsxs("label", { className: "vertebr-mail-field", children: [_jsx("span", { children: "Discount code" }), _jsx("input", { type: "text", value: editor.discountCode, disabled: saving, placeholder: "WELCOME20", onChange: (event) => setEditor((current) => current
330
+ ? {
331
+ ...current,
332
+ discountCode: event.target.value,
333
+ }
334
+ : current) })] }), _jsxs("label", { className: "vertebr-mail-field", children: [_jsx("span", { children: "Offer label" }), _jsx("input", { type: "text", value: editor.discountOffer, disabled: saving, placeholder: "20% off", onChange: (event) => setEditor((current) => current
335
+ ? {
336
+ ...current,
337
+ discountOffer: event.target.value,
338
+ }
339
+ : current) })] }), _jsxs("label", { className: "vertebr-mail-field", children: [_jsx("span", { children: "Expires (optional)" }), _jsx("input", { type: "text", value: editor.discountExpires, disabled: saving, placeholder: "Expires Mar 1", onChange: (event) => setEditor((current) => current
340
+ ? {
341
+ ...current,
342
+ discountExpires: event.target.value,
343
+ }
344
+ : current) })] })] })) : null, productOptions.length > 0 ? (_jsxs("label", { className: "vertebr-mail-field", children: [_jsx("span", { children: "Products" }), _jsx(MultiCheckSelect, { options: productOptions, value: editor.productIds, disabled: saving, placeholder: "Add products with photos\u2026", emptyLabel: "No products", onChange: (next) => setEditor((current) => current ? { ...current, productIds: next } : current) })] })) : null, editor.fromPreset ? (_jsxs("p", { className: "vertebr-emails-library-meta", children: ["From system preset ", _jsx("code", { children: editor.fromPreset })] })) : null, error ? (_jsx("p", { className: "vertebr-mail-compose-error", children: error })) : null] }), _jsx("div", { className: "vertebr-mail-compose-foot", children: _jsx("button", { type: "button", className: "vertebr-btn vertebr-btn-primary", disabled: saving || !onSaveLibrary, onClick: () => void save(), children: saving ? "Saving…" : "Save template" }) })] }), _jsx("div", { className: "vertebr-emails-side", children: _jsxs("div", { className: "vertebr-mail-preview", children: [_jsxs("div", { className: "vertebr-mail-preview-meta", children: [_jsxs("p", { children: [_jsx("span", { children: "To" }), _jsx("strong", { children: preview
345
+ ? formatRecipients(preview.to)
346
+ : previewing
347
+ ? "Updating…"
348
+ : "Sample recipient" })] }), _jsxs("p", { children: [_jsx("span", { children: "Subject" }), _jsx("strong", { children: preview?.subject ??
349
+ (previewing ? "Updating…" : editor.subject || "—") })] })] }), _jsx("div", { className: "vertebr-mail-preview-stage", children: _jsx("div", { className: "vertebr-mail-preview-frame", children: preview?.html ? (_jsx("iframe", { title: "Email preview", sandbox: "", srcDoc: preview.html })) : preview?.text ? (_jsx("pre", { children: preview.text })) : (_jsx("div", { className: "vertebr-mail-preview-empty", children: previewing
350
+ ? "Rendering host template…"
351
+ : !previewResourceId || !previewTemplateId
352
+ ? "No sample audience to preview against."
353
+ : editor.kind === "discount" &&
354
+ (!editor.discountCode.trim() ||
355
+ !editor.discountOffer.trim())
356
+ ? "Add a discount code and offer to preview."
357
+ : "Write a subject and body to preview." })) }) })] }) })] })] })) : (_jsxs(_Fragment, { children: [_jsxs("div", { className: "vertebr-emails-subnav", children: [_jsx(EmailsNavSelect, { size: "sm", ariaLabel: "Templates subsection", value: section, onChange: setSection, options: [
358
+ {
359
+ id: "all",
360
+ label: "All templates",
361
+ count: systemPresets.length + library.length,
362
+ hint: "Presets and saved",
363
+ },
364
+ {
365
+ id: "system",
366
+ label: "System presets",
367
+ count: systemPresets.length,
368
+ hint: "Built-in starting points",
369
+ },
370
+ {
371
+ id: "saved",
372
+ label: "Saved templates",
373
+ count: library.length,
374
+ hint: "Owner-edited copies",
375
+ },
376
+ ] }), canLibrary && onSaveLibrary && section !== "system" ? (_jsx("button", { type: "button", className: "vertebr-btn vertebr-btn-quiet", disabled: disabled, onClick: openCreate, children: "New" })) : null] }), _jsxs("div", { className: "vertebr-emails-library-cols", "data-section": section, children: [section !== "saved" ? (_jsxs("section", { className: "vertebr-emails-library-section", children: [_jsxs("header", { children: [_jsx("h3", { children: "System presets" }), _jsx("span", { className: "vertebr-count", children: systemPresets.length })] }), systemPresets.length === 0 ? (_jsx("p", { className: "vertebr-emails-campaigns-empty", children: "No system presets exposed on template inputs." })) : (_jsx("ul", { children: systemPresets.map((preset) => (_jsxs("li", { children: [_jsxs("div", { children: [_jsx("strong", { children: preset.label }), _jsx("span", { children: preset.id })] }), canLibrary && onSaveLibrary ? (_jsx("button", { type: "button", className: "vertebr-btn vertebr-btn-quiet", disabled: disabled, onClick: () => openDuplicate(preset), children: "Duplicate" })) : null] }, preset.id))) }))] })) : null, section !== "system" ? (_jsxs("section", { className: "vertebr-emails-library-section", children: [_jsxs("header", { children: [_jsx("h3", { children: "Saved templates" }), _jsx("span", { className: "vertebr-count", children: library.length })] }), !canLibrary ? (_jsx("p", { className: "vertebr-emails-campaigns-empty", children: "Host has not enabled the template library." })) : library.length === 0 ? (_jsx("p", { className: "vertebr-emails-campaigns-empty", children: "No saved templates yet. Duplicate a system preset or create one." })) : (_jsx("ul", { children: library.map((item) => (_jsxs("li", { children: [_jsxs("div", { children: [_jsx("strong", { children: item.label }), _jsxs("span", { children: [item.fromPreset ? `from ${item.fromPreset}` : "custom", item.kind === "discount" ? " · discount" : "", item.kind === "order" ? " · order" : "", item.discountCode ? ` · ${item.discountCode}` : "", item.productIds?.length
377
+ ? ` · ${item.productIds.length} products`
378
+ : ""] }), item.subject ? (_jsx("span", { className: "vertebr-emails-campaign-subject", children: item.subject })) : null] }), _jsxs("div", { className: "vertebr-emails-library-actions", children: [_jsx("button", { type: "button", className: "vertebr-btn vertebr-btn-quiet", disabled: disabled, onClick: () => openEdit(item), children: "Edit" }), onDeleteLibrary && item.kind !== "order" ? (_jsx("button", { type: "button", className: "vertebr-btn vertebr-btn-quiet", disabled: disabled, onClick: () => void remove(item), children: "Delete" })) : null] })] }, item.id))) }))] })) : null] })] })) }));
379
+ }
@@ -0,0 +1,18 @@
1
+ export type EmailsNavOption<T extends string> = {
2
+ id: T;
3
+ label: string;
4
+ count?: number;
5
+ hint?: string;
6
+ };
7
+ export type EmailsNavSelectProps<T extends string> = {
8
+ value: T;
9
+ options: EmailsNavOption<T>[];
10
+ onChange: (next: T) => void;
11
+ ariaLabel: string;
12
+ size?: "md" | "sm";
13
+ };
14
+ /**
15
+ * Compact dropdown used for Emails board sections / subsections.
16
+ */
17
+ export declare function EmailsNavSelect<T extends string>({ value, options, onChange, ariaLabel, size, }: EmailsNavSelectProps<T>): import("react").JSX.Element;
18
+ //# sourceMappingURL=EmailsNavSelect.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"EmailsNavSelect.d.ts","sourceRoot":"","sources":["../../../src/components/emails/EmailsNavSelect.tsx"],"names":[],"mappings":"AAEA,MAAM,MAAM,eAAe,CAAC,CAAC,SAAS,MAAM,IAAI;IAC9C,EAAE,EAAE,CAAC,CAAC;IACN,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,IAAI,CAAC,EAAE,MAAM,CAAC;CACf,CAAC;AAEF,MAAM,MAAM,oBAAoB,CAAC,CAAC,SAAS,MAAM,IAAI;IACnD,KAAK,EAAE,CAAC,CAAC;IACT,OAAO,EAAE,eAAe,CAAC,CAAC,CAAC,EAAE,CAAC;IAC9B,QAAQ,EAAE,CAAC,IAAI,EAAE,CAAC,KAAK,IAAI,CAAC;IAC5B,SAAS,EAAE,MAAM,CAAC;IAClB,IAAI,CAAC,EAAE,IAAI,GAAG,IAAI,CAAC;CACpB,CAAC;AAEF;;GAEG;AACH,wBAAgB,eAAe,CAAC,CAAC,SAAS,MAAM,EAAE,EAChD,KAAK,EACL,OAAO,EACP,QAAQ,EACR,SAAS,EACT,IAAW,GACZ,EAAE,oBAAoB,CAAC,CAAC,CAAC,+BA6FzB"}
@@ -0,0 +1,37 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useEffect, useId, useRef, useState } from "react";
3
+ /**
4
+ * Compact dropdown used for Emails board sections / subsections.
5
+ */
6
+ export function EmailsNavSelect({ value, options, onChange, ariaLabel, size = "md", }) {
7
+ const [open, setOpen] = useState(false);
8
+ const rootRef = useRef(null);
9
+ const listId = useId();
10
+ const current = options.find((option) => option.id === value) ?? options[0];
11
+ useEffect(() => {
12
+ if (!open)
13
+ return;
14
+ const onPointer = (event) => {
15
+ if (!rootRef.current?.contains(event.target)) {
16
+ setOpen(false);
17
+ }
18
+ };
19
+ const onKey = (event) => {
20
+ if (event.key === "Escape")
21
+ setOpen(false);
22
+ };
23
+ document.addEventListener("mousedown", onPointer);
24
+ document.addEventListener("keydown", onKey);
25
+ return () => {
26
+ document.removeEventListener("mousedown", onPointer);
27
+ document.removeEventListener("keydown", onKey);
28
+ };
29
+ }, [open]);
30
+ return (_jsxs("div", { className: "vertebr-emails-nav", "data-size": size, "data-open": open || undefined, ref: rootRef, children: [_jsxs("button", { type: "button", className: "vertebr-emails-nav-trigger", "aria-label": ariaLabel, "aria-haspopup": "listbox", "aria-expanded": open, "aria-controls": listId, onClick: () => setOpen((prev) => !prev), children: [_jsxs("span", { className: "vertebr-emails-nav-current", children: [_jsx("span", { className: "vertebr-emails-nav-label", children: current?.label }), current?.count != null ? (_jsx("span", { className: "vertebr-emails-nav-count", children: current.count })) : null] }), _jsx("span", { className: "vertebr-emails-nav-chevron", "aria-hidden": "true", children: "\u25BE" })] }), open ? (_jsx("ul", { id: listId, className: "vertebr-emails-nav-menu", role: "listbox", "aria-label": ariaLabel, children: options.map((option) => {
31
+ const active = option.id === value;
32
+ return (_jsx("li", { role: "presentation", children: _jsxs("button", { type: "button", role: "option", "aria-selected": active, "data-active": active || undefined, className: "vertebr-emails-nav-option", onClick: () => {
33
+ onChange(option.id);
34
+ setOpen(false);
35
+ }, children: [_jsxs("span", { className: "vertebr-emails-nav-option-main", children: [_jsx("span", { className: "vertebr-emails-nav-label", children: option.label }), option.count != null ? (_jsx("span", { className: "vertebr-emails-nav-count", children: option.count })) : null] }), option.hint ? (_jsx("span", { className: "vertebr-emails-nav-hint", children: option.hint })) : null] }) }, option.id));
36
+ }) })) : null] }));
37
+ }
@@ -0,0 +1,18 @@
1
+ export type MultiCheckOption = {
2
+ id: string;
3
+ label: string;
4
+ secondary?: string;
5
+ imageUrl?: string;
6
+ };
7
+ export type MultiCheckSelectProps = {
8
+ options: MultiCheckOption[];
9
+ value: string[];
10
+ loading?: boolean;
11
+ disabled?: boolean;
12
+ placeholder?: string;
13
+ emptyLabel?: string;
14
+ searchPlaceholder?: string;
15
+ onChange: (ids: string[]) => void;
16
+ };
17
+ export declare function MultiCheckSelect({ options, value, loading, disabled, placeholder, emptyLabel, searchPlaceholder, onChange, }: MultiCheckSelectProps): import("react").JSX.Element;
18
+ //# sourceMappingURL=MultiCheckSelect.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"MultiCheckSelect.d.ts","sourceRoot":"","sources":["../../../src/components/emails/MultiCheckSelect.tsx"],"names":[],"mappings":"AAEA,MAAM,MAAM,gBAAgB,GAAG;IAC7B,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB,CAAC;AAEF,MAAM,MAAM,qBAAqB,GAAG;IAClC,OAAO,EAAE,gBAAgB,EAAE,CAAC;IAC5B,KAAK,EAAE,MAAM,EAAE,CAAC;IAChB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,QAAQ,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,IAAI,CAAC;CACnC,CAAC;AAMF,wBAAgB,gBAAgB,CAAC,EAC/B,OAAO,EACP,KAAK,EACL,OAAO,EACP,QAAQ,EACR,WAAuB,EACvB,UAAgC,EAChC,iBAA6B,EAC7B,QAAQ,GACT,EAAE,qBAAqB,+BA4LvB"}
@@ -0,0 +1,74 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useEffect, useId, useMemo, useRef, useState } from "react";
3
+ function normalizeId(value) {
4
+ return value.trim();
5
+ }
6
+ export function MultiCheckSelect({ options, value, loading, disabled, placeholder = "Select…", emptyLabel = "Nothing available", searchPlaceholder = "Search…", onChange, }) {
7
+ const listId = useId();
8
+ const rootRef = useRef(null);
9
+ const [open, setOpen] = useState(false);
10
+ const [query, setQuery] = useState("");
11
+ const selected = useMemo(() => new Set(value.map(normalizeId)), [value]);
12
+ const filtered = useMemo(() => {
13
+ const q = query.trim().toLowerCase();
14
+ if (!q)
15
+ return options;
16
+ return options.filter((option) => {
17
+ const hay = `${option.label} ${option.secondary ?? ""} ${option.id}`.toLowerCase();
18
+ return hay.includes(q);
19
+ });
20
+ }, [options, query]);
21
+ useEffect(() => {
22
+ if (!open)
23
+ return;
24
+ const onPointer = (event) => {
25
+ if (!rootRef.current?.contains(event.target)) {
26
+ setOpen(false);
27
+ }
28
+ };
29
+ const onKey = (event) => {
30
+ if (event.key === "Escape")
31
+ setOpen(false);
32
+ };
33
+ window.addEventListener("mousedown", onPointer);
34
+ window.addEventListener("keydown", onKey);
35
+ return () => {
36
+ window.removeEventListener("mousedown", onPointer);
37
+ window.removeEventListener("keydown", onKey);
38
+ };
39
+ }, [open]);
40
+ const toggle = (id) => {
41
+ const key = normalizeId(id);
42
+ const next = new Set(selected);
43
+ if (next.has(key))
44
+ next.delete(key);
45
+ else
46
+ next.add(key);
47
+ onChange(options
48
+ .map((option) => normalizeId(option.id))
49
+ .filter((item) => next.has(item)));
50
+ };
51
+ const selectAllFiltered = () => {
52
+ const next = new Set(selected);
53
+ for (const option of filtered) {
54
+ next.add(normalizeId(option.id));
55
+ }
56
+ onChange(options
57
+ .map((option) => normalizeId(option.id))
58
+ .filter((item) => next.has(item)));
59
+ };
60
+ const clearAll = () => onChange([]);
61
+ const summary = value.length === 0
62
+ ? loading
63
+ ? "Loading…"
64
+ : options.length === 0
65
+ ? emptyLabel
66
+ : placeholder
67
+ : value.length === 1
68
+ ? (options.find((option) => option.id === value[0])?.label ?? value[0])
69
+ : `${value.length} selected`;
70
+ return (_jsxs("div", { className: "vertebr-email-select", ref: rootRef, "data-open": open ? "true" : undefined, children: [_jsxs("button", { type: "button", className: "vertebr-select-trigger vertebr-email-select-trigger", "aria-haspopup": "listbox", "aria-expanded": open, "aria-controls": listId, disabled: disabled || loading || options.length === 0, onClick: () => setOpen((current) => !current), children: [_jsx("span", { className: "vertebr-select-value", children: summary }), _jsx("span", { className: "vertebr-select-icon", "aria-hidden": "true", children: _jsx("svg", { width: "12", height: "12", viewBox: "0 0 12 12", children: _jsx("path", { d: "M2.5 4.5 6 8l3.5-3.5", fill: "none", stroke: "currentColor", strokeWidth: "1.4", strokeLinecap: "round", strokeLinejoin: "round" }) }) })] }), open ? (_jsxs("div", { className: "vertebr-email-select-panel", id: listId, role: "listbox", children: [options.length > 6 ? (_jsxs("div", { className: "vertebr-email-select-toolbar", children: [_jsx("input", { type: "search", value: query, placeholder: searchPlaceholder, "aria-label": "Search", onChange: (event) => setQuery(event.target.value) }), _jsxs("div", { className: "vertebr-email-select-actions", children: [_jsx("button", { type: "button", onClick: selectAllFiltered, children: "All" }), _jsx("button", { type: "button", onClick: clearAll, disabled: value.length === 0, children: "Clear" })] })] })) : (_jsx("div", { className: "vertebr-email-select-toolbar vertebr-email-select-toolbar-compact", children: _jsxs("div", { className: "vertebr-email-select-actions", children: [_jsx("button", { type: "button", onClick: selectAllFiltered, children: "All" }), _jsx("button", { type: "button", onClick: clearAll, disabled: value.length === 0, children: "Clear" })] }) })), _jsx("ul", { className: "vertebr-email-select-list", children: filtered.length === 0 ? (_jsx("li", { className: "vertebr-email-select-empty", children: "No matches" })) : (filtered.map((option) => {
71
+ const on = selected.has(normalizeId(option.id));
72
+ return (_jsx("li", { children: _jsxs("label", { "data-on": on ? "true" : undefined, "data-media": option.imageUrl ? "true" : undefined, children: [_jsx("input", { type: "checkbox", checked: on, onChange: () => toggle(option.id) }), option.imageUrl ? (_jsx("img", { className: "vertebr-email-select-thumb", src: option.imageUrl, alt: "", loading: "lazy" })) : null, _jsxs("span", { className: "vertebr-email-select-copy", children: [_jsx("span", { className: "vertebr-email-select-primary", children: option.label }), option.secondary ? (_jsx("span", { className: "vertebr-email-select-secondary", children: option.secondary })) : null] })] }) }, option.id));
73
+ })) })] })) : null] }));
74
+ }
@@ -1,10 +1,16 @@
1
- import type { MetricCard } from "@vertebr/client";
1
+ import type { MetricCard, MetricChart } from "@vertebr/client";
2
+ import { type MetricsRangeId } from "../../lib/metricsRanges";
2
3
  type MetricsBoardProps = {
3
4
  label?: string;
5
+ windowLabel?: string;
6
+ compareLabel?: string;
4
7
  metrics: MetricCard[];
8
+ charts?: MetricChart[];
9
+ range?: MetricsRangeId;
10
+ onRangeChange?: (range: MetricsRangeId) => void;
5
11
  onRefresh?: () => void;
6
12
  loading?: boolean;
7
13
  };
8
- export declare function MetricsBoard({ label, metrics, onRefresh, loading, }: MetricsBoardProps): import("react").JSX.Element;
14
+ export declare function MetricsBoard({ label, windowLabel, compareLabel, metrics, charts, range, onRangeChange, onRefresh, loading, }: MetricsBoardProps): import("react").JSX.Element;
9
15
  export {};
10
16
  //# sourceMappingURL=MetricsBoard.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"MetricsBoard.d.ts","sourceRoot":"","sources":["../../../src/components/metrics/MetricsBoard.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAElD,KAAK,iBAAiB,GAAG;IACvB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,UAAU,EAAE,CAAC;IACtB,SAAS,CAAC,EAAE,MAAM,IAAI,CAAC;IACvB,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB,CAAC;AAEF,wBAAgB,YAAY,CAAC,EAC3B,KAAkB,EAClB,OAAO,EACP,SAAS,EACT,OAAO,GACR,EAAE,iBAAiB,+BAqCnB"}
1
+ {"version":3,"file":"MetricsBoard.d.ts","sourceRoot":"","sources":["../../../src/components/metrics/MetricsBoard.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAC;AAE/D,OAAO,EAEL,KAAK,cAAc,EACpB,MAAM,yBAAyB,CAAC;AAGjC,KAAK,iBAAiB,GAAG;IACvB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,OAAO,EAAE,UAAU,EAAE,CAAC;IACtB,MAAM,CAAC,EAAE,WAAW,EAAE,CAAC;IACvB,KAAK,CAAC,EAAE,cAAc,CAAC;IACvB,aAAa,CAAC,EAAE,CAAC,KAAK,EAAE,cAAc,KAAK,IAAI,CAAC;IAChD,SAAS,CAAC,EAAE,MAAM,IAAI,CAAC;IACvB,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB,CAAC;AA6GF,wBAAgB,YAAY,CAAC,EAC3B,KAAkB,EAClB,WAAW,EACX,YAAY,EACZ,OAAO,EACP,MAAW,EACX,KAAK,EACL,aAAa,EACb,SAAS,EACT,OAAO,GACR,EAAE,iBAAiB,+BAsKnB"}