@marlinjai/email-editor-ui 0.2.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.mts CHANGED
@@ -1,5 +1,5 @@
1
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';
2
+ import { TemplateSnapshotIn, TemplateSnapshotOut, BlockRegistryImpl, PrebuiltTemplateRegistry, RootStoreInstance, EditorUIInstance, BlockInstance, ColumnInstance, SectionInstance, TemplateInstance, EmailTemplate, CompileResult, TextBlock, BackgroundGradient, SubColumnInstance, BlockDefinition, PrebuiltTemplate } from '@marlinjai/email-editor-core';
3
3
 
4
4
  /**
5
5
  * What the editor tells the host when it asks for an image.
@@ -71,6 +71,12 @@ interface EmailEditorProps {
71
71
  initialTemplate?: TemplateSnapshotIn;
72
72
  /** Called when template changes */
73
73
  onChange?: (template: TemplateSnapshotOut) => void;
74
+ /**
75
+ * The messages of the host's most recent server compile, when it refused
76
+ * something. Shown as a badge in the toolbar; the canvas itself is a browser
77
+ * compile that cannot run the service's asset policy.
78
+ */
79
+ policyErrors?: string[];
74
80
  /** Block registry for available block types */
75
81
  blockRegistry: BlockRegistryImpl;
76
82
  /** Pre-built template registry */
@@ -142,168 +148,172 @@ declare function useSelectedSection(): SectionInstance | undefined;
142
148
  */
143
149
  declare function useSelectedColumn(): ColumnInstance | undefined;
144
150
 
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;
151
+ declare const DESKTOP_WIDTH = 600;
152
+ declare const MOBILE_WIDTH = 375;
153
+ /** The frame runs nothing: same origin so the overlay can measure and hit-test, no scripts so nothing a Raw block brought along can run. */
154
+ declare const FRAME_SANDBOX = "allow-same-origin";
155
+ declare const CompiledCanvas: React.FunctionComponent<object>;
168
156
 
169
- interface SectionRendererProps {
170
- section: SectionInstance;
171
- sectionIndex: number;
157
+ type NodeKind = 'block' | 'column' | 'subcolumn' | 'section' | 'wrapper';
158
+ interface NodeRef {
159
+ id: string;
160
+ kind: NodeKind;
172
161
  }
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;
162
+ /** A node's box, in the frame document's coordinates (the frame never scrolls, so these are also offsets from the frame's top-left). */
163
+ interface Box {
164
+ x: number;
165
+ y: number;
166
+ width: number;
167
+ height: number;
190
168
  }
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
- };
169
+ /** Every node id in the document and what it is, so an `el-<id>` class in the frame resolves to a node the store knows. */
170
+ declare function nodeIndex(template: TemplateInstance): Map<string, NodeKind>;
171
+ /** The node an element belongs to: itself or its nearest ancestor that carries an `el-<id>` of the document, or a tagged Raw block's markup. */
172
+ declare function nodeFromElement(element: Element | null, index: Map<string, NodeKind>): NodeRef | null;
173
+ /**
174
+ * A Raw block's markup is emitted verbatim, between `<!--ee:raw:id-->` and
175
+ * `<!--/ee:raw:id-->` in editor mode, because MJML drops `css-class` on
176
+ * `mj-raw`. This tags every element between the two comments with the id so
177
+ * hit-testing and measuring treat them as the block. Display-side only.
178
+ */
179
+ declare function tagRawBlocks(doc: Document): void;
180
+ /** The elements that make up a node: one for most, several for a Raw block. */
181
+ declare function elementsOf(doc: Document, id: string): Element[];
182
+ /** The box of every node that rendered, in frame coordinates. Nodes with nothing on screen (a hidden column, empty raw markup) are absent. */
183
+ declare function measureNodes(doc: Document, index: Map<string, NodeKind>): Map<string, Box>;
184
+ /** The `<head>` of a compiled document, for deciding whether a morph can apply the next one. */
185
+ declare function headOf(html: string): string;
186
+ /**
187
+ * Put a compiled document into the frame. The first document, or one whose
188
+ * head changed (fonts, styles), loads through `srcdoc`; any other is morphed
189
+ * into the live body so images do not reflash and the scroll position holds.
190
+ * Returns how it was applied; after a reload the caller waits for `load`.
191
+ */
192
+ declare function applyHtml(iframe: HTMLIFrameElement, html: string, previous: string | null): 'reload' | 'morph';
193
+ /**
194
+ * Nothing in the frame may navigate or submit: the overlay owns pointer
195
+ * events, and this is the second guard, for the moments it lets them through
196
+ * (in-place editing) and for keyboard focus reaching an anchor. Capture phase,
197
+ * so a handler in the mail could not run first even if scripts were allowed.
198
+ */
199
+ declare function guardNavigation(doc: Document): void;
203
200
 
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;
201
+ /** The data a band carries; the editor's drop handler reads it off `over.data`. */
202
+ type DropTarget = {
203
+ columnId: string;
204
+ index: number;
205
+ } | {
206
+ subColumnId: string;
207
+ index: number;
220
208
  };
