@jhits/plugin-newsletter 0.0.15 → 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 (90) 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/newsletters.d.ts.map +1 -1
  4. package/dist/api/handlers/newsletters.js +33 -16
  5. package/dist/api/handlers/send-newsletter.d.ts.map +1 -1
  6. package/dist/api/handlers/send-newsletter.js +54 -6
  7. package/dist/api/handlers/settings.d.ts.map +1 -1
  8. package/dist/api/handlers/settings.js +51 -1
  9. package/dist/index.d.ts +27 -10
  10. package/dist/index.d.ts.map +1 -1
  11. package/dist/index.js +15 -122
  12. package/dist/lib/blocks/BlockRenderer.d.ts.map +1 -1
  13. package/dist/lib/blocks/BlockRenderer.js +14 -2
  14. package/dist/lib/email/EmailRenderer.d.ts +1 -0
  15. package/dist/lib/email/EmailRenderer.d.ts.map +1 -1
  16. package/dist/lib/email/EmailRenderer.js +31 -19
  17. package/dist/lib/utils/config-resolver.d.ts +33 -0
  18. package/dist/lib/utils/config-resolver.d.ts.map +1 -0
  19. package/dist/lib/utils/config-resolver.js +47 -0
  20. package/dist/registry/BlockRegistry.d.ts +9 -1
  21. package/dist/registry/BlockRegistry.d.ts.map +1 -1
  22. package/dist/registry/BlockRegistry.js +126 -8
  23. package/dist/state/EditorContext.d.ts +11 -1
  24. package/dist/state/EditorContext.d.ts.map +1 -1
  25. package/dist/state/EditorContext.js +23 -5
  26. package/dist/state/types.d.ts +12 -0
  27. package/dist/state/types.d.ts.map +1 -1
  28. package/dist/types/block.d.ts +9 -0
  29. package/dist/types/block.d.ts.map +1 -1
  30. package/dist/types/newsletter.d.ts +4 -0
  31. package/dist/types/newsletter.d.ts.map +1 -1
  32. package/dist/views/CanvasEditor/BlockWrapper.d.ts.map +1 -1
  33. package/dist/views/CanvasEditor/BlockWrapper.js +24 -3
  34. package/dist/views/CanvasEditor/CanvasEditorView.d.ts.map +1 -1
  35. package/dist/views/CanvasEditor/CanvasEditorView.js +77 -17
  36. package/dist/views/CanvasEditor/EditorBody.d.ts.map +1 -1
  37. package/dist/views/CanvasEditor/EditorBody.js +1 -1
  38. package/dist/views/CanvasEditor/components/EditorCanvas.d.ts.map +1 -1
  39. package/dist/views/CanvasEditor/components/EditorCanvas.js +158 -100
  40. package/dist/views/CanvasEditor/components/EditorSidebar.d.ts +3 -1
  41. package/dist/views/CanvasEditor/components/EditorSidebar.d.ts.map +1 -1
  42. package/dist/views/CanvasEditor/components/EditorSidebar.js +3 -3
  43. package/dist/views/CanvasEditor/hooks/useRegisteredBlocks.d.ts +1 -1
  44. package/dist/views/CanvasEditor/hooks/useRegisteredBlocks.d.ts.map +1 -1
  45. package/dist/views/CanvasEditor/hooks/useRegisteredBlocks.js +6 -40
  46. package/dist/views/NewsletterManager.d.ts.map +1 -1
  47. package/dist/views/NewsletterManager.js +87 -5
  48. package/dist/views/components/DomainPromptModal.d.ts +13 -0
  49. package/dist/views/components/DomainPromptModal.d.ts.map +1 -0
  50. package/dist/views/components/DomainPromptModal.js +58 -0
  51. package/dist/views/components/NewsletterCard.d.ts +16 -0
  52. package/dist/views/components/NewsletterCard.d.ts.map +1 -0
  53. package/dist/views/components/NewsletterCard.js +94 -0
  54. package/dist/views/components/NewsletterGrid.d.ts +16 -0
  55. package/dist/views/components/NewsletterGrid.d.ts.map +1 -0
  56. package/dist/views/components/NewsletterGrid.js +13 -0
  57. package/dist/views/components/SendNewsletterModal.d.ts.map +1 -1
  58. package/dist/views/components/SendNewsletterModal.js +91 -22
  59. package/dist/views/components/SmtpSettingsModal.d.ts.map +1 -1
  60. package/dist/views/components/SmtpSettingsModal.js +10 -0
  61. package/dist/views/components/TestEmailModal.d.ts.map +1 -1
  62. package/dist/views/components/TestEmailModal.js +86 -17
  63. package/package.json +53 -9
  64. package/src/api/email-utils.ts +53 -4
  65. package/src/api/handlers/newsletters.ts +40 -20
  66. package/src/api/handlers/send-newsletter.ts +65 -6
  67. package/src/api/handlers/settings.ts +60 -2
  68. package/src/index.tsx +49 -155
  69. package/src/lib/blocks/BlockRenderer.tsx +16 -2
  70. package/src/lib/email/EmailRenderer.tsx +31 -20
  71. package/src/lib/utils/config-resolver.ts +71 -0
  72. package/src/registry/BlockRegistry.tsx +255 -0
  73. package/src/state/EditorContext.tsx +43 -8
  74. package/src/state/types.ts +16 -0
  75. package/src/types/block.ts +10 -0
  76. package/src/types/newsletter.ts +5 -0
  77. package/src/views/CanvasEditor/BlockWrapper.tsx +27 -2
  78. package/src/views/CanvasEditor/CanvasEditorView.tsx +142 -61
  79. package/src/views/CanvasEditor/EditorBody.tsx +17 -13
  80. package/src/views/CanvasEditor/components/EditorCanvas.tsx +178 -115
  81. package/src/views/CanvasEditor/components/EditorSidebar.tsx +57 -2
  82. package/src/views/CanvasEditor/hooks/useRegisteredBlocks.ts +6 -45
  83. package/src/views/NewsletterManager.tsx +164 -6
  84. package/src/views/components/DomainPromptModal.tsx +160 -0
  85. package/src/views/components/NewsletterCard.tsx +212 -0
  86. package/src/views/components/NewsletterGrid.tsx +48 -0
  87. package/src/views/components/SendNewsletterModal.tsx +270 -184
  88. package/src/views/components/SmtpSettingsModal.tsx +11 -0
  89. package/src/views/components/TestEmailModal.tsx +235 -149
  90. package/src/registry/BlockRegistry.ts +0 -53
