@jhits/plugin-newsletter 0.0.16 → 0.0.17

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 (76) hide show
  1. package/dist/api/email-utils.d.ts.map +1 -1
  2. package/dist/api/email-utils.js +45 -4
  3. package/dist/api/handlers/send-newsletter.d.ts.map +1 -1
  4. package/dist/api/handlers/send-newsletter.js +54 -6
  5. package/dist/api/handlers/settings.d.ts.map +1 -1
  6. package/dist/api/handlers/settings.js +51 -1
  7. package/dist/index.d.ts +27 -10
  8. package/dist/index.d.ts.map +1 -1
  9. package/dist/index.js +15 -122
  10. package/dist/lib/blocks/BlockRenderer.d.ts.map +1 -1
  11. package/dist/lib/blocks/BlockRenderer.js +14 -2
  12. package/dist/lib/email/EmailRenderer.d.ts +1 -0
  13. package/dist/lib/email/EmailRenderer.d.ts.map +1 -1
  14. package/dist/lib/email/EmailRenderer.js +31 -19
  15. package/dist/lib/utils/config-resolver.d.ts +33 -0
  16. package/dist/lib/utils/config-resolver.d.ts.map +1 -0
  17. package/dist/lib/utils/config-resolver.js +47 -0
  18. package/dist/registry/BlockRegistry.d.ts +9 -1
  19. package/dist/registry/BlockRegistry.d.ts.map +1 -1
  20. package/dist/registry/BlockRegistry.js +126 -8
  21. package/dist/state/EditorContext.d.ts +11 -1
  22. package/dist/state/EditorContext.d.ts.map +1 -1
  23. package/dist/state/EditorContext.js +23 -5
  24. package/dist/state/types.d.ts +12 -0
  25. package/dist/state/types.d.ts.map +1 -1
  26. package/dist/types/block.d.ts +9 -0
  27. package/dist/types/block.d.ts.map +1 -1
  28. package/dist/types/newsletter.d.ts +2 -0
  29. package/dist/types/newsletter.d.ts.map +1 -1
  30. package/dist/views/CanvasEditor/BlockWrapper.d.ts.map +1 -1
  31. package/dist/views/CanvasEditor/BlockWrapper.js +24 -3
  32. package/dist/views/CanvasEditor/CanvasEditorView.d.ts.map +1 -1
  33. package/dist/views/CanvasEditor/CanvasEditorView.js +77 -17
  34. package/dist/views/CanvasEditor/EditorBody.d.ts.map +1 -1
  35. package/dist/views/CanvasEditor/EditorBody.js +1 -1
  36. package/dist/views/CanvasEditor/components/EditorCanvas.d.ts.map +1 -1
  37. package/dist/views/CanvasEditor/components/EditorCanvas.js +158 -100
  38. package/dist/views/CanvasEditor/components/EditorSidebar.d.ts +3 -1
  39. package/dist/views/CanvasEditor/components/EditorSidebar.d.ts.map +1 -1
  40. package/dist/views/CanvasEditor/components/EditorSidebar.js +3 -3
  41. package/dist/views/CanvasEditor/hooks/useRegisteredBlocks.d.ts +1 -1
  42. package/dist/views/CanvasEditor/hooks/useRegisteredBlocks.d.ts.map +1 -1
  43. package/dist/views/CanvasEditor/hooks/useRegisteredBlocks.js +6 -40
  44. package/dist/views/components/DomainPromptModal.d.ts +13 -0
  45. package/dist/views/components/DomainPromptModal.d.ts.map +1 -0
  46. package/dist/views/components/DomainPromptModal.js +58 -0
  47. package/dist/views/components/SendNewsletterModal.d.ts.map +1 -1
  48. package/dist/views/components/SendNewsletterModal.js +91 -22
  49. package/dist/views/components/SmtpSettingsModal.d.ts.map +1 -1
  50. package/dist/views/components/SmtpSettingsModal.js +10 -0
  51. package/dist/views/components/TestEmailModal.d.ts.map +1 -1
  52. package/dist/views/components/TestEmailModal.js +86 -17
  53. package/package.json +53 -9
  54. package/src/api/email-utils.ts +53 -4
  55. package/src/api/handlers/send-newsletter.ts +65 -6
  56. package/src/api/handlers/settings.ts +60 -2
  57. package/src/index.tsx +49 -155
  58. package/src/lib/blocks/BlockRenderer.tsx +16 -2
  59. package/src/lib/email/EmailRenderer.tsx +31 -20
  60. package/src/lib/utils/config-resolver.ts +71 -0
  61. package/src/registry/BlockRegistry.tsx +255 -0
  62. package/src/state/EditorContext.tsx +43 -8
  63. package/src/state/types.ts +16 -0
  64. package/src/types/block.ts +10 -0
  65. package/src/types/newsletter.ts +3 -0
  66. package/src/views/CanvasEditor/BlockWrapper.tsx +27 -2
  67. package/src/views/CanvasEditor/CanvasEditorView.tsx +142 -61
  68. package/src/views/CanvasEditor/EditorBody.tsx +17 -13
  69. package/src/views/CanvasEditor/components/EditorCanvas.tsx +178 -115
  70. package/src/views/CanvasEditor/components/EditorSidebar.tsx +57 -2
  71. package/src/views/CanvasEditor/hooks/useRegisteredBlocks.ts +6 -45
  72. package/src/views/components/DomainPromptModal.tsx +160 -0
  73. package/src/views/components/SendNewsletterModal.tsx +270 -184
  74. package/src/views/components/SmtpSettingsModal.tsx +11 -0
  75. package/src/views/components/TestEmailModal.tsx +235 -149
  76. package/src/registry/BlockRegistry.ts +0 -53
