@endora-commerce/page-builder-admin 0.100.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 (96) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +49 -0
  3. package/dist/chrome/AssetPickers.d.ts +16 -0
  4. package/dist/chrome/AssetPickers.d.ts.map +1 -0
  5. package/dist/chrome/AssetPickers.js +112 -0
  6. package/dist/chrome/AssetPickers.js.map +1 -0
  7. package/dist/chrome/CatalogPickers.d.ts +6 -0
  8. package/dist/chrome/CatalogPickers.d.ts.map +1 -0
  9. package/dist/chrome/CatalogPickers.js +310 -0
  10. package/dist/chrome/CatalogPickers.js.map +1 -0
  11. package/dist/chrome/ColorPaletteModal.d.ts +11 -0
  12. package/dist/chrome/ColorPaletteModal.d.ts.map +1 -0
  13. package/dist/chrome/ColorPaletteModal.js +82 -0
  14. package/dist/chrome/ColorPaletteModal.js.map +1 -0
  15. package/dist/chrome/ColorPaletteProvider.d.ts +7 -0
  16. package/dist/chrome/ColorPaletteProvider.d.ts.map +1 -0
  17. package/dist/chrome/ColorPaletteProvider.js +40 -0
  18. package/dist/chrome/ColorPaletteProvider.js.map +1 -0
  19. package/dist/chrome/PageBuilderHeaderActions.d.ts +52 -0
  20. package/dist/chrome/PageBuilderHeaderActions.d.ts.map +1 -0
  21. package/dist/chrome/PageBuilderHeaderActions.js +242 -0
  22. package/dist/chrome/PageBuilderHeaderActions.js.map +1 -0
  23. package/dist/chrome/PageBuilderOverlayBridge.d.ts +10 -0
  24. package/dist/chrome/PageBuilderOverlayBridge.d.ts.map +1 -0
  25. package/dist/chrome/PageBuilderOverlayBridge.js +25 -0
  26. package/dist/chrome/PageBuilderOverlayBridge.js.map +1 -0
  27. package/dist/chrome/QuickTooltip.d.ts +8 -0
  28. package/dist/chrome/QuickTooltip.d.ts.map +1 -0
  29. package/dist/chrome/QuickTooltip.js +33 -0
  30. package/dist/chrome/QuickTooltip.js.map +1 -0
  31. package/dist/chrome/action-bar-target.d.ts +12 -0
  32. package/dist/chrome/action-bar-target.d.ts.map +1 -0
  33. package/dist/chrome/action-bar-target.js +37 -0
  34. package/dist/chrome/action-bar-target.js.map +1 -0
  35. package/dist/chrome/cms-page-builder-api.d.ts +21 -0
  36. package/dist/chrome/cms-page-builder-api.d.ts.map +1 -0
  37. package/dist/chrome/cms-page-builder-api.js +50 -0
  38. package/dist/chrome/cms-page-builder-api.js.map +1 -0
  39. package/dist/chrome/page-builder-data.d.ts +5 -0
  40. package/dist/chrome/page-builder-data.d.ts.map +1 -0
  41. package/dist/chrome/page-builder-data.js +12 -0
  42. package/dist/chrome/page-builder-data.js.map +1 -0
  43. package/dist/email/EmailBuilderActionBar.d.ts +8 -0
  44. package/dist/email/EmailBuilderActionBar.d.ts.map +1 -0
  45. package/dist/email/EmailBuilderActionBar.js +91 -0
  46. package/dist/email/EmailBuilderActionBar.js.map +1 -0
  47. package/dist/email/EmailEditorPane.d.ts +42 -0
  48. package/dist/email/EmailEditorPane.d.ts.map +1 -0
  49. package/dist/email/EmailEditorPane.js +637 -0
  50. package/dist/email/EmailEditorPane.js.map +1 -0
  51. package/dist/email/EmailRichTextField.d.ts +14 -0
  52. package/dist/email/EmailRichTextField.d.ts.map +1 -0
  53. package/dist/email/EmailRichTextField.js +45 -0
  54. package/dist/email/EmailRichTextField.js.map +1 -0
  55. package/dist/email/EmailRowLayoutPicker.d.ts +8 -0
  56. package/dist/email/EmailRowLayoutPicker.d.ts.map +1 -0
  57. package/dist/email/EmailRowLayoutPicker.js +17 -0
  58. package/dist/email/EmailRowLayoutPicker.js.map +1 -0
  59. package/dist/email/EmailVariableFields.d.ts +13 -0
  60. package/dist/email/EmailVariableFields.d.ts.map +1 -0
  61. package/dist/email/EmailVariableFields.js +49 -0
  62. package/dist/email/EmailVariableFields.js.map +1 -0
  63. package/dist/email/EmailVariablesProvider.d.ts +15 -0
  64. package/dist/email/EmailVariablesProvider.d.ts.map +1 -0
  65. package/dist/email/EmailVariablesProvider.js +54 -0
  66. package/dist/email/EmailVariablesProvider.js.map +1 -0
  67. package/dist/email/email-builder-plugin.d.ts +6 -0
  68. package/dist/email/email-builder-plugin.d.ts.map +1 -0
  69. package/dist/email/email-builder-plugin.js +14 -0
  70. package/dist/email/email-builder-plugin.js.map +1 -0
  71. package/dist/email/email-template-layout.d.ts +16 -0
  72. package/dist/email/email-template-layout.d.ts.map +1 -0
  73. package/dist/email/email-template-layout.js +55 -0
  74. package/dist/email/email-template-layout.js.map +1 -0
  75. package/dist/email/email-templates-api.d.ts +14 -0
  76. package/dist/email/email-templates-api.d.ts.map +1 -0
  77. package/dist/email/email-templates-api.js +54 -0
  78. package/dist/email/email-templates-api.js.map +1 -0
  79. package/dist/email/index.d.ts +33 -0
  80. package/dist/email/index.d.ts.map +1 -0
  81. package/dist/email/index.js +33 -0
  82. package/dist/email/index.js.map +1 -0
  83. package/dist/email/insert-at-cursor.d.ts +8 -0
  84. package/dist/email/insert-at-cursor.d.ts.map +1 -0
  85. package/dist/email/insert-at-cursor.js +12 -0
  86. package/dist/email/insert-at-cursor.js.map +1 -0
  87. package/dist/email/variables.d.ts +20 -0
  88. package/dist/email/variables.d.ts.map +1 -0
  89. package/dist/email/variables.js +41 -0
  90. package/dist/email/variables.js.map +1 -0
  91. package/dist/index.d.ts +52 -0
  92. package/dist/index.d.ts.map +1 -0
  93. package/dist/index.js +49 -0
  94. package/dist/index.js.map +1 -0
  95. package/package.json +69 -0
  96. package/tailwind.css +14 -0