@@ -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
 
@@ -160,6 +163,8 @@ export interface NewsletterListItem {
160
163
  recipientCount?: number;
161
164
  hidden?: boolean;
162
165
  sendHistory?: SendHistoryEntry[];
166
+ availableLanguages?: string[];
167
+ languages?: NewsletterLanguages;
163
168
  }
164
169
 
165
170
  /**
@@ -42,7 +42,10 @@ export function BlockWrapper({
42
42
  onAddBlockBelow,
43
43
  }: BlockWrapperProps) {
44
44
  const [isHovered, setIsHovered] = useState(false);
45
- const { state } = useEditor();
45
+ const { state, translations, registeredBlockTypes } = useEditor();
46
+
47
+ // We use the block definition from registry
48
+ // The component will re-render when registeredBlockTypes changes
46
49
  const blockDefinition = blockRegistry.get(block.type);
47
50
 
48
51
  // Check if this is a container block
@@ -65,6 +68,20 @@ export function BlockWrapper({
65
68
 
66
69
  const EditComponent = blockDefinition.components.Edit;
67
70
 
71
+ // Helper to check if block is empty (for easier deletion)
72
+ const isBlockEmpty = () => {
73
+ if (block.type === 'paragraph' || block.type === 'heading') {
74
+ const text = (block.data.text as string) || '';
75
+ const html = (block.data.html as string) || '';
76
+ return text.trim() === '' && html.trim() === '';
77
+ }
78
+ if (block.type === 'list') {
79
+ const items = (block.data.items as any[]) || [];
80
+ return items.length === 0 || (items.length === 1 && (typeof items[0] === 'string' ? items[0].trim() === '' : items[0].text?.trim() === ''));
81
+ }
82
+ return false;
83
+ };
84
+
68
85
  // Store block ID when hovering for paste context
69
86
  useEffect(() => {
70
87
  if (isHovered) {
@@ -103,6 +120,10 @@ export function BlockWrapper({
103
120
  onDelete={onDelete}
104
121
  isSelected={state.selectedBlockId === block.id}
105
122
  childBlocks={childBlocks}
123
+ context={{
124
+ locale: state.metadata.lang || 'en',
125
+ translations
126
+ }}
106
127
  />
107
128
  </div>
108
129
  </SlashCommandDetector>
@@ -114,6 +135,10 @@ export function BlockWrapper({
114
135
  onDelete={onDelete}
115
136
  isSelected={state.selectedBlockId === block.id}
116
137
  childBlocks={childBlocks}
138
+ context={{
139
+ locale: state.metadata.lang || 'en',
140
+ translations
141
+ }}
117
142
  />
118
143
  </div>
119
144
  )}
@@ -128,7 +153,7 @@ export function BlockWrapper({
128
153
  <button
129
154
  onClick={(e) => {
130
155
  e.stopPropagation();
131
- if (confirm('Delete this block?')) {
156
+ if (isBlockEmpty() || confirm('Delete this block?')) {
132
157
  onDelete();
133
158
  }
134
159
  }}