@@ -13,7 +13,6 @@
13
13
 
14
14
  import { Block } from '../../types/block';
15
15
  import { blockRegistry } from '../../registry/BlockRegistry';
16
- import { getChildBlocks } from '../utils/blockHelpers';
17
16
 
18
17
  /**
19
18
  * Email rendering context
@@ -62,15 +61,25 @@ function renderBlockByType(block: Block, context: EmailRenderContext): string {
62
61
  const level = (block.data.level as number) || 1;
63
62
  const text = (block.data.text as string) || '';
64
63
  const tag = `h${Math.min(Math.max(level, 1), 6)}`;
65
- const fontSize = level === 1 ? '32px' : level === 2 ? '24px' : level === 3 ? '20px' : '18px';
66
- return `<${tag} style="font-size: ${fontSize}; font-weight: bold; color: #1a2e26; margin: 20px 0 10px 0; line-height: 1.3;">${escapeHtml(text)}</${tag}>`;
64
+
65
+ // Aligned with BotanicsAndYou heading sizes
66
+ let fontSize = '30px';
67
+ if (level === 1) fontSize = '48px';
68
+ if (level === 2) fontSize = '36px';
69
+ if (level === 3) fontSize = '30px';
70
+ if (level >= 4) fontSize = '20px';
71
+
72
+ const fontWeight = level >= 4 ? '500' : 'bold';
73
+
74
+ return `<${tag} style="font-family: 'Georgia', serif; font-size: ${fontSize}; font-weight: ${fontWeight}; color: #1a2e26; margin: 40px 0 15px 0; line-height: 1.2;">${escapeHtml(text)}</${tag}>`;
67
75
  }
68
76
 
69
77
  case 'paragraph': {
70
78
  const html = (block.data.html as string) || (block.data.text as string) || '';
71
79
  // Convert basic HTML tags to email-safe inline styles
72
80
  const emailHtml = convertHtmlToEmailSafe(html);
73
- return `<p style="font-size: 15px; line-height: 1.8; color: #1a2e26; margin: 0 0 15px 0;">${emailHtml}</p>`;
81
+ // Aligned with BotanicsAndYou paragraph style (text-lg = 18px)
82
+ return `<p style="font-family: 'Georgia', serif; font-size: 18px; line-height: 1.625; color: #1a2e26; margin: 0 0 24px 0;">${emailHtml}</p>`;
74
83
  }
75
84
 
76
85
  case 'image': {
@@ -146,7 +155,7 @@ function renderBlockByType(block: Block, context: EmailRenderContext): string {
146
155
 
147
156
  const imageStyleString = imageStyles.join('; ');
148
157
 
149
- let html = `<table width="100%" cellpadding="0" cellspacing="0" border="0" style="margin: 20px 0;">
158
+ let html = `<table width="100%" cellpadding="0" cellspacing="0" border="0" style="margin: 30px 0;">
150
159
  <tr>
151
160
  <td align="center">
152
161
  <img src="${imageUrl}" alt="${escapeHtml(alt)}" style="${imageStyleString}" />
@@ -155,7 +164,7 @@ function renderBlockByType(block: Block, context: EmailRenderContext): string {
155
164
 
156
165
  if (caption) {
157
166
  html += `<tr>
158
- <td align="center" style="padding-top: 10px; font-size: 12px; color: #666; font-style: italic;">
167
+ <td align="center" style="padding-top: 10px; font-size: 14px; color: #666; font-style: italic; font-family: 'Georgia', serif;">
159
168
  ${escapeHtml(caption)}
160
169
  </td>
161
170
  </tr>`;
@@ -174,10 +183,10 @@ function renderBlockByType(block: Block, context: EmailRenderContext): string {
174
183
  const itemsHtml = items.map((item, idx) => {
175
184
  const text = typeof item === 'string' ? item : (item.html || item.text || '');
176
185
  const emailHtml = convertHtmlToEmailSafe(text);
177
- return `<li style="margin: 8px 0; padding-left: 5px; line-height: 1.8; color: #1a2e26;">${emailHtml}</li>`;
186
+ return `<li style="margin: 12px 0; padding-left: 5px; line-height: 1.625; color: #1a2e26;">${emailHtml}</li>`;
178
187
  }).join('');
179
188
 
180
- return `<${tag} style="margin: 15px 0; padding-left: 25px; color: #1a2e26; font-size: 15px; line-height: 1.8;">${itemsHtml}</${tag}>`;
189
+ return `<${tag} style="font-family: 'Georgia', serif; margin: 24px 0; padding-left: 24px; color: #1a2e26; font-size: 18px; line-height: 1.625;">${itemsHtml}</${tag}>`;
181
190
  }
182
191
 
183
192
  case 'table': {
@@ -189,7 +198,7 @@ function renderBlockByType(block: Block, context: EmailRenderContext): string {
189
198
  }
190
199
 
191
200
  // Build table HTML using email-safe table structure
192
- let tableHtml = `<table width="100%" cellpadding="12" cellspacing="0" border="0" style="margin: 20px 0; border-collapse: collapse; width: 100%;">`;
201
+ let tableHtml = `<table width="100%" cellpadding="12" cellspacing="0" border="0" style="margin: 30px 0; border-collapse: collapse; width: 100%; font-family: 'Georgia', serif;">`;
193
202
 
194
203
  rows.forEach((row, rIdx) => {
195
204
  const isHeaderRow = rIdx === 0 && useHeader;
@@ -201,7 +210,7 @@ function renderBlockByType(block: Block, context: EmailRenderContext): string {
201
210
  tableHtml += `<tr>`;
202
211
  row.forEach((cell) => {
203
212
  const cellHtml = convertHtmlToEmailSafe(cell.html || cell.text || '');
204
- tableHtml += `<${tag} style="padding: 12px; border: 1px solid #e5e7eb; text-align: left; color: ${textColor}; font-size: 15px; line-height: 1.8; background-color: ${bgColor}; font-weight: ${fontWeight};">${cellHtml}</${tag}>`;
213
+ tableHtml += `<${tag} style="padding: 15px; border: 1px solid #e5e7eb; text-align: left; color: ${textColor}; font-size: 16px; line-height: 1.6; background-color: ${bgColor}; font-weight: ${fontWeight};">${cellHtml}</${tag}>`;
205
214
  });
206
215
  tableHtml += `</tr>`;
207
216
  });
@@ -211,7 +220,7 @@ function renderBlockByType(block: Block, context: EmailRenderContext): string {
211
220
  }
212
221
 
213
222
  case 'divider': {
214
- return `<table width="100%" cellpadding="0" cellspacing="0" border="0" style="margin: 30px 0;">
223
+ return `<table width="100%" cellpadding="0" cellspacing="0" border="0" style="margin: 40px 0;">
215
224
  <tr>
216
225
  <td align="center">
217
226
  <table width="40" cellpadding="0" cellspacing="0" border="0">
@@ -228,7 +237,7 @@ function renderBlockByType(block: Block, context: EmailRenderContext): string {
228
237
  // Generic fallback - try to extract text content
229
238
  const text = extractTextFromBlock(block);
230
239
  if (text) {
231
- return `<p style="font-size: 15px; line-height: 1.8; color: #1a2e26; margin: 0 0 15px 0;">${escapeHtml(text)}</p>`;
240
+ return `<p style="font-family: 'Georgia', serif; font-size: 18px; line-height: 1.625; color: #1a2e26; margin: 0 0 24px 0;">${escapeHtml(text)}</p>`;
232
241
  }
233
242
  return '';
234
243
  }
@@ -302,18 +311,20 @@ export function generateNewsletterEmailHtml(
302
311
  },
303
312
  context: EmailRenderContext & {
304
313
  unsubscribeUrl?: string;
314
+ unsubscribeText?: string;
305
315
  footerText?: string;
306
316
  logoUrl?: string;
307
317
  logoAlt?: string;
308
318
  locale?: string;
309
319
  }
310
320
  ): string {
311
- const { baseUrl = '', unsubscribeUrl, footerText, logoUrl, logoAlt = 'Logo', locale = 'en' } = context;
321
+ const { baseUrl = '', unsubscribeUrl, unsubscribeText: propsUnsubscribeText, footerText, logoUrl, logoAlt = 'Logo', locale = 'en' } = context;
312
322
  const contentHtml = renderBlocksToEmail(blocks, context);
313
323
 
314
324
  // Get unsubscribe text based on locale (matching welcome email)
315
325
  const isDutch = locale === 'nl';
316
- const unsubscribeText = isDutch ? 'Afmelden' : 'Unsubscribe';
326
+ const defaultText = isDutch ? 'Afmelden' : 'Unsubscribe';
327
+ const unsubscribeText = propsUnsubscribeText || defaultText;
317
328
 
318
329
  // Build header HTML if logo is provided
319
330
  const headerHtml = logoUrl ? `
@@ -355,8 +366,8 @@ export function generateNewsletterEmailHtml(
355
366
  .content {
356
367
  padding: 0 50px 40px 50px;
357
368
  color: #1a2e26;
358
- line-height: 1.8;
359
- font-size: 15px;
369
+ line-height: 1.625;
370
+ font-size: 18px;
360
371
  }
361
372
  .footer {
362
373
  padding: 40px 50px;
@@ -368,12 +379,12 @@ export function generateNewsletterEmailHtml(
368
379
  border-top: 1px solid #faf9f6;
369
380
  }
370
381
  h1 {
371
- font-weight: normal;
372
- font-style: italic;
373
- font-size: 30px;
382
+ font-weight: bold;
383
+ font-size: 48px;
374
384
  margin-bottom: 30px;
375
385
  color: #1a2e26;
376
- text-align: center;
386
+ text-align: left;
387
+ line-height: 1.2;
377
388
  }
378
389
  .divider {
379
390
  height: 1px;
@@ -0,0 +1,71 @@
1
+ /**
2
+ * Newsletter Plugin Configuration Resolver
3
+ * Centralizes the logic for resolving plugin settings from multiple sources
4
+ */
5
+
6
+ import { ClientBlockDefinition } from '../../types/block';
7
+
8
+ export interface ResolvedConfig {
9
+ customBlocks: ClientBlockDefinition[];
10
+ darkMode: boolean;
11
+ backgroundColors?: {
12
+ light: string;
13
+ dark?: string;
14
+ };
15
+ translations?: Record<string, any>;
16
+ unsubscribeTranslations?: Record<string, string>;
17
+ emailConfig?: {
18
+ logoUrl?: string;
19
+ logoAlt?: string;
20
+ footerText?: string;
21
+ };
22
+ }
23
+
24
+ /**
25
+ * Resolves a specific configuration value from priority sources:
26
+ * 1. LocalStorage (Dev overrides)
27
+ * 2. Component Props
28
+ * 3. Window Global (__JHITS_PLUGIN_PROPS__)
29
+ * 4. Default Value
30
+ */
31
+ export function resolveConfigValue<T>(
32
+ key: string,
33
+ propValue: T | undefined,
34
+ defaultValue: T,
35
+ pluginKey: string = 'plugin-newsletter'
36
+ ): T {
37
+ if (typeof window === 'undefined') return propValue ?? defaultValue;
38
+
39
+ // 1. Check LocalStorage (Dev priority)
40
+ try {
41
+ const saved = localStorage.getItem(`__JHITS_PLUGIN_NEWSLETTER_CONFIG__`);
42
+ if (saved) {
43
+ const config = JSON.parse(saved);
44
+ if (config[key] !== undefined) return config[key];
45
+ }
46
+ } catch (e) {}
47
+
48
+ // 2. Check Props
49
+ if (propValue !== undefined) return propValue;
50
+
51
+ // 3. Check Window Global
52
+ const globalProps = (window as any).__JHITS_PLUGIN_PROPS__?.[pluginKey] || (window as any).__JHITS_PLUGIN_PROPS__?.['newsletter'];
53
+ if (globalProps && globalProps[key] !== undefined) return globalProps[key];
54
+
55
+ // 4. Default
56
+ return defaultValue;
57
+ }
58
+
59
+ /**
60
+ * Resolves all plugin configuration
61
+ */
62
+ export function resolvePluginConfig(props: any): ResolvedConfig {
63
+ return {
64
+ customBlocks: resolveConfigValue('customBlocks', props.customBlocks, []),
65
+ darkMode: resolveConfigValue('darkMode', props.darkMode, true),
66
+ backgroundColors: resolveConfigValue('backgroundColors', props.backgroundColors, undefined),
67
+ translations: resolveConfigValue('translations', props.translations, undefined),
68
+ unsubscribeTranslations: resolveConfigValue('unsubscribeTranslations', props.unsubscribeTranslations, undefined),
69
+ emailConfig: resolveConfigValue('emailConfig', props.emailConfig, undefined),
70
+ };
71
+ }
@@ -0,0 +1,255 @@
1
+ /**
2
+ * Block Registry for Newsletter Plugin
3
+ * Dynamic registry for all block types in system
4
+ * Multi-Tenant Architecture: Blocks are provided by client applications
5
+ *
6
+ * The registry is a singleton that starts empty and is populated by client apps
7
+ */
8
+
9
+ import React from 'react';
10
+ import {
11
+ BlockTypeDefinition,
12
+ ClientBlockDefinition,
13
+ BlockEditProps,
14
+ BlockPreviewProps
15
+ } from '../types/block';
16
+ import {
17
+ Type,
18
+ Heading as HeadingIcon,
19
+ List,
20
+ Image as ImageIcon,
21
+ Table as TableIcon,
22
+ Minus
23
+ } from 'lucide-react';
24
+
25
+ // --- Default Block Components ---
26
+
27
+ // Paragraph
28
+ const DefaultParagraphEdit: React.FC<BlockEditProps> = ({ block, onUpdate }) => (
29
+ <textarea
30
+ value={(block.data.text as string) || ''}
31
+ onChange={(e) => onUpdate({ text: e.target.value })}
32
+ onInput={(e) => {
33
+ const target = e.target as HTMLTextAreaElement;
34
+ target.style.height = 'auto';
35
+ target.style.height = `${target.scrollHeight}px`;
36
+ }}
37
+ placeholder="Type your text..."
38
+ className="w-full bg-transparent border-none outline-none focus:ring-0 p-0 resize-none min-h-[1.5em] font-serif text-[18px] leading-[1.625] text-[#1a2e26] overflow-hidden"
39
+ />
40
+ );
41
+ const DefaultParagraphPreview: React.FC<BlockPreviewProps> = ({ block }) => (
42
+ <p
43
+ className="font-serif text-[18px] leading-[1.625] text-[#1a2e26]"
44
+ dangerouslySetInnerHTML={{ __html: (block.data.html as string) || (block.data.text as string) || '' }}
45
+ />
46
+ );
47
+
48
+ // Heading
49
+ const DefaultHeadingEdit: React.FC<BlockEditProps> = ({ block, onUpdate }) => {
50
+ const level = (block.data.level as number) || 2;
51
+ const fontSize = level === 1 ? '48px' : level === 2 ? '36px' : level === 3 ? '30px' : '20px';
52
+ return (
53
+ <input
54
+ type="text"
55
+ value={(block.data.text as string) || ''}
56
+ onChange={(e) => onUpdate({ text: e.target.value })}
57
+ placeholder="Heading..."
58
+ style={{ fontSize, lineHeight: '1.2' }}
59
+ className="w-full bg-transparent border-none outline-none focus:ring-0 p-0 font-serif font-bold text-[#1a2e26]"
60
+ />
61
+ );
62
+ };
63
+ const DefaultHeadingPreview: React.FC<BlockPreviewProps> = ({ block }) => {
64
+ const level = (block.data.level as number) || 2;
65
+ const fontSize = level === 1 ? '48px' : level === 2 ? '36px' : level === 3 ? '30px' : '20px';
66
+ const Tag = `h${level}` as keyof React.JSX.IntrinsicElements;
67
+ return (
68
+ <Tag
69
+ style={{ fontSize, lineHeight: '1.2' }}
70
+ className="font-serif font-bold text-[#1a2e26]"
71
+ dangerouslySetInnerHTML={{ __html: (block.data.text as string) || '' }}
72
+ />
73
+ );
74
+ };
75
+
76
+ // List
77
+ const DefaultListEdit: React.FC<BlockEditProps> = ({ block, onUpdate }) => {
78
+ const items = Array.isArray(block.data.items) ? block.data.items : [''];
79
+ return (
80
+ <div className="space-y-2">
81
+ {items.map((item, i) => (
82
+ <div key={i} className="flex gap-2">
83
+ <span className="text-neutral-400">•</span>
84
+ <input
85
+ type="text"
86
+ value={typeof item === 'string' ? item : item.text || ''}
87
+ onChange={(e) => {
88
+ const newItems = [...items];
89
+ newItems[i] = e.target.value;
90
+ onUpdate({ items: newItems });
91
+ }}
92
+ className="flex-1 bg-transparent border-none outline-none focus:ring-0 p-0 text-[18px] leading-[1.625] font-serif"
93
+ />
94
+ </div>
95
+ ))}
96
+ <button
97
+ type="button"
98
+ onClick={() => onUpdate({ items: [...items, ''] })}
99
+ className="text-xs text-primary font-bold hover:underline"
100
+ >
101
+ + Add Item
102
+ </button>
103
+ </div>
104
+ );
105
+ };
106
+ const DefaultListPreview: React.FC<BlockPreviewProps> = ({ block }) => {
107
+ const items = Array.isArray(block.data.items) ? block.data.items : [];
108
+ const Tag = block.data.type === 'ol' ? 'ol' : 'ul';
109
+ return (
110
+ <Tag className="list-disc pl-5 space-y-1 font-serif text-[18px] leading-[1.625] text-[#1a2e26]">
111
+ {items.map((item, i) => (
112
+ <li key={i}>{typeof item === 'string' ? item : item.text || ''}</li>
113
+ ))}
114
+ </Tag>
115
+ );
116
+ };
117
+
118
+ // Local interface to avoid import issues
119
+ interface IBlockRegistry {
120
+ register(definition: BlockTypeDefinition): void;
121
+ registerClientBlocks(definitions: ClientBlockDefinition[]): void;
122
+ get(type: string): BlockTypeDefinition | undefined;
123
+ getAll(): BlockTypeDefinition[];
124
+ has(type: string): boolean;
125
+ clear(): void;
126
+ }
127
+
128
+ /**
129
+ * Block Registry Implementation
130
+ * Singleton that manages all block types in the system
131
+ */
132
+ class BlockRegistryImpl implements IBlockRegistry {
133
+ private blocks: Map<string, BlockTypeDefinition> = new Map();
134
+
135
+ constructor() {
136
+ // Register default blocks on instantiation (server & client)
137
+ this.registerDefaultBlocks();
138
+ }
139
+
140
+ private registerDefaultBlocks(): void {
141
+ const defaults: BlockTypeDefinition[] = [
142
+ {
143
+ type: 'paragraph',
144
+ name: 'Paragraph',
145
+ description: 'Simple text paragraph',
146
+ icon: Type,
147
+ category: 'text',
148
+ defaultData: { text: '' },
149
+ components: { Edit: DefaultParagraphEdit, Preview: DefaultParagraphPreview, Icon: Type }
150
+ },
151
+ {
152
+ type: 'heading',
153
+ name: 'Heading',
154
+ description: 'Section heading',
155
+ icon: HeadingIcon,
156
+ category: 'text',
157
+ defaultData: { text: '', level: 2 },
158
+ components: { Edit: DefaultHeadingEdit, Preview: DefaultHeadingPreview, Icon: HeadingIcon }
159
+ },
160
+ {
161
+ type: 'list',
162
+ name: 'List',
163
+ description: 'Bullet or numbered list',
164
+ icon: List,
165
+ category: 'text',
166
+ defaultData: { items: [''], type: 'ul' },
167
+ components: { Edit: DefaultListEdit, Preview: DefaultListPreview, Icon: List }
168
+ },
169
+ {
170
+ type: 'divider',
171
+ name: 'Divider',
172
+ description: 'Horizontal separator',
173
+ icon: Minus,
174
+ category: 'layout',
175
+ defaultData: {},
176
+ components: {
177
+ Edit: () => <hr className="my-4 border-t border-neutral-200" />,
178
+ Preview: () => <hr className="my-4 border-t border-neutral-200" />,
179
+ Icon: Minus
180
+ }
181
+ }
182
+ ];
183
+
184
+ for (const def of defaults) {
185
+ // Use register to ensure it's also added to window
186
+ this.register(def);
187
+ }
188
+ }
189
+
190
+ register(definition: BlockTypeDefinition): void {
191
+ this.blocks.set(definition.type, definition);
192
+
193
+ // Also attach to window for cross-context access
194
+ if (typeof window !== 'undefined') {
195
+ if (!(window as any).__JHITS_NEWSLETTER_REGISTRY__) {
196
+ (window as any).__JHITS_NEWSLETTER_REGISTRY__ = new Map();
197
+ }
198
+ (window as any).__JHITS_NEWSLETTER_REGISTRY__.set(definition.type, definition);
199
+ }
200
+ }
201
+
202
+ /**
203
+ * Register multiple client blocks at once
204
+ * This is the primary method for client applications to register their blocks
205
+ */
206
+ registerClientBlocks(definitions: ClientBlockDefinition[]): void {
207
+ for (const def of definitions) {
208
+ if (!def.type || !def.name || !def.components) continue;
209
+ if (!def.components.Edit || !def.components.Preview) continue;
210
+
211
+ const blockDefinition: BlockTypeDefinition = {
212
+ type: def.type,
213
+ name: def.name,
214
+ description: def.description,
215
+ icon: def.icon || def.components.Icon,
216
+ defaultData: def.defaultData,
217
+ validate: def.validate,
218
+ isContainer: def.isContainer,
219
+ allowedChildren: def.allowedChildren,
220
+ category: def.category || 'custom',
221
+ components: def.components,
222
+ };
223
+
224
+ this.register(blockDefinition);
225
+ }
226
+ }
227
+
228
+ get(type: string): BlockTypeDefinition | undefined {
229
+ const block = this.blocks.get(type);
230
+ if (block) return block;
231
+
232
+ if (typeof window !== 'undefined' && (window as any).__JHITS_NEWSLETTER_REGISTRY__) {
233
+ return (window as any).__JHITS_NEWSLETTER_REGISTRY__.get(type);
234
+ }
235
+
236
+ return undefined;
237
+ }
238
+
239
+ getAll(): BlockTypeDefinition[] {
240
+ return Array.from(this.blocks.values());
241
+ }
242
+
243
+ has(type: string): boolean {
244
+ return this.blocks.has(type);
245
+ }
246
+
247
+ clear(): void {
248
+ this.blocks.clear();
249
+ this.registerDefaultBlocks(); // Re-add defaults after clear
250
+ }
251
+ }
252
+
253
+ // Export singleton instance
254
+ export const BlockRegistry = new BlockRegistryImpl();
255
+ export const blockRegistry = BlockRegistry;
@@ -40,8 +40,18 @@ export interface EditorProviderProps {
40
40
  /** Background color for dark mode (optional) */
41
41
  dark?: string;
42
42
  };