@@ -0,0 +1,637 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import '@endora-commerce/cms-components/styles.css';
3
+ import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
4
+ import { Puck } from '@measured/puck';
5
+ import '@measured/puck/puck.css';
6
+ import { Eye, Monitor, Smartphone, X } from 'lucide-react';
7
+ import { applyEmailRowLayoutPreset, buildSampleVariableContext, DEFAULT_ORDER_ITEMS_SAMPLE, defaultEmailBuilderConfig, EmailBrandingPreviewProvider, EmailEmbedsProvider, filterEmailPaletteByVariables, findEmailRowById, replaceEmailRowInData, renderDirectives, renderEmailHtml, sampleOrderDiscountsText, sampleOrderSummaryText, useEmailBrandingPreview, } from '@endora-commerce/email-components';
8
+ import { buildPaletteCategories, filterConfigByContext, } from '@endora-commerce/page-builder-core';
9
+ import { hasInvalidColumnPlacement, shouldRevertPuckAction, toPuckItemArray, } from '@endora-commerce/page-builder-core/editor';
10
+ import { Button } from '@endora-commerce/admin-kit/ui';
11
+ import { apiClient, cn } from '@endora-commerce/admin-kit/lib';
12
+ import { createImageAssetField, createImageSourceField, createImageUrlField, } from '../chrome/AssetPickers.js';
13
+ import { createCategorySlugsField, createProductSlugField, createProductSlugsField, } from '../chrome/CatalogPickers.js';
14
+ import { PageBuilderColorPaletteProvider } from '../chrome/ColorPaletteProvider.js';
15
+ import { PageBuilderHeaderActions, PageBuilderHeaderShell, PageBuilderTemplateActions, } from '../chrome/PageBuilderHeaderActions.js';
16
+ import { PageBuilderOverlayBridge } from '../chrome/PageBuilderOverlayBridge.js';
17
+ import { getPageBuilderColorPalette, getPageBuilderDescriptor, } from '../chrome/cms-page-builder-api.js';
18
+ import { getEmailBranding } from './email-templates-api.js';
19
+ import { fetchAssetDetail } from '@endora-commerce/admin-kit/components';
20
+ import { toAbsoluteAssetUrl } from '@endora-commerce/admin-kit/lib';
21
+ import { useTranslation, useTranslationContext } from '@endora-commerce/admin-kit/i18n';
22
+ import { emailRichTextContentField, emailHtmlFromRichContent } from './EmailRichTextField.js';
23
+ import { emailTextareaWithVariablesField } from './EmailVariableFields.js';
24
+ import { createEmailBuilderEditorPlugin } from './email-builder-plugin.js';
25
+ import { useEmailVariables } from './EmailVariablesProvider.js';
26
+ import { EmailBuilderActionBar } from './EmailBuilderActionBar.js';
27
+ import { EmailRowLayoutPicker } from './EmailRowLayoutPicker.js';
28
+ const emptyData = { root: { props: {} }, content: [] };
29
+ const CANVAS_WIDTH = 600;
30
+ const emailBuilderPlugin = createEmailBuilderEditorPlugin();
31
+ function collectEmailRowIds(data) {
32
+ const ids = new Set();
33
+ const visit = (items) => {
34
+ for (const item of items) {
35
+ if (item.type === 'transactional_emails.EmailRow' && typeof item.props.id === 'string') {
36
+ ids.add(item.props.id);
37
+ }
38
+ for (const value of Object.values(item.props)) {
39
+ const nested = toPuckItemArray(value);
40
+ if (nested.length > 0)
41
+ visit(nested);
42
+ }
43
+ }
44
+ };
45
+ visit(toPuckItemArray(data.content));
46
+ for (const zoneItems of Object.values(data.zones ?? {})) {
47
+ visit(toPuckItemArray(zoneItems));
48
+ }
49
+ return ids;
50
+ }
51
+ const PREVIEW_PLACEHOLDER_LOGO = 'https://placehold.co/160x48?text=Logo';
52
+ const PREVIEW_UNSUBSCRIBE_URL = 'https://shop.example/newsletter/unsubscribe?token=preview';
53
+ function pickLanguageTree(content) {
54
+ const raw = content['pl-PL'] ?? content['en-US'] ?? Object.values(content)[0] ?? null;
55
+ if (!raw || typeof raw !== 'object')
56
+ return null;
57
+ return raw;
58
+ }
59
+ /**
60
+ * Canvas preview for an embedded block/template. Resolves branding (and other
61
+ * sample) directives so `{{var branding.logoUrl}}` becomes a real `<img src>`.
62
+ */
63
+ function EmbedPreviewHtml({ content }) {
64
+ const { logoUrl } = useEmailBrandingPreview();
65
+ const tree = pickLanguageTree(content);
66
+ const raw = renderEmailHtml(tree, { document: false });
67
+ const html = renderDirectives(raw, {
68
+ branding: {
69
+ logoUrl: logoUrl || PREVIEW_PLACEHOLDER_LOGO,
70
+ accentColor: '#1f2937',
71
+ },
72
+ unsubscribeUrl: PREVIEW_UNSUBSCRIBE_URL,
73
+ }, { escape: true });
74
+ return _jsx("div", { dangerouslySetInnerHTML: { __html: html } });
75
+ }
76
+ function previewNode(content) {
77
+ return _jsx(EmbedPreviewHtml, { content: content });
78
+ }
79
+ const STOREFRONT_BASE_URL = (import.meta.env.VITE_STOREFRONT_BASE_URL ?? 'http://localhost:3000').replace(/\/+$/, '');
80
+ function toAbsoluteStorefrontUrl(pathOrUrl) {
81
+ if (!pathOrUrl)
82
+ return '';
83
+ if (/^https?:\/\//i.test(pathOrUrl))
84
+ return pathOrUrl;
85
+ if (pathOrUrl.startsWith('/'))
86
+ return `${STOREFRONT_BASE_URL}${pathOrUrl}`;
87
+ return pathOrUrl;
88
+ }
89
+ function pickName(name, fallback) {
90
+ if (typeof name === 'string')
91
+ return name || fallback;
92
+ return name['pl-PL'] ?? name['en-US'] ?? Object.values(name)[0] ?? fallback;
93
+ }
94
+ async function resolveProductForCard(slug) {
95
+ if (!slug) {
96
+ return { productId: '', productSlug: '', title: 'Select a product', sku: '', imageSrc: '', href: '' };
97
+ }
98
+ const params = new URLSearchParams();
99
+ params.set('q', slug);
100
+ params.set('pageSize', '10');
101
+ const res = await apiClient.get(`/api/v1/admin/catalog/products?${params.toString()}`);
102
+ const product = res.data.find((p) => p.slug === slug) ?? res.data[0];
103
+ if (!product) {
104
+ return {
105
+ productId: '',
106
+ productSlug: slug,
107
+ title: slug,
108
+ sku: '',
109
+ imageSrc: '',
110
+ href: toAbsoluteStorefrontUrl(`/p/${slug}`),
111
+ };
112
+ }
113
+ let imageSrc = '';
114
+ try {
115
+ const gallery = await apiClient.get(`/api/v1/admin/catalog/products/${product.id}/gallery`);
116
+ const ordered = [...gallery.data].sort((a, b) => (a.position ?? 0) - (b.position ?? 0));
117
+ const preferred = ordered.find((g) => g.labels?.includes('thumbnail')) ??
118
+ ordered.find((g) => g.labels?.includes('small_image')) ??
119
+ ordered.find((g) => g.labels?.includes('base_image')) ??
120
+ ordered[0];
121
+ if (preferred?.assetId) {
122
+ const asset = await fetchAssetDetail(preferred.assetId);
123
+ imageSrc = toAbsoluteAssetUrl(asset.url);
124
+ }
125
+ }
126
+ catch {
127
+ /* no gallery */
128
+ }
129
+ return {
130
+ productId: product.id,
131
+ productSlug: product.slug,
132
+ title: pickName(product.name, product.slug),
133
+ sku: product.sku,
134
+ imageSrc,
135
+ href: toAbsoluteStorefrontUrl(`/p/${product.slug}`),
136
+ };
137
+ }
138
+ async function resolveCategoryForGrid(slug) {
139
+ if (!slug) {
140
+ return { categoryId: '', categorySlug: '', title: '', href: '', imageSrc: '' };
141
+ }
142
+ const res = await apiClient.get('/api/v1/admin/catalog/categories');
143
+ const category = res.data.find((c) => c.slug === slug);
144
+ if (!category) {
145
+ return {
146
+ categoryId: '',
147
+ categorySlug: slug,
148
+ title: slug,
149
+ href: toAbsoluteStorefrontUrl(`/c/${slug}`),
150
+ imageSrc: '',
151
+ };
152
+ }
153
+ let imageSrc = '';
154
+ if (category.mainImageAssetId) {
155
+ try {
156
+ const asset = await fetchAssetDetail(category.mainImageAssetId);
157
+ imageSrc = toAbsoluteAssetUrl(asset.url);
158
+ }
159
+ catch {
160
+ /* ignore */
161
+ }
162
+ }
163
+ return {
164
+ categoryId: category.id,
165
+ categorySlug: category.slug,
166
+ title: pickName(category.name, category.slug),
167
+ href: toAbsoluteStorefrontUrl(`/c/${category.slug}`),
168
+ imageSrc,
169
+ };
170
+ }
171
+ function withEditorFields(base) {
172
+ const components = { ...(base.components ?? {}) };
173
+ const patchTextarea = (name, fieldKey) => {
174
+ const c = components[name];
175
+ if (!c?.fields?.[fieldKey])
176
+ return;
177
+ components[name] = {
178
+ ...c,
179
+ fields: {
180
+ ...c.fields,
181
+ [fieldKey]: {
182
+ ...emailTextareaWithVariablesField,
183
+ label: c.fields[fieldKey].label,
184
+ },
185
+ },
186
+ };
187
+ };
188
+ patchTextarea('transactional_emails.EmailText', 'text');
189
+ patchTextarea('transactional_emails.EmailHeading', 'text');
190
+ patchTextarea('transactional_emails.EmailCallout', 'text');
191
+ patchTextarea('transactional_emails.EmailFooterLegal', 'text');
192
+ const rich = components['transactional_emails.EmailRichText'];
193
+ if (rich) {
194
+ components['transactional_emails.EmailRichText'] = {
195
+ ...rich,
196
+ fields: {
197
+ ...rich.fields,
198
+ content: emailRichTextContentField,
199
+ align: rich.fields?.['align'],
200
+ },
201
+ resolveData: async ({ props }, { changed }) => {
202
+ if (changed['content'] || (!props['html'] && props['content'])) {
203
+ return {
204
+ props: {
205
+ ...props,
206
+ html: emailHtmlFromRichContent(props['content']) || props['html'] || '',
207
+ },
208
+ };
209
+ }
210
+ return { props };
211
+ },
212
+ };
213
+ }
214
+ const image = components['transactional_emails.EmailImage'];
215
+ if (image) {
216
+ components['transactional_emails.EmailImage'] = {
217
+ ...image,
218
+ fields: {
219
+ ...image.fields,
220
+ imageSource: createImageSourceField(),
221
+ src: createImageUrlField('Image URL'),
222
+ assetId: createImageAssetField('Image'),
223
+ },
224
+ resolveData: async ({ props }) => {
225
+ const source = props.imageSource === 'library' ? 'library' : 'url';
226
+ let src = typeof props.src === 'string' ? props.src : '';
227
+ const assetId = typeof props.assetId === 'string' ? props.assetId : '';
228
+ if (source === 'library' && assetId) {
229
+ try {
230
+ const detail = await fetchAssetDetail(assetId);
231
+ src = toAbsoluteAssetUrl(detail.url);
232
+ }
233
+ catch {
234
+ /* keep */
235
+ }
236
+ }
237
+ if (source === 'url') {
238
+ return { props: { ...props, imageSource: 'url', assetId: '', src } };
239
+ }
240
+ return { props: { ...props, imageSource: 'library', assetId, src } };
241
+ },
242
+ };
243
+ }
244
+ const productCard = components['catalog.EmailProductCard'];
245
+ if (productCard) {
246
+ components['catalog.EmailProductCard'] = {
247
+ ...productCard,
248
+ fields: {
249
+ ...productCard.fields,
250
+ productSlug: createProductSlugField(),
251
+ },
252
+ resolveData: async ({ props }) => {
253
+ const slug = typeof props.productSlug === 'string' ? props.productSlug : '';
254
+ if (!slug) {
255
+ return {
256
+ props: {
257
+ ...props,
258
+ productId: '',
259
+ title: 'Select a product',
260
+ sku: '',
261
+ imageSrc: '',
262
+ href: typeof props.href === 'string' ? props.href : '',
263
+ },
264
+ };
265
+ }
266
+ try {
267
+ const resolved = await resolveProductForCard(slug);
268
+ const priceOverride = typeof props.price === 'string' ? props.price : '';
269
+ const hrefOverride = typeof props.href === 'string' ? props.href : '';
270
+ return {
271
+ props: {
272
+ ...props,
273
+ productSlug: slug,
274
+ productId: resolved.productId,
275
+ title: resolved.title,
276
+ sku: resolved.sku,
277
+ imageSrc: resolved.imageSrc,
278
+ // Keep author price override when set; otherwise leave blank for B2B.
279
+ price: priceOverride,
280
+ href: hrefOverride
281
+ ? toAbsoluteStorefrontUrl(hrefOverride)
282
+ : resolved.href,
283
+ },
284
+ };
285
+ }
286
+ catch {
287
+ return { props };
288
+ }
289
+ },
290
+ };
291
+ }
292
+ const productGrid = components['catalog.EmailProductGrid'];
293
+ if (productGrid) {
294
+ components['catalog.EmailProductGrid'] = {
295
+ ...productGrid,
296
+ fields: {
297
+ ...productGrid.fields,
298
+ productSlugs: createProductSlugsField(),
299
+ },
300
+ resolveData: async ({ props }) => {
301
+ const slugs = Array.isArray(props.productSlugs)
302
+ ? props.productSlugs.filter((s) => typeof s === 'string' && s)
303
+ : [];
304
+ if (slugs.length === 0) {
305
+ return { props: { ...props, productSlugs: [], items: [] } };
306
+ }
307
+ try {
308
+ const items = await Promise.all(slugs.map((slug) => resolveProductForCard(slug)));
309
+ return {
310
+ props: {
311
+ ...props,
312
+ productSlugs: slugs,
313
+ items: items.map((item) => ({
314
+ productId: item.productId,
315
+ productSlug: item.productSlug,
316
+ title: item.title,
317
+ sku: item.sku,
318
+ price: '',
319
+ href: item.href,
320
+ imageSrc: item.imageSrc,
321
+ })),
322
+ },
323
+ };
324
+ }
325
+ catch {
326
+ return { props };
327
+ }
328
+ },
329
+ };
330
+ }
331
+ const categoryGrid = components['catalog.EmailCategoryGrid'];
332
+ if (categoryGrid) {
333
+ components['catalog.EmailCategoryGrid'] = {
334
+ ...categoryGrid,
335
+ fields: {
336
+ ...categoryGrid.fields,
337
+ categorySlugs: createCategorySlugsField(),
338
+ },
339
+ resolveData: async ({ props }) => {
340
+ const slugs = Array.isArray(props.categorySlugs)
341
+ ? props.categorySlugs.filter((s) => typeof s === 'string' && s)
342
+ : [];
343
+ if (slugs.length === 0) {
344
+ return { props: { ...props, categorySlugs: [], items: [] } };
345
+ }
346
+ try {
347
+ const items = await Promise.all(slugs.map((slug) => resolveCategoryForGrid(slug)));
348
+ return {
349
+ props: {
350
+ ...props,
351
+ categorySlugs: slugs,
352
+ items,
353
+ },
354
+ };
355
+ }
356
+ catch {
357
+ return { props };
358
+ }
359
+ },
360
+ };
361
+ }
362
+ return { ...base, components };
363
+ }
364
+ function mergeEmbedSelects(base, blockOptions) {
365
+ const components = { ...(base.components ?? {}) };
366
+ const insertBlock = components['transactional_emails.EmailInsertBlock'];
367
+ if (insertBlock) {
368
+ components['transactional_emails.EmailInsertBlock'] = {
369
+ ...insertBlock,
370
+ fields: {
371
+ ...insertBlock.fields,
372
+ code: { type: 'select', label: 'Block', options: [{ label: '—', value: '' }, ...blockOptions] },
373
+ },
374
+ };
375
+ }
376
+ return { ...base, components };
377
+ }
378
+ function EmailHtmlPreviewModal({ open, html, onClose, }) {
379
+ const [width, setWidth] = useState(600);
380
+ useEffect(() => {
381
+ if (!open)
382
+ return undefined;
383
+ const onKey = (e) => {
384
+ if (e.key === 'Escape')
385
+ onClose();
386
+ };
387
+ window.addEventListener('keydown', onKey);
388
+ return () => window.removeEventListener('keydown', onKey);
389
+ }, [open, onClose]);
390
+ if (!open)
391
+ return null;
392
+ return (_jsx("div", { className: "fixed inset-0 z-[80] flex items-center justify-center bg-black/40 p-4", role: "dialog", "aria-modal": "true", "aria-label": "Preview email", onClick: (e) => {
393
+ if (e.target === e.currentTarget)
394
+ onClose();
395
+ }, children: _jsxs("div", { className: "flex max-h-[90vh] w-full max-w-4xl flex-col overflow-hidden rounded-lg border bg-background shadow-lg", children: [_jsxs("div", { className: "flex flex-wrap items-center justify-between gap-2 border-b px-4 py-3", children: [_jsxs("div", { className: "flex flex-wrap items-center gap-2", children: [_jsx("span", { className: "text-sm font-semibold", children: "Preview email" }), _jsxs(Button, { type: "button", variant: width === 600 ? 'default' : 'outline', size: "sm", onClick: () => setWidth(600), "aria-pressed": width === 600, children: [_jsx(Monitor, { className: "mr-1 h-4 w-4" }), "Desktop mail (600px)"] }), _jsxs(Button, { type: "button", variant: width === 320 ? 'default' : 'outline', size: "sm", onClick: () => setWidth(320), "aria-pressed": width === 320, children: [_jsx(Smartphone, { className: "mr-1 h-4 w-4" }), "Narrow (320px)"] })] }), _jsx(Button, { type: "button", variant: "ghost", size: "sm", onClick: onClose, "aria-label": "Close preview", children: _jsx(X, { className: "h-4 w-4" }) })] }), _jsx("div", { className: "min-h-0 flex-1 overflow-auto bg-muted/30 p-4", children: _jsx("iframe", { title: "Email HTML preview", srcDoc: html, className: "mx-auto block h-[min(70vh,640px)] border-0 bg-white shadow-sm", style: { width, maxWidth: '100%', overflow: 'hidden' }, scrolling: "auto" }) })] }) }));
396
+ }
397
+ function buildPreviewHtml(data, variables, logoUrl, extras, embedTrees, previewLanguage) {
398
+ const language = previewLanguage || 'en-US';
399
+ const raw = renderEmailHtml((data ?? emptyData), {
400
+ document: true,
401
+ language,
402
+ ...(embedTrees ? { embeds: embedTrees } : {}),
403
+ });
404
+ const fromDescriptors = buildSampleVariableContext(variables.map((v) => ({ key: v.key, sampleValue: v.sampleValue ?? v.snippet })), {
405
+ branding: { logoUrl: logoUrl || PREVIEW_PLACEHOLDER_LOGO, accentColor: '#1f2937' },
406
+ ...extras,
407
+ });
408
+ // Ensure order.items exists for Order summary even when not in the variable catalogue.
409
+ const order = fromDescriptors['order'] ?? {};
410
+ if (!Array.isArray(order['items'])) {
411
+ order['items'] = DEFAULT_ORDER_ITEMS_SAMPLE;
412
+ fromDescriptors['order'] = order;
413
+ }
414
+ // Always localize built-in order money lines for the active editor language —
415
+ // catalogue sampleValue is English-only.
416
+ order['summaryText'] = sampleOrderSummaryText(language);
417
+ if (typeof order['discountsText'] !== 'string' || order['discountsText'] === ' none') {
418
+ order['discountsText'] = sampleOrderDiscountsText(language);
419
+ }
420
+ fromDescriptors['order'] = order;
421
+ if (typeof fromDescriptors['unsubscribeUrl'] !== 'string' || !fromDescriptors['unsubscribeUrl']) {
422
+ fromDescriptors['unsubscribeUrl'] = PREVIEW_UNSUBSCRIBE_URL;
423
+ }
424
+ return renderDirectives(raw, fromDescriptors, { escape: true });
425
+ }
426
+ export function EmailEditorPane({ data, onChange, editorKey, builderContext = 'email', blockOptions = [], embeds: embedsProp, embedTrees: embedTreesProp, previewLanguage = null, salesChannelId = null, previewExtras = {}, onSaveAsTemplate, onListTemplatesForApply, onResolveTemplateLayout, }) {
427
+ const { variables } = useEmailVariables();
428
+ // `core`, not `cms` — the page-builder chrome this pane renders owns no module
429
+ // knowledge and its copy ships in `_i18n`'s bundle (feature 091 P5a, R-1).
430
+ const tChrome = useTranslation('core');
431
+ const [embeds, setEmbeds] = useState(embedsProp ?? { blocks: {}, templates: {} });
432
+ const [embedTrees, setEmbedTrees] = useState(embedTreesProp ?? { blocks: {}, templates: {} });
433
+ const [fullscreen, setFullscreen] = useState(false);
434
+ const [previewOpen, setPreviewOpen] = useState(false);
435
+ const [canvasEpoch, setCanvasEpoch] = useState(0);
436
+ const [rowLayoutPickerForId, setRowLayoutPickerForId] = useState(null);
437
+ const [logoUrl, setLogoUrl] = useState('');
438
+ const [descriptor, setDescriptor] = useState(null);
439
+ const { t: translateInScope } = useTranslationContext();
440
+ const [paletteEntries, setPaletteEntries] = useState([]);
441
+ const knownRowIdsRef = useRef(collectEmailRowIds(data ?? emptyData));
442
+ const lastValidDataRef = useRef(data ?? emptyData);
443
+ const applyCanvasData = (next) => {
444
+ knownRowIdsRef.current = collectEmailRowIds(next);
445
+ lastValidDataRef.current = next;
446
+ onChange(next);
447
+ setCanvasEpoch((n) => n + 1);
448
+ };
449
+ const handleEditorChange = useCallback((next) => {
450
+ if (hasInvalidColumnPlacement(next)) {
451
+ // Remount to last good tree — EmailColumn must stay inside EmailRow.
452
+ setCanvasEpoch((n) => n + 1);
453
+ return;
454
+ }
455
+ const nextRowIds = collectEmailRowIds(next);
456
+ let newRowId = null;
457
+ for (const id of nextRowIds) {
458
+ if (!knownRowIdsRef.current.has(id)) {
459
+ newRowId = id;
460
+ break;
461
+ }
462
+ }
463
+ knownRowIdsRef.current = nextRowIds;
464
+ lastValidDataRef.current = next;
465
+ onChange(next);
466
+ if (newRowId) {
467
+ const rowId = newRowId;
468
+ window.setTimeout(() => setRowLayoutPickerForId(rowId), 0);
469
+ }
470
+ }, [onChange]);
471
+ const handlePuckAction = useCallback((action, appState) => {
472
+ if (shouldRevertPuckAction(action, appState.data, lastValidDataRef.current)) {
473
+ return false;
474
+ }
475
+ return action;
476
+ }, []);
477
+ const applyRowLayout = useCallback((rowId, presetId) => {
478
+ const row = findEmailRowById(lastValidDataRef.current, rowId);
479
+ if (!row) {
480
+ setRowLayoutPickerForId(null);
481
+ return;
482
+ }
483
+ const nextProps = applyEmailRowLayoutPreset(row.props, presetId);
484
+ const next = replaceEmailRowInData(lastValidDataRef.current, rowId, {
485
+ type: 'transactional_emails.EmailRow',
486
+ props: { ...nextProps, id: rowId },
487
+ });
488
+ lastValidDataRef.current = next;
489
+ knownRowIdsRef.current = collectEmailRowIds(next);
490
+ onChange(next);
491
+ setCanvasEpoch((n) => n + 1);
492
+ setRowLayoutPickerForId(null);
493
+ }, [onChange]);
494
+ const hasTemplateActions = Boolean(onSaveAsTemplate || (onListTemplatesForApply && onResolveTemplateLayout));
495
+ useEffect(() => {
496
+ if (embedsProp)
497
+ setEmbeds(embedsProp);
498
+ }, [embedsProp]);
499
+ useEffect(() => {
500
+ if (embedTreesProp)
501
+ setEmbedTrees(embedTreesProp);
502
+ }, [embedTreesProp]);
503
+ useEffect(() => {
504
+ let live = true;
505
+ void getEmailBranding(salesChannelId)
506
+ .then((b) => {
507
+ if (live)
508
+ setLogoUrl(toAbsoluteAssetUrl(b.logoUrl || ''));
509
+ })
510
+ .catch(() => {
511
+ if (live)
512
+ setLogoUrl('');
513
+ });
514
+ return () => {
515
+ live = false;
516
+ };
517
+ }, [salesChannelId]);
518
+ useEffect(() => {
519
+ let live = true;
520
+ void getPageBuilderColorPalette()
521
+ .then((entries) => {
522
+ if (live)
523
+ setPaletteEntries(entries);
524
+ })
525
+ .catch(() => {
526
+ if (live)
527
+ setPaletteEntries([]);
528
+ });
529
+ return () => {
530
+ live = false;
531
+ };
532
+ }, []);
533
+ /**
534
+ * The declared palette (feature 096, T305).
535
+ *
536
+ * A failure leaves `descriptor` null, which renders every block in Puck's
537
+ * *Other* drawer rather than in its section — degraded and still usable,
538
+ * which is the same answer the colour-palette fetch above gives. It is not a
539
+ * `catch` hiding a capability's absence: the endpoint is `cms`', and an
540
+ * operator who has switched `cms` off has not thereby switched off the e-mail
541
+ * builder.
542
+ */
543
+ useEffect(() => {
544
+ let live = true;
545
+ void getPageBuilderDescriptor()
546
+ .then((served) => {
547
+ if (live)
548
+ setDescriptor(served);
549
+ })
550
+ .catch(() => {
551
+ if (live)
552
+ setDescriptor(null);
553
+ });
554
+ return () => {
555
+ live = false;
556
+ };
557
+ }, []);
558
+ useEffect(() => {
559
+ if (!fullscreen)
560
+ return undefined;
561
+ const onKey = (e) => {
562
+ if (e.key === 'Escape')
563
+ setFullscreen(false);
564
+ };
565
+ window.addEventListener('keydown', onKey);
566
+ return () => window.removeEventListener('keydown', onKey);
567
+ }, [fullscreen]);
568
+ const config = useMemo(() => {
569
+ const withFields = withEditorFields(defaultEmailBuilderConfig);
570
+ const withEmbeds = mergeEmbedSelects(withFields, blockOptions);
571
+ const byContext = filterConfigByContext(withEmbeds, builderContext);
572
+ // Feature 096, T302/T305 — the sections come from the modules that declared
573
+ // them, merged and presence-filtered by the server, and are resolved for
574
+ // `builderContext` through `contextAdmits`. That last part is what keeps the
575
+ // **newsletter** palette sectioned: no block declares `newsletter`, the
576
+ // newsletter palette *is* the e-mail palette, and the admission is the one
577
+ // thing that says so (`contracts/block-definition.md` §4.1.1). The
578
+ // `categories` map that used to sit in `@endora-commerce/email-components`
579
+ // named `orders`' eight blocks and `catalog`'s three from a package neither
580
+ // module owns.
581
+ const sectioned = {
582
+ ...byContext,
583
+ categories: buildPaletteCategories(descriptor?.components ?? [], descriptor?.categories ?? [], builderContext, {
584
+ title: (section) => {
585
+ const owner = section.ownerModule ?? 'transactional_emails';
586
+ const resolved = translateInScope(owner, section.titleKey);
587
+ return resolved === `${owner}.${section.titleKey}` ? section.key : resolved;
588
+ },
589
+ renderable: new Set(Object.keys(byContext.components ?? {})),
590
+ }),
591
+ };
592
+ return filterEmailPaletteByVariables(sectioned, variables.map((v) => v.key));
593
+ }, [blockOptions, builderContext, descriptor, translateInScope, variables]);
594
+ const previewHtml = useMemo(() => buildPreviewHtml(data ?? emptyData, variables, logoUrl, previewExtras, embedTrees, previewLanguage), [data, variables, logoUrl, previewExtras, embedTrees, previewLanguage]);
595
+ return (_jsx(PageBuilderColorPaletteProvider, { initialEntries: paletteEntries, children: _jsxs("div", { className: cn('space-y-3', fullscreen && 'fixed inset-0 z-50 flex flex-col overflow-auto bg-background p-4'), children: [_jsx("div", { className: cn('overflow-hidden rounded-md border', fullscreen ? 'min-h-0 flex-1' : 'min-h-[560px]'), style: { ['--email-preview-width']: `${CANVAS_WIDTH}px` }, children: _jsx(EmailBrandingPreviewProvider, { value: { logoUrl }, children: _jsx(EmailEmbedsProvider, { value: embeds, children: _jsx("div", { className: "email-builder-pane", "data-preview-width": CANVAS_WIDTH, children: _jsx(Puck, { config: config, data: data ?? emptyData, onChange: handleEditorChange, onAction: handlePuckAction, plugins: [emailBuilderPlugin], overrides: {
596
+ drawerItem: ({ name, children, }) => (name === 'transactional_emails.EmailColumn' ? _jsx(_Fragment, {}) : _jsx(_Fragment, { children: children })),
597
+ actionBar: (props) => _jsx(EmailBuilderActionBar, { ...props }),
598
+ componentOverlay: (props) => _jsx(PageBuilderOverlayBridge, { ...props }),
599
+ ...(hasTemplateActions
600
+ ? {
601
+ header: ({ children }) => (_jsx(PageBuilderHeaderShell, { leading: _jsx(PageBuilderTemplateActions, { currentData: data ?? emptyData, ...(onSaveAsTemplate ? { onSaveAsTemplate } : {}), ...(onListTemplatesForApply
602
+ ? { onListTemplatesForApply }
603
+ : {}), ...(onResolveTemplateLayout
604
+ ? {
605
+ onApplyTemplate: async (templateId) => {
606
+ const next = await onResolveTemplateLayout(templateId);
607
+ applyCanvasData(structuredClone(next));
608
+ },
609
+ }
610
+ : {}), t: tChrome }), children: children })),
611
+ }
612
+ : {}),
613
+ headerActions: () => (_jsxs("div", { className: "cms-pb-header-actions", children: [_jsxs(Button, { type: "button", variant: "outline", size: "sm", onClick: () => setPreviewOpen(true), title: "Preview email", "aria-label": "Preview email", children: [_jsx(Eye, { className: "h-4 w-4" }), _jsx("span", { className: "cms-pb-header-actions__label", children: "Preview email" })] }), _jsx(PageBuilderHeaderActions, { fullscreen: fullscreen, onToggleFullscreen: () => setFullscreen((f) => !f), currentData: data ?? emptyData, onClearCanvas: () => applyCanvasData(emptyData), t: tChrome })] })),
614
+ } }, `${editorKey}:${canvasEpoch}`) }) }) }) }), _jsx(EmailRowLayoutPicker, { open: rowLayoutPickerForId !== null, onOpenChange: (open) => {
615
+ if (!open) {
616
+ if (rowLayoutPickerForId) {
617
+ const row = findEmailRowById(lastValidDataRef.current, rowLayoutPickerForId);
618
+ const cols = Array.isArray(row?.props.content) ? row.props.content : [];
619
+ const hasColumns = cols.length > 0 ||
620
+ Object.keys(lastValidDataRef.current.zones ?? {}).some((zone) => zone.startsWith(`${rowLayoutPickerForId}:`));
621
+ // Zones may hold columns before props.content does — check zone length.
622
+ const zoneCols = lastValidDataRef.current.zones?.[`${rowLayoutPickerForId}:content`] ?? [];
623
+ if (!hasColumns && (!Array.isArray(zoneCols) || zoneCols.length === 0)) {
624
+ applyRowLayout(rowLayoutPickerForId, '2');
625
+ return;
626
+ }
627
+ }
628
+ setRowLayoutPickerForId(null);
629
+ }
630
+ }, onSelect: (presetId) => {
631
+ if (rowLayoutPickerForId)
632
+ applyRowLayout(rowLayoutPickerForId, presetId);
633
+ setRowLayoutPickerForId(null);
634
+ } }), _jsx(EmailHtmlPreviewModal, { open: previewOpen, html: previewHtml, onClose: () => setPreviewOpen(false) })] }) }));
635
+ }
636
+ export { previewNode };
637
+ //# sourceMappingURL=EmailEditorPane.js.map