best-email-builder 0.2.0 → 0.4.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 (82) hide show
  1. package/CHANGELOG.md +186 -0
  2. package/README.md +309 -12
  3. package/dist/builder.js +44 -0
  4. package/dist/builder.js.map +1 -0
  5. package/dist/chunk-AT73Y7JA.js +1427 -0
  6. package/dist/chunk-AT73Y7JA.js.map +1 -0
  7. package/dist/chunk-HHLOE6OZ.js +1682 -0
  8. package/dist/chunk-HHLOE6OZ.js.map +1 -0
  9. package/dist/chunk-JNHFZXDL.js +1776 -0
  10. package/dist/chunk-JNHFZXDL.js.map +1 -0
  11. package/dist/chunk-MY6WPDT4.js +6110 -0
  12. package/dist/chunk-MY6WPDT4.js.map +1 -0
  13. package/dist/chunk-NAEGU6PR.js +538 -0
  14. package/dist/chunk-NAEGU6PR.js.map +1 -0
  15. package/dist/chunk-TGDZ4NFI.js +114 -0
  16. package/dist/chunk-TGDZ4NFI.js.map +1 -0
  17. package/dist/index.js +124 -4866
  18. package/dist/index.js.map +1 -1
  19. package/dist/library.js +31 -0
  20. package/dist/library.js.map +1 -0
  21. package/dist/server.js +554 -210
  22. package/dist/server.js.map +1 -1
  23. package/dist/styles.css +1 -1
  24. package/dist/types/blocks/shared.d.ts +20 -0
  25. package/dist/types/builder.d.ts +8 -0
  26. package/dist/types/core/EmailEditor.d.ts +72 -19
  27. package/dist/types/core/constants.d.ts +1 -1
  28. package/dist/types/core/mjmlDefaults.d.ts +13 -0
  29. package/dist/types/core/scrollbars.d.ts +13 -0
  30. package/dist/types/defaultEditor.d.ts +11 -0
  31. package/dist/types/engine/NativeAdapter.d.ts +28 -0
  32. package/dist/types/engine/canvas/CanvasView.d.ts +88 -0
  33. package/dist/types/engine/canvas/canvasStyles.d.ts +3 -0
  34. package/dist/types/engine/canvas/icons.d.ts +14 -0
  35. package/dist/types/engine/canvas/morph.d.ts +21 -0
  36. package/dist/types/engine/canvasMjml.d.ts +30 -0
  37. package/dist/types/engine/mjml.d.ts +31 -0
  38. package/dist/types/engine/model.d.ts +130 -0
  39. package/dist/types/engine/placement.d.ts +69 -0
  40. package/dist/types/engine/richText.d.ts +15 -0
  41. package/dist/types/engine/unwrapModule.d.ts +29 -0
  42. package/dist/types/features/brand/BrandButton.d.ts +4 -0
  43. package/dist/types/features/brand/brand.d.ts +108 -0
  44. package/dist/types/features/brand/extract.d.ts +61 -0
  45. package/dist/types/features/brand/index.d.ts +2 -0
  46. package/dist/types/features/brand/messages.d.ts +13 -0
  47. package/dist/types/features/brand/useBrand.d.ts +22 -0
  48. package/dist/types/features/generate/GenerateButton.d.ts +3 -0
  49. package/dist/types/features/generate/RewriteText.d.ts +5 -0
  50. package/dist/types/features/generate/SubjectIdeas.d.ts +16 -0
  51. package/dist/types/features/generate/chip.d.ts +1 -0
  52. package/dist/types/features/generate/claude.d.ts +21 -0
  53. package/dist/types/features/generate/draft.d.ts +122 -0
  54. package/dist/types/features/generate/generator.d.ts +118 -0
  55. package/dist/types/features/generate/index.d.ts +2 -0
  56. package/dist/types/features/generate/prompt.d.ts +76 -0
  57. package/dist/types/features/generate/purposes.d.ts +48 -0
  58. package/dist/types/features/generate/rewrite.d.ts +83 -0
  59. package/dist/types/features/generate/subjects.d.ts +83 -0
  60. package/dist/types/features/preview/PreviewButton.d.ts +1 -1
  61. package/dist/types/features/preview/darkMode.d.ts +48 -0
  62. package/dist/types/features/templates/gallery.d.ts +7 -0
  63. package/dist/types/index.d.ts +14 -10
  64. package/dist/types/library.d.ts +5 -0
  65. package/dist/types/server/brand.d.ts +32 -0
  66. package/dist/types/server/brandImport.d.ts +93 -0
  67. package/dist/types/server/compileMjml.d.ts +2 -0
  68. package/dist/types/server/handler.d.ts +25 -0
  69. package/dist/types/server/index.d.ts +11 -0
  70. package/dist/types/storage/client.d.ts +79 -3
  71. package/dist/types/ui/EmailStudio.d.ts +42 -0
  72. package/dist/types/ui/fields.d.ts +2 -1
  73. package/dist/types/ui/library/TemplateLibrary.d.ts +41 -0
  74. package/dist/types/ui/primitives/button.d.ts +2 -2
  75. package/dist/types/ui/primitives/color-picker.d.ts +2 -1
  76. package/dist/types/ui/useLayoutWidth.d.ts +1 -1
  77. package/package.json +27 -4
  78. package/dist/chunk-MTALS5GC.js +0 -747
  79. package/dist/chunk-MTALS5GC.js.map +0 -1
  80. package/dist/types/adapters/grapesjs/GrapesJsAdapter.d.ts +0 -33
  81. package/dist/types/adapters/grapesjs/engineConfig.d.ts +0 -162
  82. package/dist/types/adapters/grapesjs/unwrapModule.d.ts +0 -18