221
-
222
- interface TextBlockProps {
223
- block: BlockInstance;
209
+ interface Band {
210
+ id: string;
211
+ target: DropTarget;
212
+ x: number;
213
+ y: number;
214
+ width: number;
224
215
  }
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
- };
216
+ /** The bands of a document, from its nodes' boxes. Pure, so it is testable without dnd-kit. */
217
+ declare function bandsFor(template: any, boxes: Map<string, Box>): Band[];
237
218
 
238
- interface ImageBlockProps {
239
- block: BlockInstance;
219
+ interface CompileSchedulerOptions<S, R> {
220
+ /** The compile itself. May be synchronous or return a promise. */
221
+ compile: (snapshot: S) => R | Promise<R>;
222
+ /** Called with a result that is newer than every result applied before it. */
223
+ onResult: (result: R, version: number) => void;
224
+ /** Called when a compile throws or rejects; the version is dropped. */
225
+ onError?: (error: unknown, version: number) => void;
226
+ /** Quiet time after the last schedule before compiling. Default 100. */
227
+ delayMs?: number;
240
228
  }
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;
229
+ interface CompileScheduler<S> {
230
+ /** A new snapshot: compiles after the quiet time, superseding any earlier pending one. */
231
+ schedule(snapshot: S): number;
232
+ /** Compile the pending snapshot now, without waiting for the quiet time. */
233
+ flush(): void;
234
+ /** The version of the newest snapshot scheduled so far (0 before the first). */
235
+ readonly version: number;
236
+ /** The version of the newest result applied so far (0 before the first). */
237
+ readonly applied: number;
238
+ dispose(): void;
250
239
  }
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
- };
240
+ declare function createCompileScheduler<S, R>(options: CompileSchedulerOptions<S, R>): CompileScheduler<S>;
257
241
 