43
+ /** Localized strings for modular blocks in the editor */
44
+ translations?: Record<string, any>;
43
45
  /** If true, this editor is for the welcome email */
44
46
  isWelcomeEmail?: boolean;
47
+ /** Email configuration (logo, footer, etc.) */
48
+ emailConfig?: {
49
+ logoUrl?: string;
50
+ logoAlt?: string;
51
+ footerText?: string;
52
+ };
53
+ /** Global translations for unsubscribe text */
54
+ unsubscribeTranslations?: Record<string, string>;
45
55
  }
46
56
 
47
57
  /**
@@ -56,23 +66,44 @@ export function EditorProvider({
56
66
  customBlocks = [],
57
67
  darkMode = true,
58
68
  backgroundColors,
59
- isWelcomeEmail
69
+ translations,
70
+ isWelcomeEmail,
71
+ emailConfig,
72
+ unsubscribeTranslations
60
73
  }: EditorProviderProps) {
61
- // Register client blocks on mount
74
+ const [state, dispatch] = useReducer(
75
+ editorReducer,
76
+ { ...initialEditorState, ...initialState }
77
+ );
78
+
79
+ // Track registered blocks in state to trigger re-renders
80
+ const [registeredBlockTypes, setRegisteredBlockBlocks] = useState<string[]>(() =>
81
+ BlockRegistry.getAll().map(b => b.type)
82
+ );
83
+
84
+ // Register client blocks on mount or when they change
62
85
  useEffect(() => {
63
86
  if (customBlocks && customBlocks.length > 0) {
64
87
  try {
65
- customBlocks.forEach(block => BlockRegistry.register(block));
88
+ BlockRegistry.registerClientBlocks(customBlocks);
89
+ // Update state to trigger re-render
90
+ setRegisteredBlockBlocks(BlockRegistry.getAll().map(b => b.type));
66
91
  } catch (error) {
67
92
  console.error('[NewsletterEditorContext] Failed to register custom blocks:', error);
68
93
  }
69
94
  }
70
95
  }, [customBlocks]);
71
96
 
72
- const [state, dispatch] = useReducer(
73
- editorReducer,
74
- { ...initialEditorState, ...initialState }
75
- );
97
+ // Periodically check for new blocks in global registry (fallback for late loads)
98
+ useEffect(() => {
99
+ const interval = setInterval(() => {
100
+ const currentTypes = BlockRegistry.getAll().map(b => b.type);
101
+ if (currentTypes.length !== registeredBlockTypes.length) {
102
+ setRegisteredBlockBlocks(currentTypes);
103
+ }
104
+ }, 1000);
105
+ return () => clearInterval(interval);
106
+ }, [registeredBlockTypes.length]);
76
107
 
77
108
  // Use a ref to always have access to the latest state in callbacks
78
109
  const stateRef = useRef(state);
@@ -244,6 +275,10 @@ export function EditorProvider({
244
275
  dispatch,
245
276
  darkMode,
246
277
  backgroundColors,
278
+ translations,
279
+ emailConfig,
280
+ unsubscribeTranslations,
281
+ registeredBlockTypes,
247
282
  helpers: {
248
283
  addBlock,
249
284
  updateBlock,
@@ -259,7 +294,7 @@ export function EditorProvider({
259
294
  canUndo: historyIndex > 0 && history.length > 0,
260
295
  canRedo: historyIndex < history.length - 1,
261
296
  }),
262
- [state, dispatch, darkMode, backgroundColors, addBlock, updateBlock, deleteBlock, duplicateBlock, moveBlock, loadNewsletter, resetEditor, save, undo, redo, historyIndex, history.length]
297
+ [state, dispatch, darkMode, backgroundColors, translations, emailConfig, unsubscribeTranslations, registeredBlockTypes, addBlock, updateBlock, deleteBlock, duplicateBlock, moveBlock, loadNewsletter, resetEditor, save, undo, redo, historyIndex, history.length]
263
298
  );
264
299
 
265
300
  return <EditorContext.Provider value={value}>{children}</EditorContext.Provider>;
@@ -90,6 +90,22 @@ export interface EditorContextValue {
90
90
  dark?: string;
91
91
  };
92
92
 
93
+ /** Localized strings for modular blocks in the editor */
94
+ translations?: Record<string, any>;
95
+
96
+ /** Email configuration (logo, footer, etc.) */
97
+ emailConfig?: {
98
+ logoUrl?: string;
99
+ logoAlt?: string;
100
+ footerText?: string;
101
+ };
102
+
103
+ /** Global translations for unsubscribe text */
104
+ unsubscribeTranslations?: Record<string, string>;
105
+
106
+ /** List of registered block types (used to trigger re-renders) */
107
+ registeredBlockTypes: string[];
108
+
93
109
  /** Helper functions for common operations */
94
110
  helpers: {
95
111
  /** Add a new block (supports nested containers via containerId) */
@@ -70,6 +70,16 @@ export interface BlockEditProps {
70
70
 
71
71
  /** Focus mode state */
72
72
  focusMode?: boolean;
73
+
74
+ /** Additional rendering context */
75
+ context?: {
76
+ /** Site ID */
77
+ siteId?: string;
78
+ /** Locale */
79
+ locale?: string;
80
+ /** Custom render props (e.g. translations) */
81
+ [key: string]: unknown;
82
+ };
73
83
 
74
84
  /** Child blocks (for container blocks like Section, Columns) */
75
85
  childBlocks?: Block[];
@@ -79,6 +79,9 @@ export interface NewsletterMetadata {
79
79
  /** Preview text */
80
80
  previewText?: string;
81
81
 
82
+ /** Unsubscribe text (localized) */
83
+ unsubscribeText?: string;
84
+
82
85
  /** Language code */
83
86
  lang?: string;
84
87