@@ -0,0 +1,538 @@
1
+ import {
2
+ Button,
3
+ EmailPreview,
4
+ Input,
5
+ NoticeArea,
6
+ SectionLabel,
7
+ TEMPLATE_GALLERY,
8
+ UiProvider,
9
+ buildGallery,
10
+ cn,
11
+ createHttpStorage,
12
+ useBrand,
13
+ useLayoutWidth,
14
+ useUi,
15
+ withPlaceholderImages
16
+ } from "./chunk-AT73Y7JA.js";
17
+ import {
18
+ brandTemplate,
19
+ brandedSender
20
+ } from "./chunk-HHLOE6OZ.js";
21
+
22
+ // src/ui/library/TemplateLibrary.jsx
23
+ import { useCallback, useEffect, useMemo, useRef, useState } from "react";
24
+ import {
25
+ AlertCircle,
26
+ ArrowLeft,
27
+ Copy,
28
+ Download,
29
+ LayoutTemplate,
30
+ Loader2,
31
+ PencilRuler,
32
+ Plus,
33
+ RefreshCw,
34
+ Search,
35
+ Star,
36
+ Trash2,
37
+ X
38
+ } from "lucide-react";
39
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
40
+ var DATE_FORMAT = { day: "2-digit", month: "2-digit", year: "numeric" };
41
+ var DEFAULT_FAVORITES_KEY = "best-email-builder:favorites";
42
+ var matches = (query, ...fields) => {
43
+ const q = query.trim().toLowerCase();
44
+ return !q || fields.some((f) => String(f || "").toLowerCase().includes(q));
45
+ };
46
+ var codeOf = (name, fallback) => String(name || "").normalize("NFD").replace(/[̀-ͯ]/g, "").toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_+|_+$/g, "").slice(0, 48) || fallback;
47
+ function useFavorites(storageKey) {
48
+ const [favorites, setFavorites] = useState(() => {
49
+ if (!storageKey) return /* @__PURE__ */ new Set();
50
+ try {
51
+ return new Set(JSON.parse(localStorage.getItem(storageKey) || "[]"));
52
+ } catch {
53
+ return /* @__PURE__ */ new Set();
54
+ }
55
+ });
56
+ const toggle = useCallback(
57
+ (key) => setFavorites((current) => {
58
+ const next = new Set(current);
59
+ if (next.has(key)) next.delete(key);
60
+ else next.add(key);
61
+ try {
62
+ localStorage.setItem(storageKey, JSON.stringify([...next]));
63
+ } catch {
64
+ }
65
+ return next;
66
+ }),
67
+ [storageKey]
68
+ );
69
+ return { enabled: Boolean(storageKey), favorites, toggle };
70
+ }
71
+ function download(name, html) {
72
+ const blob = new Blob([html], { type: "text/html;charset=utf-8" });
73
+ const url = URL.createObjectURL(blob);
74
+ const a = document.createElement("a");
75
+ a.href = url;
76
+ a.download = `${codeOf(name, "email")}.html`;
77
+ a.click();
78
+ setTimeout(() => URL.revokeObjectURL(url), 1e3);
79
+ }
80
+ function Badge({ children }) {
81
+ return /* @__PURE__ */ jsx("span", { className: "eb:inline-flex eb:w-fit eb:shrink-0 eb:items-center eb:rounded-md eb:border eb:border-border eb:px-1.5 eb:py-0 eb:text-[10px] eb:whitespace-nowrap eb:text-foreground", children });
82
+ }
83
+ function Row({ entry, selected, favorite, onSelect, testId }) {
84
+ return /* @__PURE__ */ jsxs(
85
+ "button",
86
+ {
87
+ type: "button",
88
+ onClick: onSelect,
89
+ "aria-current": selected ? "true" : void 0,
90
+ "data-testid": testId,
91
+ className: cn(
92
+ "eb:w-full eb:rounded-lg2 eb:border eb:px-2.5 eb:py-2 eb:text-left eb:transition-colors eb:outline-none",
93
+ "eb:focus-visible:ring-[3px] eb:focus-visible:ring-ring/50",
94
+ selected ? "eb:border-primary/50 eb:bg-primary/5" : "eb:border-transparent eb:hover:bg-surface-75"
95
+ ),
96
+ children: [
97
+ /* @__PURE__ */ jsxs("span", { className: "eb:flex eb:items-center eb:gap-1.5", children: [
98
+ /* @__PURE__ */ jsx("span", { className: "eb:min-w-0 eb:flex-1 eb:truncate eb:text-[13px] eb:text-foreground", children: entry.name }),
99
+ favorite && /* @__PURE__ */ jsx(Star, { className: "eb:size-3.5 eb:shrink-0 eb:fill-current eb:text-warning", "aria-hidden": "true" })
100
+ ] }),
101
+ /* @__PURE__ */ jsx("span", { className: "eb:mt-0.5 eb:block eb:truncate eb:font-mono eb:text-[11px] eb:text-foreground-lighter", children: entry.code }),
102
+ /* @__PURE__ */ jsxs("span", { className: "eb:mt-1 eb:flex eb:flex-wrap eb:items-center eb:gap-1", children: [
103
+ /* @__PURE__ */ jsx(Badge, { children: entry.badge }),
104
+ entry.date && /* @__PURE__ */ jsx("span", { className: "eb:ml-auto eb:text-[10.5px] eb:text-foreground-muted", children: entry.date })
105
+ ] })
106
+ ]
107
+ }
108
+ );
109
+ }
110
+ function Section({ label, children, ...props }) {
111
+ return /* @__PURE__ */ jsxs("div", { className: "eb:flex eb:flex-col eb:gap-1.5", ...props, children: [
112
+ /* @__PURE__ */ jsx(SectionLabel, { children: label }),
113
+ children
114
+ ] });
115
+ }
116
+ function Library({
117
+ storage: injectedStorage,
118
+ sender,
119
+ templates: galleryProp,
120
+ title,
121
+ description,
122
+ selectedId = null,
123
+ favoritesKey = DEFAULT_FAVORITES_KEY,
124
+ onEdit,
125
+ onCreate,
126
+ onClose,
127
+ className
128
+ }) {
129
+ const { t, locale, notify, confirm } = useUi();
130
+ const storage = useMemo(() => injectedStorage || createHttpStorage(), [injectedStorage]);
131
+ const { brand } = useBrand(storage);
132
+ const bases = useMemo(
133
+ () => buildGallery({ templates: galleryProp ?? TEMPLATE_GALLERY, sender: brandedSender(sender, brand), t }).map((base2) => ({
134
+ ...base2,
135
+ mjml: brandTemplate(base2.mjml, brand, { sender, t })
136
+ })),
137
+ [galleryProp, sender, brand, t]
138
+ );
139
+ const dateFormat = useMemo(() => new Intl.DateTimeFormat(locale, DATE_FORMAT), [locale]);
140
+ const formatDate = useCallback((at) => at ? dateFormat.format(new Date(at)) : "", [dateFormat]);
141
+ const stars = useFavorites(favoritesKey);
142
+ const rootRef = useRef(null);
143
+ const searchRef = useRef(null);
144
+ const layout = useLayoutWidth(rootRef);
145
+ const [items, setItems] = useState({ status: "loading", list: [], error: null });
146
+ const [query, setQuery] = useState("");
147
+ const [selection, setSelection] = useState(selectedId != null ? `template:${selectedId}` : null);
148
+ const [detailsOpen, setDetailsOpen] = useState(false);
149
+ const [detail, setDetail] = useState({ status: "idle", template: null, error: null });
150
+ const [preview, setPreview] = useState({ html: "", loading: false, error: null });
151
+ const [device, setDevice] = useState("desktop");
152
+ const [busy, setBusy] = useState(null);
153
+ const lastRead = useRef(0);
154
+ const reload = useCallback(async () => {
155
+ const ticket = ++lastRead.current;
156
+ if (typeof storage.list !== "function") {
157
+ setItems({ status: "ready", list: [], error: null });
158
+ return;
159
+ }
160
+ setItems((s) => ({ ...s, status: s.list.length ? "ready" : "loading", error: null }));
161
+ try {
162
+ const list = await storage.list();
163
+ if (ticket === lastRead.current) setItems({ status: "ready", list, error: null });
164
+ } catch (error) {
165
+ if (ticket === lastRead.current) setItems({ status: "error", list: [], error: error.message });
166
+ }
167
+ }, [storage]);
168
+ useEffect(() => {
169
+ reload();
170
+ }, [reload]);
171
+ const templateEntries = useMemo(
172
+ () => items.list.map((item) => ({
173
+ key: `template:${item.id}`,
174
+ kind: "template",
175
+ id: item.id,
176
+ name: item.name,
177
+ code: codeOf(item.name, `#${item.id}`),
178
+ badge: t("Builder"),
179
+ date: formatDate(item.updatedAt || item.createdAt),
180
+ subject: item.subject,
181
+ item
182
+ })),
183
+ [items.list, t, formatDate]
184
+ );
185
+ const baseEntries = useMemo(
186
+ () => bases.map((base2) => ({
187
+ key: `base:${base2.id}`,
188
+ kind: "base",
189
+ id: base2.id,
190
+ name: base2.name,
191
+ code: `base_${base2.id}`,
192
+ badge: t("Base"),
193
+ date: "",
194
+ subject: base2.subject,
195
+ base: base2
196
+ })),
197
+ [bases, t]
198
+ );
199
+ const shown = (entries) => entries.filter((e) => matches(query, e.name, e.code, e.subject, e.base?.description));
200
+ const favoriteEntries = shown([...templateEntries, ...baseEntries]).filter((e) => stars.favorites.has(e.key));
201
+ const otherTemplates = shown(templateEntries).filter((e) => !stars.favorites.has(e.key));
202
+ const otherBases = shown(baseEntries).filter((e) => !stars.favorites.has(e.key));
203
+ useEffect(() => {
204
+ if (items.status !== "ready" || selection) return;
205
+ const base2 = bases.find((b) => b.mjml.includes("<mj-section")) || bases[0];
206
+ if (items.list.length) setSelection(`template:${items.list[0].id}`);
207
+ else if (base2) setSelection(`base:${base2.id}`);
208
+ }, [items, selection, bases]);
209
+ useEffect(() => {
210
+ const onKey = (event) => {
211
+ if ((event.ctrlKey || event.metaKey) && event.key.toLowerCase() === "k" && rootRef.current?.isConnected) {
212
+ event.preventDefault();
213
+ searchRef.current?.focus();
214
+ }
215
+ };
216
+ window.addEventListener("keydown", onKey);
217
+ return () => window.removeEventListener("keydown", onKey);
218
+ }, []);
219
+ const selected = templateEntries.find((e) => e.key === selection) || baseEntries.find((e) => e.key === selection) || null;
220
+ const base = selected?.kind === "base" ? selected.base : null;
221
+ const templateId = selection?.startsWith("template:") ? selection.slice("template:".length) : null;
222
+ useEffect(() => {
223
+ if (templateId == null) {
224
+ setDetail({ status: "idle", template: null, error: null });
225
+ return void 0;
226
+ }
227
+ let live = true;
228
+ setDetail((d) => ({ status: "loading", template: String(d.template?.id) === templateId ? d.template : null, error: null }));
229
+ storage.load(templateId).then(
230
+ (template2) => live && setDetail({ status: "ready", template: template2, error: null }),
231
+ (error) => live && setDetail({ status: "error", template: null, error: error.message })
232
+ );
233
+ return () => {
234
+ live = false;
235
+ };
236
+ }, [templateId, storage]);
237
+ const template = detail.template;
238
+ const source = base ? { mjml: base.mjml, subject: base.subject, preheader: base.preheader, html: "" } : template ? { mjml: template.mjml, subject: template.subject, preheader: template.preheader, html: template.html || "" } : null;
239
+ const sourceKey = base ? `base:${base.id}:${locale}` : template ? `template:${template.id}:${template.revision}` : "";
240
+ useEffect(() => {
241
+ if (!source) {
242
+ setPreview({ html: "", loading: templateId != null && detail.status === "loading", error: null });
243
+ return void 0;
244
+ }
245
+ if (typeof storage.render !== "function" || !source.mjml) {
246
+ setPreview({ html: source.html, loading: false, error: source.html ? null : t("No preview: this storage cannot render emails.") });
247
+ return void 0;
248
+ }
249
+ let live = true;
250
+ setPreview((p) => ({ ...p, loading: true, error: null }));
251
+ storage.render({ mjml: source.mjml, subject: source.subject, preheader: source.preheader }).then(
252
+ (result) => live && setPreview({ html: withPlaceholderImages(result.html), loading: false, error: null }),
253
+ (error) => live && setPreview({ html: source.html, loading: false, error: source.html ? null : error.message })
254
+ );
255
+ return () => {
256
+ live = false;
257
+ };
258
+ }, [sourceKey, storage]);
259
+ const select = (key) => {
260
+ setSelection(key);
261
+ setDetailsOpen(true);
262
+ };
263
+ const remove = async () => {
264
+ const ok = await confirm({
265
+ title: t("Delete \u201C{name}\u201D?", { name: template.name }),
266
+ description: t("The template and its versions will be deleted."),
267
+ confirmLabel: t("Delete"),
268
+ destructive: true
269
+ });
270
+ if (!ok) return;
271
+ setBusy("delete");
272
+ try {
273
+ await storage.remove(template.id);
274
+ setSelection(null);
275
+ setDetailsOpen(false);
276
+ await reload();
277
+ notify("success", t("Template deleted."));
278
+ } catch (error) {
279
+ notify("error", error.message);
280
+ } finally {
281
+ setBusy(null);
282
+ }
283
+ };
284
+ const duplicate = async () => {
285
+ setBusy("duplicate");
286
+ try {
287
+ const { template: copy } = await storage.duplicate(template.id);
288
+ await reload();
289
+ setSelection(`template:${copy.id}`);
290
+ notify("success", t("Template duplicated."));
291
+ } catch (error) {
292
+ notify("error", error.message);
293
+ } finally {
294
+ setBusy(null);
295
+ }
296
+ };
297
+ const copyHtml = async () => {
298
+ try {
299
+ await navigator.clipboard.writeText(template.html);
300
+ notify("success", t("HTML copied."));
301
+ } catch {
302
+ notify("error", t("The browser refused to copy."));
303
+ }
304
+ };
305
+ const canList = typeof storage.list === "function";
306
+ const rowProps = (entry) => ({
307
+ entry,
308
+ testId: entry.kind === "base" ? "eb-library-base" : "eb-library-template",
309
+ selected: entry.key === selection,
310
+ favorite: stars.favorites.has(entry.key),
311
+ onSelect: () => select(entry.key)
312
+ });
313
+ const listColumn = /* @__PURE__ */ jsxs("div", { className: "eb:flex eb:min-h-0 eb:min-w-0 eb:flex-col", "data-testid": "eb-library-list", children: [
314
+ /* @__PURE__ */ jsxs("div", { className: "eb:flex eb:shrink-0 eb:flex-col eb:gap-2 eb:border-b eb:border-border eb:p-3", children: [
315
+ onCreate && /* @__PURE__ */ jsxs(Button, { className: "eb:w-full", onClick: () => onCreate(null), children: [
316
+ /* @__PURE__ */ jsx(PencilRuler, {}),
317
+ t("New template")
318
+ ] }),
319
+ /* @__PURE__ */ jsxs("div", { className: "eb:relative", children: [
320
+ /* @__PURE__ */ jsx(Search, { className: "eb:pointer-events-none eb:absolute eb:top-1/2 eb:left-2.5 eb:size-3.5 eb:-translate-y-1/2 eb:text-foreground-muted" }),
321
+ /* @__PURE__ */ jsx(
322
+ Input,
323
+ {
324
+ ref: searchRef,
325
+ type: "search",
326
+ value: query,
327
+ onChange: (e) => setQuery(e.target.value),
328
+ onKeyDown: (e) => {
329
+ if (e.key === "Escape" && query) {
330
+ e.preventDefault();
331
+ setQuery("");
332
+ }
333
+ },
334
+ placeholder: t("Search templates"),
335
+ "aria-label": t("Search templates"),
336
+ "aria-keyshortcuts": "Control+K",
337
+ className: "eb:pr-16 eb:pl-8 eb:[&::-webkit-search-cancel-button]:hidden"
338
+ }
339
+ ),
340
+ /* @__PURE__ */ jsxs("div", { className: "eb:absolute eb:top-1/2 eb:right-2 eb:flex eb:-translate-y-1/2 eb:items-center eb:gap-1", children: [
341
+ query && /* @__PURE__ */ jsx("button", { type: "button", onClick: () => setQuery(""), "aria-label": t("Clear the search"), className: "eb:cursor-pointer", children: /* @__PURE__ */ jsx(X, { className: "eb:size-3.5 eb:text-foreground-muted eb:hover:text-foreground" }) }),
342
+ /* @__PURE__ */ jsx(
343
+ "kbd",
344
+ {
345
+ className: "eb:inline-flex eb:h-5 eb:items-center eb:rounded eb:border eb:border-border eb:bg-surface-100 eb:px-1.5 eb:font-mono eb:text-[10px] eb:text-foreground-muted",
346
+ "aria-hidden": "true",
347
+ children: query ? "Esc" : "Ctrl K"
348
+ }
349
+ )
350
+ ] })
351
+ ] })
352
+ ] }),
353
+ /* @__PURE__ */ jsxs("div", { className: "eb:flex eb:min-h-0 eb:flex-1 eb:flex-col eb:gap-4 eb:overflow-y-auto eb:p-3", children: [
354
+ stars.enabled && favoriteEntries.length > 0 && /* @__PURE__ */ jsx(Section, { label: t("Favorites"), role: "group", "aria-label": t("Favorites"), "data-testid": "eb-library-favorites", children: favoriteEntries.map((entry) => /* @__PURE__ */ jsx(Row, { ...rowProps(entry) }, entry.key)) }),
355
+ canList && /* @__PURE__ */ jsx(Section, { label: `${t("Templates")} (${otherTemplates.length})`, "data-testid": "eb-library-templates", children: items.status === "loading" ? /* @__PURE__ */ jsxs("p", { className: "eb:m-0 eb:flex eb:items-center eb:gap-2 eb:px-1 eb:text-[12px] eb:text-foreground-lighter", children: [
356
+ /* @__PURE__ */ jsx(Loader2, { className: "eb:size-3.5 eb:animate-spin" }),
357
+ t("Loading\u2026")
358
+ ] }) : items.status === "error" ? /* @__PURE__ */ jsxs("div", { className: "eb:px-1 eb:text-[12px] eb:text-destructive", children: [
359
+ /* @__PURE__ */ jsx("p", { className: "eb:m-0", children: t("Could not load the templates.") }),
360
+ /* @__PURE__ */ jsx("p", { className: "eb:m-0 eb:mt-0.5 eb:text-foreground-muted", children: items.error }),
361
+ /* @__PURE__ */ jsxs(Button, { variant: "outline", size: "sm-compact", className: "eb:mt-2", onClick: reload, children: [
362
+ /* @__PURE__ */ jsx(RefreshCw, {}),
363
+ t("Retry")
364
+ ] })
365
+ ] }) : otherTemplates.length === 0 ? /* @__PURE__ */ jsx("p", { className: "eb:m-0 eb:px-1 eb:text-[12px] eb:leading-relaxed eb:text-foreground-lighter", children: query ? t("Nothing matches \u201C{query}\u201D.", { query }) : templateEntries.length ? t("All your templates are in the favorites.") : t("No template yet. Start from a base, or create a new one.") }) : otherTemplates.map((entry) => /* @__PURE__ */ jsx(Row, { ...rowProps(entry) }, entry.key)) }),
366
+ otherBases.length > 0 && /* @__PURE__ */ jsx(Section, { label: `${t("Bases")} (${otherBases.length})`, "data-testid": "eb-library-bases", children: otherBases.map((entry) => /* @__PURE__ */ jsx(Row, { ...rowProps(entry) }, entry.key)) })
367
+ ] })
368
+ ] }, "list");
369
+ const meta = base ? { name: base.name, code: `base_${base.id}`, description: base.description, origin: t("Base"), subject: base.subject, preheader: base.preheader } : selected || template ? {
370
+ name: template?.name ?? selected.name,
371
+ code: selected?.code ?? codeOf(template?.name, `#${templateId}`),
372
+ description: template?.description ?? selected?.item?.description,
373
+ origin: t("Builder"),
374
+ subject: (template ?? selected?.item)?.subject,
375
+ preheader: (template ?? selected?.item)?.preheader,
376
+ createdAt: (template ?? selected?.item)?.createdAt,
377
+ createdBy: (template ?? selected?.item)?.createdBy,
378
+ updatedAt: (template ?? selected?.item)?.updatedAt,
379
+ updatedBy: (template ?? selected?.item)?.updatedBy
380
+ } : null;
381
+ const favorite = selection ? stars.favorites.has(selection) : false;
382
+ const detailsBody = templateId != null && detail.status === "error" ? /* @__PURE__ */ jsxs("p", { className: "eb:m-0 eb:flex eb:items-start eb:gap-2 eb:text-[13px] eb:text-destructive", children: [
383
+ /* @__PURE__ */ jsx(AlertCircle, { className: "eb:mt-0.5 eb:size-4 eb:shrink-0" }),
384
+ detail.error
385
+ ] }) : meta ? /* @__PURE__ */ jsxs(Fragment, { children: [
386
+ /* @__PURE__ */ jsxs("div", { className: "eb:flex eb:flex-col eb:gap-1", children: [
387
+ /* @__PURE__ */ jsxs("div", { className: "eb:flex eb:items-center eb:gap-2", children: [
388
+ /* @__PURE__ */ jsx("h3", { className: "eb:m-0 eb:min-w-0 eb:flex-1 eb:text-[15px] eb:font-medium eb:text-foreground", children: meta.name }),
389
+ stars.enabled && /* @__PURE__ */ jsx(
390
+ Button,
391
+ {
392
+ variant: "ghost",
393
+ size: "icon-sm-compact",
394
+ className: cn("eb:shrink-0", favorite ? "eb:text-warning" : "eb:text-foreground-muted eb:hover:text-warning"),
395
+ onClick: () => stars.toggle(selection),
396
+ "aria-pressed": favorite,
397
+ "aria-label": favorite ? t("Remove from favorites") : t("Add to favorites"),
398
+ title: favorite ? t("Remove from favorites") : t("Add to favorites"),
399
+ children: /* @__PURE__ */ jsx(Star, { className: cn("eb:size-4", favorite && "eb:fill-current") })
400
+ }
401
+ )
402
+ ] }),
403
+ /* @__PURE__ */ jsx("p", { className: "eb:m-0 eb:font-mono eb:text-[11.5px] eb:text-foreground-lighter", children: meta.code }),
404
+ meta.description && /* @__PURE__ */ jsx("p", { className: "eb:m-0 eb:text-[12.5px] eb:leading-relaxed eb:text-foreground-lighter", children: meta.description })
405
+ ] }),
406
+ /* @__PURE__ */ jsxs("dl", { className: "eb:m-0 eb:grid eb:grid-cols-[110px_minmax(0,1fr)] eb:gap-x-3 eb:gap-y-1.5 eb:text-[12px]", children: [
407
+ /* @__PURE__ */ jsx("dt", { className: "eb:text-foreground-muted", children: t("Origin") }),
408
+ /* @__PURE__ */ jsx("dd", { className: "eb:m-0 eb:text-foreground", children: meta.origin }),
409
+ /* @__PURE__ */ jsx("dt", { className: "eb:text-foreground-muted", children: t("Subject") }),
410
+ /* @__PURE__ */ jsx("dd", { className: "eb:m-0 eb:break-words eb:text-foreground", children: meta.subject || "\u2014" }),
411
+ /* @__PURE__ */ jsx("dt", { className: "eb:text-foreground-muted", children: t("Preview text") }),
412
+ /* @__PURE__ */ jsx("dd", { className: "eb:m-0 eb:break-words eb:text-foreground-lighter", children: meta.preheader || "\u2014" }),
413
+ !base && /* @__PURE__ */ jsxs(Fragment, { children: [
414
+ /* @__PURE__ */ jsx("dt", { className: "eb:text-foreground-muted", children: t("Created") }),
415
+ /* @__PURE__ */ jsxs("dd", { className: "eb:m-0 eb:text-foreground", children: [
416
+ formatDate(meta.createdAt) || "\u2014",
417
+ meta.createdBy ? ` ${t("by {name}", { name: meta.createdBy })}` : ""
418
+ ] }),
419
+ meta.updatedAt && /* @__PURE__ */ jsxs(Fragment, { children: [
420
+ /* @__PURE__ */ jsx("dt", { className: "eb:text-foreground-muted", children: t("Updated") }),
421
+ /* @__PURE__ */ jsxs("dd", { className: "eb:m-0 eb:text-foreground", children: [
422
+ formatDate(meta.updatedAt),
423
+ meta.updatedBy ? ` ${t("by {name}", { name: meta.updatedBy })}` : ""
424
+ ] })
425
+ ] })
426
+ ] })
427
+ ] }),
428
+ base && /* @__PURE__ */ jsx("p", { className: "eb:m-0 eb:rounded-lg2 eb:border eb:border-border eb:bg-surface-75 eb:p-2.5 eb:text-[12px] eb:leading-relaxed eb:text-foreground-lighter", children: t("Base \u2014 not editable: start from it to create your template.") })
429
+ ] }) : /* @__PURE__ */ jsx("div", { className: "eb:flex eb:flex-1 eb:items-center eb:justify-center", children: /* @__PURE__ */ jsxs("div", { className: "eb:flex eb:w-full eb:flex-col eb:items-center eb:justify-center eb:gap-2 eb:rounded-lg2 eb:border eb:border-border eb:bg-surface-75 eb:px-6 eb:py-12 eb:text-center", children: [
430
+ /* @__PURE__ */ jsx("span", { className: "eb:flex eb:size-12 eb:items-center eb:justify-center eb:rounded-full eb:bg-surface-200 eb:text-foreground-muted", children: /* @__PURE__ */ jsx(LayoutTemplate, { className: "eb:size-6" }) }),
431
+ /* @__PURE__ */ jsx("p", { className: "eb:m-0 eb:text-[13px] eb:font-medium eb:text-foreground", children: t("Choose a template") }),
432
+ /* @__PURE__ */ jsx("p", { className: "eb:m-0 eb:max-w-sm eb:text-[13px] eb:leading-relaxed eb:text-foreground-muted", children: t("Select a template on the left.") })
433
+ ] }) });
434
+ const actions = base ? onCreate && /* @__PURE__ */ jsxs(
435
+ Button,
436
+ {
437
+ className: "eb:w-full",
438
+ onClick: () => onCreate({ baseId: base.id, mjml: base.mjml, meta: { subject: base.subject, preheader: base.preheader } }),
439
+ children: [
440
+ /* @__PURE__ */ jsx(Plus, {}),
441
+ t("Start from this base")
442
+ ]
443
+ }
444
+ ) : template ? /* @__PURE__ */ jsxs(Fragment, { children: [
445
+ /* @__PURE__ */ jsxs("div", { className: "eb:flex eb:gap-2", children: [
446
+ onEdit && /* @__PURE__ */ jsxs(Button, { className: "eb:flex-1", onClick: () => onEdit(template), children: [
447
+ /* @__PURE__ */ jsx(PencilRuler, {}),
448
+ t("Edit")
449
+ ] }),
450
+ /* @__PURE__ */ jsxs(Button, { variant: "outline", className: "eb:flex-1", disabled: !!busy, onClick: duplicate, children: [
451
+ busy === "duplicate" ? /* @__PURE__ */ jsx(Loader2, { className: "eb:animate-spin" }) : /* @__PURE__ */ jsx(Copy, {}),
452
+ t("Duplicate")
453
+ ] })
454
+ ] }),
455
+ (template.html || typeof storage.remove === "function") && /* @__PURE__ */ jsxs("div", { className: "eb:flex eb:gap-2", children: [
456
+ template.html && /* @__PURE__ */ jsxs(Fragment, { children: [
457
+ /* @__PURE__ */ jsxs(Button, { variant: "outline", size: "sm-compact", className: "eb:flex-1", onClick: copyHtml, children: [
458
+ /* @__PURE__ */ jsx(Copy, {}),
459
+ t("Copy HTML")
460
+ ] }),
461
+ /* @__PURE__ */ jsxs(Button, { variant: "outline", size: "sm-compact", className: "eb:flex-1", onClick: () => download(template.name, template.html), children: [
462
+ /* @__PURE__ */ jsx(Download, {}),
463
+ t("Download")
464
+ ] })
465
+ ] }),
466
+ typeof storage.remove === "function" && /* @__PURE__ */ jsxs(
467
+ Button,
468
+ {
469
+ variant: "outline",
470
+ size: "sm-compact",
471
+ className: cn("eb:text-destructive", !template.html && "eb:ml-auto"),
472
+ disabled: !!busy,
473
+ onClick: remove,
474
+ "aria-label": t("Delete"),
475
+ title: t("Delete"),
476
+ children: [
477
+ busy === "delete" ? /* @__PURE__ */ jsx(Loader2, { className: "eb:animate-spin" }) : /* @__PURE__ */ jsx(Trash2, {}),
478
+ !template.html && t("Delete")
479
+ ]
480
+ }
481
+ )
482
+ ] })
483
+ ] }) : null;
484
+ const previewPane = preview.html || preview.loading || preview.error ? /* @__PURE__ */ jsx(EmailPreview, { html: preview.html, loading: preview.loading, error: preview.error, device, onDeviceChange: setDevice }) : /* @__PURE__ */ jsx("div", { className: "eb:flex eb:h-full eb:min-h-40 eb:items-center eb:justify-center eb:rounded-lg2 eb:border eb:border-dashed eb:border-border eb:p-6 eb:text-center eb:text-[13px] eb:text-foreground-lighter", children: t("The template preview appears here.") });
485
+ const detailsColumn = /* @__PURE__ */ jsxs("div", { className: "eb:flex eb:min-h-0 eb:min-w-0 eb:flex-col", "data-testid": "eb-library-details", children: [
486
+ layout === "narrow" && /* @__PURE__ */ jsx("div", { className: "eb:shrink-0 eb:border-b eb:border-border eb:px-2 eb:py-1.5", children: /* @__PURE__ */ jsxs(Button, { variant: "ghost", size: "sm-compact", onClick: () => setDetailsOpen(false), children: [
487
+ /* @__PURE__ */ jsx(ArrowLeft, {}),
488
+ t("All templates")
489
+ ] }) }),
490
+ /* @__PURE__ */ jsxs("div", { className: "eb:flex eb:min-h-0 eb:flex-1 eb:flex-col eb:gap-4 eb:overflow-y-auto eb:p-4", children: [
491
+ detailsBody,
492
+ layout !== "wide" && meta && /* @__PURE__ */ jsx("div", { className: "eb:h-[560px] eb:shrink-0", children: previewPane })
493
+ ] }),
494
+ actions && /* @__PURE__ */ jsx("div", { className: "eb:flex eb:shrink-0 eb:flex-col eb:gap-2 eb:border-t eb:border-border eb:p-3", children: actions })
495
+ ] }, "details");
496
+ return /* @__PURE__ */ jsxs(
497
+ "div",
498
+ {
499
+ ref: rootRef,
500
+ className: cn("eb-root eb:relative eb:flex eb:h-full eb:min-h-0 eb:flex-col eb:bg-background eb:text-foreground", className),
501
+ "data-testid": "template-library",
502
+ "data-layout": layout,
503
+ children: [
504
+ /* @__PURE__ */ jsxs("header", { className: "eb:flex eb:shrink-0 eb:items-center eb:gap-3 eb:border-b eb:border-border eb:px-5 eb:py-3.5", children: [
505
+ /* @__PURE__ */ jsx("span", { className: "eb:flex eb:size-10 eb:shrink-0 eb:items-center eb:justify-center eb:rounded-lg2 eb:bg-primary/10 eb:text-primary", children: /* @__PURE__ */ jsx(LayoutTemplate, { className: "eb:size-5" }) }),
506
+ /* @__PURE__ */ jsxs("div", { className: "eb:min-w-0 eb:flex-1", children: [
507
+ /* @__PURE__ */ jsx("h2", { className: "eb:m-0 eb:truncate eb:text-base eb:font-normal eb:text-foreground", children: title ?? t("Email template library") }),
508
+ /* @__PURE__ */ jsx("p", { className: "eb:m-0 eb:truncate eb:text-[13px] eb:text-foreground-muted", children: description ?? t("Ready-made templates to copy into your campaigns.") })
509
+ ] }),
510
+ onClose && /* @__PURE__ */ jsx(Button, { variant: "ghost", size: "icon-sm", onClick: onClose, "aria-label": t("Close"), children: /* @__PURE__ */ jsx(X, {}) })
511
+ ] }),
512
+ layout === "narrow" ? /* @__PURE__ */ jsx("div", { className: "eb:grid eb:min-h-0 eb:flex-1 eb:grid-cols-[minmax(0,1fr)]", children: detailsOpen && selection ? detailsColumn : listColumn }) : /* @__PURE__ */ jsxs(
513
+ "div",
514
+ {
515
+ className: cn(
516
+ "eb:grid eb:min-h-0 eb:flex-1 eb:grid-rows-1 eb:divide-x eb:divide-border eb:overflow-hidden",
517
+ layout === "wide" ? "eb:grid-cols-[290px_minmax(0,1fr)_minmax(0,1fr)]" : "eb:grid-cols-[260px_minmax(0,1fr)]"
518
+ ),
519
+ children: [
520
+ listColumn,
521
+ detailsColumn,
522
+ layout === "wide" && /* @__PURE__ */ jsx("div", { className: "eb:min-h-0 eb:min-w-0 eb:p-3", "data-testid": "eb-library-preview", children: previewPane })
523
+ ]
524
+ }
525
+ ),
526
+ /* @__PURE__ */ jsx(NoticeArea, {})
527
+ ]
528
+ }
529
+ );
530
+ }
531
+ function TemplateLibrary({ locale = "en", messages, onNotify, ...props }) {
532
+ return /* @__PURE__ */ jsx(UiProvider, { locale, messages, onNotify, children: /* @__PURE__ */ jsx(Library, { ...props }) });
533
+ }
534
+
535
+ export {
536
+ TemplateLibrary
537
+ };
538
+ //# sourceMappingURL=chunk-NAEGU6PR.js.map