258
- interface DividerBlockProps {
259
- block: BlockInstance;
242
+ interface CompiledDocument {
243
+ /** Monotonic; a later document always has a higher version. */
244
+ version: number;
245
+ html: string;
246
+ /** MJML's soft-validation messages, when any. */
247
+ errors?: string[];
260
248
  }
261
- /**
262
- * DividerBlock - Renders a horizontal divider in the email preview
263
- */
264
- declare const DividerBlock: (({ block }: DividerBlockProps) => React.JSX.Element) & {
265
- displayName: string;
249
+ type CompileStatus = {
250
+ kind: 'loading';
251
+ } | {
252
+ kind: 'ready';
253
+ } | {
254
+ kind: 'failed';
255
+ message: string;
266
256
  };
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;
257
+ type Compile = (template: EmailTemplate, options: {
258
+ editor: boolean;
259
+ }) => CompileResult | Promise<CompileResult>;
260
+ /** Tests only: swap the loader. */
261
+ declare function setBrowserCompilerLoader(loader: () => Promise<Compile>): void;
262
+ /** The browser compiler, loaded once per page and shared by every caller. */
263
+ declare function browserCompiler(): Promise<Compile>;
264
+ /** The browser compiler once it has loaded, null before; failures stay null (the canvas reports them). */
265
+ declare function useBrowserCompiler(): Compile | null;
266
+ declare function useCompiledDocument(template: TemplateInstance, hold: boolean, delayMs?: number): {
267
+ document: CompiledDocument | null;
268
+ status: CompileStatus;
269
+ retry: () => void;
276
270
  };
277
271
 
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
- };
272
+ declare function SectionThumbnail({ section, width, height, className }: {
273
+ section: Record<string, unknown>;
274
+ width: number;
275
+ height: number;
276
+ className?: string;
277
+ }): React.JSX.Element;
287
278
 
288
- interface HeroBlockProps {
289
- block: BlockInstance;
290
- }
279
+ /** The `contenteditable` element of the in-place edit in progress, or null. */
280
+ declare function activeEditingHost(): HTMLElement | null;
291
281
  /**
292
- * HeroBlock - Renders a hero section with background image
282
+ * Whether a pointer press at `target` belongs to the edit rather than ending
283
+ * it: the formatting toolbar and any dialog it opens carry `data-ee-keeps-edit`.
293
284
  */
294
- declare const HeroBlock: (({ block }: HeroBlockProps) => React.JSX.Element) & {
295
- displayName: string;
296
- };
285
+ declare function keepsEdit(target: EventTarget | null): boolean;
297
286
 
298
- interface RawBlockProps {
299
- block: BlockInstance;
300
- }
301
287
  /**
302
- * RawBlock - Renders custom HTML content
303
- */
304
- declare const RawBlock: (({ block }: RawBlockProps) => React.JSX.Element) & {
305
- displayName: string;
306
- };
288
+ * The element whose innerHTML is the block's content. MJML renders an
289
+ * `mj-text` as `<td class="el-text el-<id>"><div style=...>CONTENT</div></td>`;
290
+ * when the block carries inline styles the compiler wraps CONTENT in one more
291
+ * `<div style>`, by the rule `textInlineStyles` states.
292
+ */
293
+ declare function editableFor(doc: Document, block: {
294
+ id: string;
295
+ } & Partial<Pick<TextBlock, 'align' | 'color' | 'fontSize' | 'fontFamily' | 'lineHeight'>>): HTMLElement | null;
296
+ interface InlineEdit {
297
+ /** The content as it was when editing began. */
298
+ readonly original: string;
299
+ /** What is in the element now. */
300
+ current(): string;
301
+ /** Store the content if it changed, without ending the edit. */
302
+ sync(): void;
303
+ /** Store the content if it changed and end the edit. */
304
+ commit(): void;
305
+ /** Put the original content back and end the edit. */
306
+ cancel(): void;
307
+ /** Whether this edit is still live. */
308
+ readonly active: boolean;
309
+ }
310
+ interface InlineEditHandlers {
311
+ /** Content to store; called on every sync or commit whose content differs from the last stored one. */
312
+ onChange: (content: string) => void;
313
+ /** The edit ended, by commit, cancel or a press elsewhere. */
314
+ onEnd: () => void;
315
+ }
316
+ declare function beginInlineEdit(element: HTMLElement, handlers: InlineEditHandlers): InlineEdit;
307
317
 
308
318
  interface PropertyInspectorProps {
309
319
  onDeleteBlock?: (blockId: string) => void;
@@ -332,10 +342,12 @@ declare const PropertyInspector: React.FunctionComponent<PropertyInspectorProps>
332
342
  declare function normalizeSpacingValue(value: string): string | undefined;
333
343
  /**
334
344
  * Formatting toolbar for text editing
335
- * Uses execCommand for contenteditable WYSIWYG editing
345
+ * Uses execCommand for contenteditable WYSIWYG editing, on the document the
346
+ * edited text lives in (the canvas frame).
336
347
  *
337
348
  * Important: Uses onMouseDown with preventDefault to keep focus
338
- * in the contenteditable text block while clicking buttons.
349
+ * in the contenteditable text block while clicking buttons, and carries
350
+ * `data-ee-keeps-edit` so a press on it does not end the in-place edit.
339
351
  */
340
352
  declare function FormattingToolbar(): React.JSX.Element;
341
353
  /**
@@ -511,4 +523,4 @@ interface DragOverlayContentProps {
511
523
  }
512
524
  declare function DragOverlayContent({ item }: DragOverlayContentProps): React.JSX.Element;
513
525
 
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 };
526
+ export { AlignmentField, type Band, BlockProperties, type Box, ButtonGroupField, CheckboxField, ColorField, ColumnProperties, type CompileScheduler, type CompileSchedulerOptions, type CompileStatus, CompiledCanvas, type CompiledDocument, DESKTOP_WIDTH, DragOverlayContent, type DropTarget, type EditorHostHooks, EditorHostProvider, ElementsPanel, EmailEditor, type EmailEditorProps, FRAME_SANDBOX, FormattingToolbar, GradientField, type ImageRequest, type InlineEdit, type InlineEditHandlers, LayersPanel, LayoutPanel, LeftSidebar, MOBILE_WIDTH, type NodeKind, type NodeRef, type OnRequestImage, type OnSaveSection, PropertyInspector, RangeField, type RequestedImage, SectionProperties, SectionThumbnail, SelectField, SpacingField, StoreProvider, SubColumnProperties, TemplateSettingsPanel, TextField, activeEditingHost, applyHtml, bandsFor, beginInlineEdit, browserCompiler, createCompileScheduler, editableFor, elementsOf, guardNavigation, headOf, keepsEdit, measureNodes, nodeFromElement, nodeIndex, normalizeSpacingValue, setBrowserCompilerLoader, tagRawBlocks, useBrowserCompiler, useCompiledDocument, useEditorHost, useEditorUI, useSelectedBlock, useSelectedColumn, useSelectedSection, useStore, useTemplate };