@marlinjai/email-editor-ui 0.2.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.
@@ -0,0 +1,514 @@
1
+ import React, { Provider } from 'react';
2
+ import { TemplateSnapshotIn, TemplateSnapshotOut, BlockRegistryImpl, PrebuiltTemplateRegistry, RootStoreInstance, EditorUIInstance, BlockInstance, ColumnInstance, SectionInstance, TemplateInstance, BackgroundGradient, SubColumnInstance, BlockDefinition, PrebuiltTemplate } from '@marlinjai/email-editor-core';
3
+
4
+ /**
5
+ * What the editor tells the host when it asks for an image.
6
+ */
7
+ interface ImageRequest {
8
+ /** The id of the block the image is for (or the section or wrapper, for a background image) */
9
+ blockId: string;
10
+ /** What is asking: a block type (`image`), or `section` or `wrapper` for a background image */
11
+ blockType: string;
12
+ /** The image URL the block currently shows, if any */
13
+ currentUrl?: string;
14
+ /** The alt text the block currently has, if any */
15
+ currentAlt?: string;
16
+ }
17
+ /**
18
+ * The image the host hands back: a URL the email can load (it must be
19
+ * publicly reachable from a recipient's mail client), and optional alt text.
20
+ */
21
+ interface RequestedImage {
22
+ url: string;
23
+ alt?: string;
24
+ }
25
+ /**
26
+ * Called when the user asks to choose an image. Open your own picker or
27
+ * uploader, then resolve with the chosen image, or with `null` when the user
28
+ * cancels (the block is left unchanged). A rejected promise is shown to the
29
+ * user as an inline error next to the button, using the error's message.
30
+ */
31
+ type OnRequestImage = (request: ImageRequest) => Promise<RequestedImage | null>;
32
+ /**
33
+ * Called when somebody asks to keep a section for reuse. The editor has already
34
+ * asked for the name and checked it against `savedSectionNames`; what is handed
35
+ * over is a plain snapshot of the section, ids and all.
36
+ *
37
+ * Resolve when it is stored. A rejected promise is shown in the dialog, using
38
+ * the error's message, and the dialog stays open so the name is not lost.
39
+ *
40
+ * Without it no "Save as a section" action appears, rather than an action that
41
+ * fails: a host that cannot store one should not offer to.
42
+ */
43
+ type OnSaveSection = (section: Record<string, unknown>, name: string) => Promise<void>;
44
+ interface EditorHostHooks {
45
+ onRequestImage?: OnRequestImage;
46
+ onSaveSection?: OnSaveSection;
47
+ /**
48
+ * The names this workspace has already used, so the dialog can say that a
49
+ * name is taken before the save is attempted rather than after it comes back
50
+ * as a conflict.
51
+ */
52
+ savedSectionNames?: readonly string[];
53
+ /**
54
+ * The editor's root element. Dialogs portal into it rather than into
55
+ * `document.body`, so they stay inside the scoped stylesheet and inherit
56
+ * the theme tokens set on the root.
57
+ */
58
+ portalContainer?: HTMLElement | null;
59
+ }
60
+ declare function EditorHostProvider({ onRequestImage, onSaveSection, savedSectionNames, portalContainer, children, }: EditorHostHooks & {
61
+ children: React.ReactNode;
62
+ }): React.JSX.Element;
63
+ /**
64
+ * The hooks the host supplied. Outside an editor every hook is undefined, so
65
+ * components fall back to their built-in behavior.
66
+ */
67
+ declare function useEditorHost(): EditorHostHooks;
68
+
69
+ interface EmailEditorProps {
70
+ /** Initial template data */
71
+ initialTemplate?: TemplateSnapshotIn;
72
+ /** Called when template changes */
73
+ onChange?: (template: TemplateSnapshotOut) => void;
74
+ /** Block registry for available block types */
75
+ blockRegistry: BlockRegistryImpl;
76
+ /** Pre-built template registry */
77
+ prebuiltRegistry?: PrebuiltTemplateRegistry;
78
+ /** Called when save button is clicked */
79
+ onSave?: () => void;
80
+ /** Called when export is requested */
81
+ onExport?: (template: TemplateSnapshotOut) => void;
82
+ /** Called when back button is clicked to navigate away from editor */
83
+ onNavigateBack?: () => void;
84
+ /**
85
+ * Supply images from the host's own picker or uploader. When set, the image
86
+ * block's inspector shows a "Choose image" button that calls it instead of a
87
+ * URL field. Resolve with `null` to cancel; a rejection is shown inline.
88
+ */
89
+ onRequestImage?: OnRequestImage;
90
+ /**
91
+ * Keep a section for reuse. When set, a selected section's controls carry a
92
+ * "Save as a section" action, which asks for a name and calls this. Without
93
+ * it no action appears, rather than one that fails.
94
+ */
95
+ onSaveSection?: OnSaveSection;
96
+ /** The names already saved, so the dialog can catch a clash before the save. */
97
+ savedSectionNames?: readonly string[];
98
+ /** Extra class names for the editor's root element (which carries `ee-root`) */
99
+ className?: string;
100
+ /**
101
+ * Inline styles for the editor's root element. Use it to set design tokens,
102
+ * e.g. `{ '--ee-accent': '#0f766e' }`. The editor fills its container's
103
+ * height, so give the container one.
104
+ */
105
+ style?: React.CSSProperties;
106
+ }
107
+ /**
108
+ * EmailEditor - Main email editor component
109
+ *
110
+ * Uses MobX State Tree for instant visual feedback on property changes.
111
+ * MJML compilation only happens on export, not during editing.
112
+ */
113
+ declare const EmailEditor: React.FunctionComponent<EmailEditorProps>;
114
+
115
+ /**
116
+ * Provider component for the store
117
+ */
118
+ declare const StoreProvider: Provider<RootStoreInstance | null>;
119
+ /**
120
+ * Hook to access the full store
121
+ * @throws Error if used outside of StoreProvider
122
+ */
123
+ declare function useStore(): RootStoreInstance;
124
+ /**
125
+ * Hook to access just the template
126
+ */
127
+ declare function useTemplate(): TemplateInstance;
128
+ /**
129
+ * Hook to access just the editor UI store
130
+ */
131
+ declare function useEditorUI(): EditorUIInstance;
132
+ /**
133
+ * Hook to get the selected block (convenience)
134
+ */
135
+ declare function useSelectedBlock(): BlockInstance | undefined;
136
+ /**
137
+ * Hook to get the selected section (convenience)
138
+ */
139
+ declare function useSelectedSection(): SectionInstance | undefined;
140
+ /**
141
+ * Hook to get the selected column (convenience)
142
+ */
143
+ declare function useSelectedColumn(): ColumnInstance | undefined;
144
+
145
+ interface EmailRendererProps {
146
+ /** Additional class names */
147
+ className?: string;
148
+ }
149
+ /**
150
+ * EmailRenderer - Root renderer for email templates
151
+ *
152
+ * This is the main preview component that renders the entire email template
153
+ * using React components instead of MJML compilation.
154
+ *
155
+ * Key benefits:
156
+ * - Instant updates (<16ms) on property changes
157
+ * - Fine-grained reactivity (only changed blocks re-render)
158
+ * - No iframe needed
159
+ * - MJML compilation only happens on export
160
+ */
161
+ declare const EmailRenderer: (({ className }: EmailRendererProps) => React.JSX.Element) & {
162
+ displayName: string;
163
+ };
164
+ /**
165
+ * Hook to get the email renderer width
166
+ */
167
+ declare function usePreviewWidth(): number;
168
+
169
+ interface SectionRendererProps {
170
+ section: SectionInstance;
171
+ sectionIndex: number;
172
+ }
173
+ /**
174
+ * SectionRenderer - Renders a section with its columns
175
+ *
176
+ * Key features:
177
+ * - Table-based layout for email compatibility
178
+ * - Section selection and hover states
179
+ * - Column distribution
180
+ * - Background styling
181
+ */
182
+ declare const SectionRenderer: (({ section, sectionIndex }: SectionRendererProps) => React.JSX.Element) & {
183
+ displayName: string;
184
+ };
185
+
186
+ interface ColumnRendererProps {
187
+ column: ColumnInstance;
188
+ section: SectionInstance;
189
+ columnIndex: number;
190
+ }
191
+ /**
192
+ * ColumnRenderer - Renders a column within a section
193
+ *
194
+ * Key features:
195
+ * - Renders blocks in order
196
+ * - Handles column selection
197
+ * - Shows drop zones during drag
198
+ * - Applies column styling
199
+ */
200
+ declare const ColumnRenderer: (({ column, section, columnIndex }: ColumnRendererProps) => React.JSX.Element) & {
201
+ displayName: string;
202
+ };
203
+
204
+ interface BlockRendererProps {
205
+ block: BlockInstance;
206
+ /** Whether this block is in inline editing mode */
207
+ isEditing?: boolean;
208
+ }
209
+ /**
210
+ * BlockRenderer - Routes blocks to their specific renderer components
211
+ *
212
+ * Key features:
213
+ * - Wraps blocks with selection/hover UI
214
+ * - Handles click to select
215
+ * - Shows hidden block placeholder
216
+ * - Routes to appropriate block renderer
217
+ */
218
+ declare const BlockRenderer: (({ block, isEditing }: BlockRendererProps) => React.JSX.Element) & {
219
+ displayName: string;
220
+ };
221
+
222
+ interface TextBlockProps {
223
+ block: BlockInstance;
224
+ }
225
+ /**
226
+ * TextBlock - Renders a text block with inline WYSIWYG editing
227
+ *
228
+ * Key implementation details:
229
+ * - Uses contenteditable for direct text editing when selected
230
+ * - Does NOT use dangerouslySetInnerHTML when editing (causes cursor reset)
231
+ * - Syncs content to MST only on blur (not on every keystroke)
232
+ * - Stops keyboard event propagation to prevent block deletion on backspace
233
+ */
234
+ declare const TextBlock: (({ block }: TextBlockProps) => React.JSX.Element) & {
235
+ displayName: string;
236
+ };
237
+
238
+ interface ImageBlockProps {
239
+ block: BlockInstance;
240
+ }
241
+ /**
242
+ * ImageBlock - Renders an image block in the email preview
243
+ */
244
+ declare const ImageBlock: (({ block }: ImageBlockProps) => React.JSX.Element) & {
245
+ displayName: string;
246
+ };
247
+
248
+ interface ButtonBlockProps {
249
+ block: BlockInstance;
250
+ }
251
+ /**
252
+ * ButtonBlock - Renders a button/CTA block in the email preview
253
+ */
254
+ declare const ButtonBlock: (({ block }: ButtonBlockProps) => React.JSX.Element) & {
255
+ displayName: string;
256
+ };
257
+
258
+ interface DividerBlockProps {
259
+ block: BlockInstance;
260
+ }
261
+ /**
262
+ * DividerBlock - Renders a horizontal divider in the email preview
263
+ */
264
+ declare const DividerBlock: (({ block }: DividerBlockProps) => React.JSX.Element) & {
265
+ displayName: string;
266
+ };
267
+
268
+ interface SpacerBlockProps {
269
+ block: BlockInstance;
270
+ }
271
+ /**
272
+ * SpacerBlock - Renders vertical spacing in the email preview
273
+ */
274
+ declare const SpacerBlock: (({ block }: SpacerBlockProps) => React.JSX.Element) & {
275
+ displayName: string;
276
+ };
277
+
278
+ interface SocialBlockProps {
279
+ block: BlockInstance;
280
+ }
281
+ /**
282
+ * SocialBlock - Renders social media icons in the email preview
283
+ */
284
+ declare const SocialBlock: (({ block }: SocialBlockProps) => React.JSX.Element) & {
285
+ displayName: string;
286
+ };
287
+
288
+ interface HeroBlockProps {
289
+ block: BlockInstance;
290
+ }
291
+ /**
292
+ * HeroBlock - Renders a hero section with background image
293
+ */
294
+ declare const HeroBlock: (({ block }: HeroBlockProps) => React.JSX.Element) & {
295
+ displayName: string;
296
+ };
297
+
298
+ interface RawBlockProps {
299
+ block: BlockInstance;
300
+ }
301
+ /**
302
+ * RawBlock - Renders custom HTML content
303
+ */
304
+ declare const RawBlock: (({ block }: RawBlockProps) => React.JSX.Element) & {
305
+ displayName: string;
306
+ };
307
+
308
+ interface PropertyInspectorProps {
309
+ onDeleteBlock?: (blockId: string) => void;
310
+ }
311
+ /**
312
+ * PropertyInspector - Property editor panel
313
+ *
314
+ * Updates MST models directly for instant visual feedback.
315
+ * No intermediate state or MJML recompilation needed.
316
+ */
317
+ declare const PropertyInspector: React.FunctionComponent<PropertyInspectorProps>;
318
+
319
+ /**
320
+ * Normalize spacing values to ensure valid CSS units.
321
+ *
322
+ * CSS ignores unitless values (except 0), so we auto-append "px" for bare numbers.
323
+ * MJML supports: px, % (em/rem have poor email client support)
324
+ *
325
+ * @example
326
+ * normalizeSpacingValue("10") // "10px" - bare number gets px
327
+ * normalizeSpacingValue("10px") // "10px" - already has unit
328
+ * normalizeSpacingValue("5%") // "5%" - percent is valid
329
+ * normalizeSpacingValue("0") // "0" - zero works without unit
330
+ * normalizeSpacingValue("") // undefined - empty clears value
331
+ */
332
+ declare function normalizeSpacingValue(value: string): string | undefined;
333
+ /**
334
+ * Formatting toolbar for text editing
335
+ * Uses execCommand for contenteditable WYSIWYG editing
336
+ *
337
+ * Important: Uses onMouseDown with preventDefault to keep focus
338
+ * in the contenteditable text block while clicking buttons.
339
+ */
340
+ declare function FormattingToolbar(): React.JSX.Element;
341
+ /**
342
+ * Text input field
343
+ */
344
+ declare function TextField({ label, value, onChange, placeholder, hint, error, }: {
345
+ label: string;
346
+ value: string;
347
+ onChange: (value: string) => void;
348
+ placeholder?: string;
349
+ /** A line under the field saying what it does. */
350
+ hint?: string;
351
+ /** Shown instead of the hint, and marks the field invalid. */
352
+ error?: string;
353
+ }): React.JSX.Element;
354
+ /**
355
+ * Theme color swatch type
356
+ */
357
+ interface ThemeColorSwatch {
358
+ name: string;
359
+ value: string;
360
+ }
361
+ /**
362
+ * Color picker with text input and theme color swatches
363
+ */
364
+ declare function ColorField({ label, value, onChange, allowEmpty, themeColors, }: {
365
+ label: string;
366
+ value: string;
367
+ onChange: (value: string) => void;
368
+ allowEmpty?: boolean;
369
+ themeColors?: ThemeColorSwatch[];
370
+ }): React.JSX.Element;
371
+ /**
372
+ * Select dropdown field
373
+ */
374
+ declare function SelectField({ label, value, options, onChange, }: {
375
+ label: string;
376
+ value: string;
377
+ options: string[];
378
+ onChange: (value: string) => void;
379
+ }): React.JSX.Element;
380
+ /**
381
+ * Text alignment buttons
382
+ */
383
+ declare function AlignmentField({ value, onChange, }: {
384
+ value: string;
385
+ onChange: (value: string) => void;
386
+ }): React.JSX.Element;
387
+ /**
388
+ * Checkbox field
389
+ */
390
+ declare function CheckboxField({ label, checked, onChange, disabled, hint, }: {
391
+ label: string;
392
+ checked: boolean;
393
+ onChange: () => void;
394
+ disabled?: boolean;
395
+ /** A line under the checkbox, e.g. why it is disabled. */
396
+ hint?: string;
397
+ }): React.JSX.Element;
398
+ /**
399
+ * 4-sided spacing input (top, right, bottom, left)
400
+ *
401
+ * Automatically normalizes values to ensure valid CSS units.
402
+ * Bare numbers like "10" become "10px".
403
+ */
404
+ declare function SpacingField({ label, top, right, bottom, left, onChange, placeholders, hint, }: {
405
+ label: string;
406
+ top?: string;
407
+ right?: string;
408
+ bottom?: string;
409
+ left?: string;
410
+ onChange: (side: 'top' | 'right' | 'bottom' | 'left', value: string | undefined) => void;
411
+ /** Placeholders per side, e.g. what the mail uses when a side is not set. */
412
+ placeholders?: Partial<Record<'top' | 'right' | 'bottom' | 'left', string>>;
413
+ hint?: string;
414
+ }): React.JSX.Element;
415
+ /**
416
+ * Range slider with value display
417
+ */
418
+ declare function RangeField({ label, value, min, max, onChange, unit, }: {
419
+ label: string;
420
+ value: number;
421
+ min: number;
422
+ max: number;
423
+ onChange: (value: number) => void;
424
+ unit?: string;
425
+ }): React.JSX.Element;
426
+ /**
427
+ * Button group for selecting from options
428
+ */
429
+ declare function ButtonGroupField({ label, value, options, onChange, }: {
430
+ label: string;
431
+ value: string | number;
432
+ options: (string | number)[];
433
+ onChange: (value: string | number) => void;
434
+ }): React.JSX.Element;
435
+ /**
436
+ * Gradient editor field
437
+ */
438
+ declare function GradientField({ value, onChange, }: {
439
+ value: BackgroundGradient | undefined;
440
+ onChange: (gradient: BackgroundGradient | undefined) => void;
441
+ }): React.JSX.Element;
442
+
443
+ interface BlockPropertiesProps {
444
+ block: BlockInstance;
445
+ onDelete: () => void;
446
+ }
447
+ /**
448
+ * Main block properties panel - routes to type-specific panels
449
+ */
450
+ declare const BlockProperties: React.FunctionComponent<BlockPropertiesProps>;
451
+
452
+ interface SectionPropertiesProps {
453
+ section: SectionInstance;
454
+ }
455
+ /**
456
+ * Section properties panel
457
+ */
458
+ declare const SectionProperties: React.FunctionComponent<SectionPropertiesProps>;
459
+
460
+ interface ColumnPropertiesProps {
461
+ column: ColumnInstance;
462
+ }
463
+ /**
464
+ * Column properties panel
465
+ */
466
+ declare const ColumnProperties: React.FunctionComponent<ColumnPropertiesProps>;
467
+
468
+ interface SubColumnPropertiesProps {
469
+ subColumn: SubColumnInstance;
470
+ parentColumn: ColumnInstance;
471
+ }
472
+ declare const SubColumnProperties: React.FunctionComponent<SubColumnPropertiesProps>;
473
+
474
+ interface ElementsPanelProps {
475
+ blocks: BlockDefinition[];
476
+ }
477
+ /**
478
+ * Panel showing draggable base elements (MJML blocks)
479
+ * Matches Mailjet's "Elements > Content" panel
480
+ */
481
+ declare const ElementsPanel: React.FunctionComponent<ElementsPanelProps>;
482
+
483
+ interface LayoutPanelProps {
484
+ templates: PrebuiltTemplate[];
485
+ onAddSection: (columns: 1 | 2 | 3) => void;
486
+ onAddPrebuilt: (template: PrebuiltTemplate) => void;
487
+ /** Adds a container (MJML mj-wrapper) around one empty section. */
488
+ onAddWrapper: () => void;
489
+ }
490
+ declare function LayoutPanel({ templates, onAddSection, onAddPrebuilt, onAddWrapper }: LayoutPanelProps): React.JSX.Element;
491
+
492
+ declare const LayersPanel: React.FunctionComponent<object>;
493
+
494
+ interface LeftSidebarProps {
495
+ blockRegistry: BlockRegistryImpl;
496
+ prebuiltRegistry?: PrebuiltTemplateRegistry;
497
+ onAddSection: (columnCount: 1 | 2 | 3) => void;
498
+ onAddPrebuilt: (template: PrebuiltTemplate) => void;
499
+ onAddWrapper: () => void;
500
+ }
501
+ declare const LeftSidebar: React.FunctionComponent<LeftSidebarProps>;
502
+
503
+ declare const TemplateSettingsPanel: React.FunctionComponent<object>;
504
+
505
+ interface DragOverlayContentProps {
506
+ item: {
507
+ type: 'block' | 'template';
508
+ id: string;
509
+ label: string;
510
+ };
511
+ }
512
+ declare function DragOverlayContent({ item }: DragOverlayContentProps): React.JSX.Element;
513
+
514
+ export { AlignmentField, BlockProperties, BlockRenderer, ButtonBlock, ButtonGroupField, CheckboxField, ColorField, ColumnProperties, ColumnRenderer, DividerBlock, DragOverlayContent, type EditorHostHooks, EditorHostProvider, ElementsPanel, EmailEditor, type EmailEditorProps, EmailRenderer, FormattingToolbar, GradientField, HeroBlock, ImageBlock, type ImageRequest, LayersPanel, LayoutPanel, LeftSidebar, type OnRequestImage, type OnSaveSection, PropertyInspector, RangeField, RawBlock, type RequestedImage, SectionProperties, SectionRenderer, SelectField, SocialBlock, SpacerBlock, SpacingField, StoreProvider, SubColumnProperties, TemplateSettingsPanel, TextBlock, TextField, normalizeSpacingValue, useEditorHost, useEditorUI, usePreviewWidth, useSelectedBlock, useSelectedColumn, useSelectedSection, useStore, useTemplate };