best-email-builder 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.
Files changed (82) hide show
  1. package/CHANGELOG.md +186 -0
  2. package/README.md +309 -12
  3. package/dist/builder.js +44 -0
  4. package/dist/builder.js.map +1 -0
  5. package/dist/chunk-AT73Y7JA.js +1427 -0
  6. package/dist/chunk-AT73Y7JA.js.map +1 -0
  7. package/dist/chunk-HHLOE6OZ.js +1682 -0
  8. package/dist/chunk-HHLOE6OZ.js.map +1 -0
  9. package/dist/chunk-JNHFZXDL.js +1776 -0
  10. package/dist/chunk-JNHFZXDL.js.map +1 -0
  11. package/dist/chunk-MY6WPDT4.js +6110 -0
  12. package/dist/chunk-MY6WPDT4.js.map +1 -0
  13. package/dist/chunk-NAEGU6PR.js +538 -0
  14. package/dist/chunk-NAEGU6PR.js.map +1 -0
  15. package/dist/chunk-TGDZ4NFI.js +114 -0
  16. package/dist/chunk-TGDZ4NFI.js.map +1 -0
  17. package/dist/index.js +124 -4866
  18. package/dist/index.js.map +1 -1
  19. package/dist/library.js +31 -0
  20. package/dist/library.js.map +1 -0
  21. package/dist/server.js +554 -210
  22. package/dist/server.js.map +1 -1
  23. package/dist/styles.css +1 -1
  24. package/dist/types/blocks/shared.d.ts +20 -0
  25. package/dist/types/builder.d.ts +8 -0
  26. package/dist/types/core/EmailEditor.d.ts +72 -19
  27. package/dist/types/core/constants.d.ts +1 -1
  28. package/dist/types/core/mjmlDefaults.d.ts +13 -0
  29. package/dist/types/core/scrollbars.d.ts +13 -0
  30. package/dist/types/defaultEditor.d.ts +11 -0
  31. package/dist/types/engine/NativeAdapter.d.ts +28 -0
  32. package/dist/types/engine/canvas/CanvasView.d.ts +88 -0
  33. package/dist/types/engine/canvas/canvasStyles.d.ts +3 -0
  34. package/dist/types/engine/canvas/icons.d.ts +14 -0
  35. package/dist/types/engine/canvas/morph.d.ts +21 -0
  36. package/dist/types/engine/canvasMjml.d.ts +30 -0
  37. package/dist/types/engine/mjml.d.ts +31 -0
  38. package/dist/types/engine/model.d.ts +130 -0
  39. package/dist/types/engine/placement.d.ts +69 -0
  40. package/dist/types/engine/richText.d.ts +15 -0
  41. package/dist/types/engine/unwrapModule.d.ts +29 -0
  42. package/dist/types/features/brand/BrandButton.d.ts +4 -0
  43. package/dist/types/features/brand/brand.d.ts +108 -0
  44. package/dist/types/features/brand/extract.d.ts +61 -0
  45. package/dist/types/features/brand/index.d.ts +2 -0
  46. package/dist/types/features/brand/messages.d.ts +13 -0
  47. package/dist/types/features/brand/useBrand.d.ts +22 -0
  48. package/dist/types/features/generate/GenerateButton.d.ts +3 -0
  49. package/dist/types/features/generate/RewriteText.d.ts +5 -0
  50. package/dist/types/features/generate/SubjectIdeas.d.ts +16 -0
  51. package/dist/types/features/generate/chip.d.ts +1 -0
  52. package/dist/types/features/generate/claude.d.ts +21 -0
  53. package/dist/types/features/generate/draft.d.ts +122 -0
  54. package/dist/types/features/generate/generator.d.ts +118 -0
  55. package/dist/types/features/generate/index.d.ts +2 -0
  56. package/dist/types/features/generate/prompt.d.ts +76 -0
  57. package/dist/types/features/generate/purposes.d.ts +48 -0
  58. package/dist/types/features/generate/rewrite.d.ts +83 -0
  59. package/dist/types/features/generate/subjects.d.ts +83 -0
  60. package/dist/types/features/preview/PreviewButton.d.ts +1 -1
  61. package/dist/types/features/preview/darkMode.d.ts +48 -0
  62. package/dist/types/features/templates/gallery.d.ts +7 -0
  63. package/dist/types/index.d.ts +14 -10
  64. package/dist/types/library.d.ts +5 -0
  65. package/dist/types/server/brand.d.ts +32 -0
  66. package/dist/types/server/brandImport.d.ts +93 -0
  67. package/dist/types/server/compileMjml.d.ts +2 -0
  68. package/dist/types/server/handler.d.ts +25 -0
  69. package/dist/types/server/index.d.ts +11 -0
  70. package/dist/types/storage/client.d.ts +79 -3
  71. package/dist/types/ui/EmailStudio.d.ts +42 -0
  72. package/dist/types/ui/fields.d.ts +2 -1
  73. package/dist/types/ui/library/TemplateLibrary.d.ts +41 -0
  74. package/dist/types/ui/primitives/button.d.ts +2 -2
  75. package/dist/types/ui/primitives/color-picker.d.ts +2 -1
  76. package/dist/types/ui/useLayoutWidth.d.ts +1 -1
  77. package/package.json +27 -4
  78. package/dist/chunk-MTALS5GC.js +0 -747
  79. package/dist/chunk-MTALS5GC.js.map +0 -1
  80. package/dist/types/adapters/grapesjs/GrapesJsAdapter.d.ts +0 -33
  81. package/dist/types/adapters/grapesjs/engineConfig.d.ts +0 -162
  82. package/dist/types/adapters/grapesjs/unwrapModule.d.ts +0 -18
@@ -0,0 +1,13 @@
1
+ /**
2
+ * MJML DEFAULTS — what MJML 5 renders when an attribute is not written.
3
+ *
4
+ * The properties panel shows them, greyed, in empty fields: an empty
5
+ * "Padding" of a section is not "0" — MJML gives it 20 px above and below,
6
+ * and the panel must say so (and start from there when one side is edited).
7
+ *
8
+ * Copied from the `defaultAttributes` of the mjml 5 components (the same
9
+ * version the server and the canvas compile with); the unit tests check them
10
+ * against the installed components.
11
+ */
12
+ /** @type {Record<string, Record<string, string>>} */
13
+ export const MJML_DEFAULTS: Record<string, Record<string, string>>;
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Scrollbars inside the builder's frames (the canvas, the preview),
3
+ * the same as in the rest of the builder: a 5.6 px gutter, a 2.8 px thumb,
4
+ * no arrows, shown only while the pointer is over the scrolling block. A frame is a
5
+ * document of its own, so `styles.css` does not reach it — the same rules
6
+ * are injected into it. Added to the canvas and the preview only, never to
7
+ * the HTML that is saved or sent.
8
+ *
9
+ * Chromium ≥ 121 drops `::-webkit-scrollbar` (and brings back the arrows)
10
+ * as soon as `scrollbar-width` or `scrollbar-color` is set, so those two are
11
+ * kept for Firefox alone.
12
+ */
13
+ export const FRAME_SCROLLBAR_CSS: "\n::-webkit-scrollbar{width:5.6px;height:5.6px;background:transparent}\n::-webkit-scrollbar-button{display:none;width:0;height:0}\n::-webkit-scrollbar-track,::-webkit-scrollbar-track-piece,::-webkit-scrollbar-corner{background:transparent;border:0;box-shadow:none}\n::-webkit-scrollbar-thumb{background-color:rgba(156,163,175,.45);border:1.4px solid transparent;background-clip:content-box;border-radius:9999px;min-height:2rem;min-width:2rem}\n::-webkit-scrollbar-thumb:hover{background-color:rgba(107,114,128,.7)}\n@supports not selector(::-webkit-scrollbar){*{scrollbar-width:thin;scrollbar-color:rgba(156,163,175,.45) transparent}}\n@media (hover:hover) and (pointer:fine){\n::-webkit-scrollbar-thumb{background-color:transparent}\n*:hover::-webkit-scrollbar-thumb{background-color:rgba(156,163,175,.45)}\n*:hover::-webkit-scrollbar-thumb:hover{background-color:rgba(107,114,128,.7)}\n@supports not selector(::-webkit-scrollbar){*{scrollbar-color:transparent transparent}*:hover{scrollbar-color:rgba(156,163,175,.45) transparent}}\n}";
@@ -0,0 +1,11 @@
1
+ /**
2
+ * An editor wired to the default engine and the built-in registries.
3
+ *
4
+ * @param {{locale?: string, t?: import('./i18n/index.js').Translate}} [options]
5
+ * `t`: the interface's translator (texts of inserted blocks, names in the
6
+ * canvas); by default the built-in one of `locale` (`'en'`).
7
+ */
8
+ export function createDefaultEmailEditor(options?: {
9
+ locale?: string;
10
+ t?: import("./i18n/index.js").Translate;
11
+ }): import("./core/EmailEditor.js").EmailEditor;
@@ -0,0 +1,28 @@
1
+ /**
2
+ * @param {{
3
+ * loadCompiler?: () => Promise<(mjml: string) => Promise<{html: string, errors: any[]}>>,
4
+ * }} [options]
5
+ * @returns {EmailEditorAdapter & {readonly view: any, readonly root: EmailNode|null}}
6
+ */
7
+ export function createNativeAdapter({ loadCompiler }?: {
8
+ loadCompiler?: () => Promise<(mjml: string) => Promise<{
9
+ html: string;
10
+ errors: any[];
11
+ }>>;
12
+ }): EmailEditorAdapter & {
13
+ readonly view: any;
14
+ readonly root: EmailNode | null;
15
+ };
16
+ /**
17
+ * @typedef {import('../core/EmailEditor.js').EmailEditorAdapter} EmailEditorAdapter
18
+ * @typedef {import('../core/EmailEditor.js').EmailBlock} EmailBlock
19
+ * @typedef {import('../core/EmailEditor.js').EngineSelection} EngineSelection
20
+ * @typedef {import('./model.js').EmailNode} EmailNode
21
+ */
22
+ export const ENGINE_NAME: "beb-mjml";
23
+ export const ENGINE_VERSION: "0.4.0";
24
+ export const EMPTY_MJML: "<mjml><mj-body background-color=\"#eceff4\" width=\"600px\"></mj-body></mjml>";
25
+ export type EmailEditorAdapter = import("../core/EmailEditor.js").EmailEditorAdapter;
26
+ export type EmailBlock = import("../core/EmailEditor.js").EmailBlock;
27
+ export type EngineSelection = import("../core/EmailEditor.js").EngineSelection;
28
+ export type EmailNode = import("./model.js").EmailNode;
@@ -0,0 +1,88 @@
1
+ /**
2
+ * @param {{
3
+ * container: HTMLElement,
4
+ * compile: (mjml: string) => Promise<{html: string, errors: any[]}>,
5
+ * getRoot: () => EmailNode,
6
+ * t: (key: string, values?: Record<string, any>) => string,
7
+ * info: (id: string) => {label: string, tag: string, canRemove: boolean, canDuplicate: boolean, canMove: boolean, hasParent: boolean, parentLabel: string}|null,
8
+ * onSelect: (id: string|null) => void,
9
+ * onAction: (action: 'parent'|'duplicate'|'remove', id: string) => void,
10
+ * onDrop: (payload: DragPayload, target: DropTarget) => void,
11
+ * onEditStart: (id: string) => void,
12
+ * onEditInput: (id: string, html: string) => void,
13
+ * onEditEnd: (id: string, html: string) => void,
14
+ * onKeyDown: (event: KeyboardEvent) => void,
15
+ * }} options
16
+ */
17
+ export function createCanvasView(options: {
18
+ container: HTMLElement;
19
+ compile: (mjml: string) => Promise<{
20
+ html: string;
21
+ errors: any[];
22
+ }>;
23
+ getRoot: () => EmailNode;
24
+ t: (key: string, values?: Record<string, any>) => string;
25
+ info: (id: string) => {
26
+ label: string;
27
+ tag: string;
28
+ canRemove: boolean;
29
+ canDuplicate: boolean;
30
+ canMove: boolean;
31
+ hasParent: boolean;
32
+ parentLabel: string;
33
+ } | null;
34
+ onSelect: (id: string | null) => void;
35
+ onAction: (action: "parent" | "duplicate" | "remove", id: string) => void;
36
+ onDrop: (payload: DragPayload, target: DropTarget) => void;
37
+ onEditStart: (id: string) => void;
38
+ onEditInput: (id: string, html: string) => void;
39
+ onEditEnd: (id: string, html: string) => void;
40
+ onKeyDown: (event: KeyboardEvent) => void;
41
+ }): {
42
+ frame: HTMLIFrameElement;
43
+ document: Document;
44
+ render: () => Promise<void>;
45
+ refresh: () => void;
46
+ /** @param {string|null} id */
47
+ setSelection(id: string | null): void;
48
+ /** Ctrl+A: boxes on every given element (empty array: none). @param {string[]} ids */
49
+ setMultiSelection(ids: string[]): void;
50
+ /** @param {'desktop'|'mobile'} next */
51
+ setDevice(next: "desktop" | "mobile"): void;
52
+ getDevice: () => "desktop" | "mobile";
53
+ startDrag: (start: {
54
+ payload: DragPayload;
55
+ label: string;
56
+ source: "host" | "frame";
57
+ pointerId: number;
58
+ captureTarget: Element;
59
+ x: number;
60
+ y: number;
61
+ }) => void;
62
+ cancelDrag: () => void;
63
+ isDragging: () => boolean;
64
+ /** @param {string} id @param {{x: number, y: number}|null} [at] */
65
+ startEditing: (id: string, at?: {
66
+ x: number;
67
+ y: number;
68
+ } | null) => void;
69
+ finishEditing: () => void;
70
+ editingId: () => string | null;
71
+ /** @param {string} text */
72
+ insertText(text: string): boolean;
73
+ /** @param {string} html */
74
+ insertHtml(html: string): boolean;
75
+ destroy(): void;
76
+ };
77
+ /** @typedef {import('../model.js').EmailNode} EmailNode */
78
+ /** @typedef {import('../placement.js').DropTarget} DropTarget */
79
+ /** @typedef {{kind: 'content'|'layout', nodeId?: string|null, block?: any}} DragPayload */
80
+ /** Elements edited as rich text in the canvas. */
81
+ export const EDITABLE_TAGS: Set<string>;
82
+ export type EmailNode = import("../model.js").EmailNode;
83
+ export type DropTarget = import("../placement.js").DropTarget;
84
+ export type DragPayload = {
85
+ kind: "content" | "layout";
86
+ nodeId?: string | null;
87
+ block?: any;
88
+ };
@@ -0,0 +1,3 @@
1
+ export const CANVAS_CSS: "\nhtml, body { min-height: 100%; }\nbody { margin: 0; }\n.eb-node { cursor: default; }\n.eb-node a { cursor: pointer; }\n[contenteditable=\"true\"] { outline: none; cursor: text; caret-color: #5b4ff0; }\ntd.eb-placeholder { outline: 1.5px dashed #c3cad6; outline-offset: -8px; }\n.eb-placeholder { user-select: none; }\n\n#eb-ui { position: absolute; top: 0; left: 0; width: 0; height: 0; z-index: 2147483000;\n font: 500 11px/1.2 system-ui, -apple-system, 'Segoe UI', Roboto, Arial, sans-serif; }\n#eb-ui * { box-sizing: border-box; }\n#eb-ui [hidden] { display: none !important; }\n.eb-ui-box { position: absolute; pointer-events: none; }\n.eb-ui-hover { box-shadow: inset 0 0 0 1px rgba(91, 79, 240, 0.55); }\n.eb-ui-select { box-shadow: inset 0 0 0 2px #5b4ff0; }\n.eb-ui-chip { position: absolute; pointer-events: none; white-space: nowrap; color: #fff; background: #5b4ff0;\n padding: 4px 7px; border-radius: 5px; font-weight: 600; letter-spacing: 0.01em; }\n.eb-ui-hover-chip { background: rgba(91, 79, 240, 0.85); }\n.eb-ui-tools { position: absolute; display: flex; gap: 1px; padding: 2px; border-radius: 6px; background: #5b4ff0;\n box-shadow: 0 4px 12px -4px rgba(40, 30, 120, 0.5); pointer-events: auto; }\n.eb-ui-tools button { all: unset; box-sizing: border-box; width: 26px; height: 24px; display: grid; place-items: center;\n color: #fff; border-radius: 4px; cursor: pointer; }\n.eb-ui-tools button:hover, .eb-ui-tools button:focus-visible { background: rgba(255, 255, 255, 0.2); }\n.eb-ui-tools button[data-action=\"move\"] { cursor: grab; touch-action: none; }\n.eb-ui-tools button.eb-ui-parent { width: auto; gap: 4px; padding: 0 7px 0 5px; display: inline-flex; align-items: center;\n font-weight: 600; white-space: nowrap; }\n.eb-ui-tools button.eb-ui-parent + button { margin-left: 2px; box-shadow: -1px 0 0 rgba(255, 255, 255, 0.25); }\n.eb-ui-drop { position: absolute; height: 4px; margin-top: -2px; border-radius: 2px; background: #5b4ff0;\n box-shadow: 0 0 0 2px rgba(255, 255, 255, 0.85); pointer-events: none; }\n.eb-ui-drop::before, .eb-ui-drop::after { content: ''; position: absolute; top: -3px; width: 10px; height: 10px;\n border-radius: 50%; background: #5b4ff0; box-shadow: 0 0 0 2px #fff; }\n.eb-ui-drop::before { left: -5px; }\n.eb-ui-drop::after { right: -5px; }\n.eb-ui-text { position: absolute; display: flex; align-items: center; gap: 1px; padding: 3px; border-radius: 8px;\n background: #fff; border: 1px solid #dfe3ea; box-shadow: 0 8px 22px -10px rgba(16, 24, 40, 0.35); pointer-events: auto; }\n.eb-ui-text button { all: unset; box-sizing: border-box; width: 28px; height: 26px; display: grid; place-items: center;\n color: #2b3038; border-radius: 5px; cursor: pointer; }\n.eb-ui-text button:hover, .eb-ui-text button:focus-visible { background: #eef0f6; }\n.eb-ui-text button[aria-pressed=\"true\"] { background: #e8e6fd; color: #5b4ff0; }\n.eb-ui-text .eb-ui-sep { width: 1px; height: 18px; margin: 0 3px; background: #e3e6ec; }\n.eb-ui-text input { all: unset; box-sizing: border-box; width: 220px; height: 26px; padding: 0 8px; border-radius: 5px;\n border: 1px solid #dfe3ea; font: 400 12px/1 system-ui, -apple-system, 'Segoe UI', Roboto, Arial, sans-serif; color: #2b3038; }\n.eb-ui-text input:focus { border-color: #5b4ff0; }\n\n::-webkit-scrollbar{width:5.6px;height:5.6px;background:transparent}\n::-webkit-scrollbar-button{display:none;width:0;height:0}\n::-webkit-scrollbar-track,::-webkit-scrollbar-track-piece,::-webkit-scrollbar-corner{background:transparent;border:0;box-shadow:none}\n::-webkit-scrollbar-thumb{background-color:rgba(156,163,175,.45);border:1.4px solid transparent;background-clip:content-box;border-radius:9999px;min-height:2rem;min-width:2rem}\n::-webkit-scrollbar-thumb:hover{background-color:rgba(107,114,128,.7)}\n@supports not selector(::-webkit-scrollbar){*{scrollbar-width:thin;scrollbar-color:rgba(156,163,175,.45) transparent}}\n@media (hover:hover) and (pointer:fine){\n::-webkit-scrollbar-thumb{background-color:transparent}\n*:hover::-webkit-scrollbar-thumb{background-color:rgba(156,163,175,.45)}\n*:hover::-webkit-scrollbar-thumb:hover{background-color:rgba(107,114,128,.7)}\n@supports not selector(::-webkit-scrollbar){*{scrollbar-color:transparent transparent}*:hover{scrollbar-color:rgba(156,163,175,.45) transparent}}\n}\n";
2
+ /** The drag label, in the host page (outside the frame): inline styles only. */
3
+ export const GHOST_STYLE: string;
@@ -0,0 +1,14 @@
1
+ export namespace ICONS {
2
+ let parent: string;
3
+ let move: string;
4
+ let duplicate: string;
5
+ let remove: string;
6
+ let bold: string;
7
+ let italic: string;
8
+ let underline: string;
9
+ let strike: string;
10
+ let link: string;
11
+ let unlink: string;
12
+ let clear: string;
13
+ let check: string;
14
+ }
@@ -0,0 +1,21 @@
1
+ /**
2
+ * MORPH — updates the canvas DOM to a newly compiled email in place.
3
+ *
4
+ * Replacing the whole page at every change would reload the images (the
5
+ * email jumps while they come back), lose the scroll position and the caret
6
+ * of a text being typed. Nodes are compared position by position instead:
7
+ * same kind of node → its attributes and children are updated, otherwise it
8
+ * is replaced. An unchanged image keeps its element, so it never reloads.
9
+ *
10
+ * `skip(element)`: an element left exactly as it is, children included (the
11
+ * text being edited: the browser owns it until the edit ends).
12
+ */
13
+ /**
14
+ * @param {Element} from @param {Element} to
15
+ */
16
+ export function syncAttributes(from: Element, to: Element): void;
17
+ /**
18
+ * Makes `from`'s children match `to`'s.
19
+ * @param {Node} from @param {Node} to @param {(element: Element) => boolean} [skip]
20
+ */
21
+ export function morphChildren(from: Node, to: Node, skip?: (element: Element) => boolean): void;
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Grey frame of an image without a file (an SVG data URL, canvas only),
3
+ * drawn at the width it will be shown so its text stays readable in a narrow
4
+ * column (it says where to choose the image only when there is room).
5
+ * @param {string} label @param {number} [width]
6
+ */
7
+ export function imagePlaceholder(label: string, width?: number): string;
8
+ /**
9
+ * @param {EmailNode} root
10
+ * @param {{t?: (key: string) => string}} [options]
11
+ * @returns {string}
12
+ */
13
+ export function canvasMjml(root: EmailNode, { t }?: {
14
+ t?: (key: string) => string;
15
+ }): string;
16
+ /**
17
+ * Id of the model element an HTML element belongs to (its own marker or its
18
+ * closest marked ancestor's), or null.
19
+ * @param {Element|null} element
20
+ * @returns {{id: string, element: Element}|null}
21
+ */
22
+ export function markedAncestor(element: Element | null): {
23
+ id: string;
24
+ element: Element;
25
+ } | null;
26
+ export const NODE_CLASS: "eb-node";
27
+ export const ID_CLASS_PREFIX: "eb-id-";
28
+ export const TAG_CLASS_PREFIX: "eb-tag-";
29
+ export const PLACEHOLDER_CLASS: "eb-placeholder";
30
+ export type EmailNode = import("./model.js").EmailNode;
@@ -0,0 +1,31 @@
1
+ /** @param {string} value */
2
+ export function decodeAttribute(value: string): string;
3
+ /** @param {string} value */
4
+ export function encodeAttribute(value: string): string;
5
+ /**
6
+ * MJML elements of a fragment (no `<mjml>` needed): `<mj-section>…</mj-section><mj-section>…`.
7
+ * @param {string} source
8
+ * @returns {EmailNode[]}
9
+ */
10
+ export function parseMjmlFragment(source: string): EmailNode[];
11
+ /**
12
+ * A whole email. Always returns an `mjml` root holding an `mj-body`: a bare
13
+ * fragment is wrapped, a missing body is added.
14
+ * @param {string} source
15
+ * @returns {EmailNode}
16
+ */
17
+ export function parseMjml(source: string): EmailNode;
18
+ /**
19
+ * The MJML of a tree. `pretty`: one element per line, indented (what is
20
+ * stored and downloaded); otherwise on one line.
21
+ * @param {EmailNode} node
22
+ * @param {{pretty?: boolean, attributes?: (node: EmailNode) => Record<string, string>, extraChildren?: (node: EmailNode) => string}} [options]
23
+ * `attributes`: replaces a node's attributes on write (the canvas adds its
24
+ * markers there); `extraChildren`: MJML appended inside a container.
25
+ */
26
+ export function serializeMjml(node: EmailNode, options?: {
27
+ pretty?: boolean;
28
+ attributes?: (node: EmailNode) => Record<string, string>;
29
+ extraChildren?: (node: EmailNode) => string;
30
+ }): string;
31
+ export type EmailNode = import("./model.js").EmailNode;
@@ -0,0 +1,130 @@
1
+ /** A fresh id: short, unique for the page's lifetime, `[a-z0-9]` only. */
2
+ export function createId(): string;
3
+ /**
4
+ * @param {string} tag
5
+ * @param {{attributes?: Record<string, string>, children?: EmailNode[], content?: string|null, id?: string}} [parts]
6
+ * @returns {EmailNode}
7
+ */
8
+ export function createNode(tag: string, { attributes, children, content, id }?: {
9
+ attributes?: Record<string, string>;
10
+ children?: EmailNode[];
11
+ content?: string | null;
12
+ id?: string;
13
+ }): EmailNode;
14
+ /**
15
+ * Deep copy with NEW ids everywhere (duplicate, paste, insert a block twice).
16
+ * @param {EmailNode} node
17
+ * @returns {EmailNode}
18
+ */
19
+ export function cloneWithNewIds(node: EmailNode): EmailNode;
20
+ /**
21
+ * The node with this id and its ancestors (root first), or null.
22
+ * @param {EmailNode} root @param {string} id
23
+ * @returns {EmailNode[]|null}
24
+ */
25
+ export function pathTo(root: EmailNode, id: string): EmailNode[] | null;
26
+ /** @param {EmailNode} root @param {string|null|undefined} id */
27
+ export function findNode(root: EmailNode, id: string | null | undefined): EmailNode | null;
28
+ /** @param {EmailNode} root @param {string} id */
29
+ export function parentOf(root: EmailNode, id: string): EmailNode | null;
30
+ /**
31
+ * Closest node (itself included) whose tag is one of `tags`.
32
+ * @param {EmailNode} root @param {string} id @param {Set<string>|string[]} tags
33
+ */
34
+ export function closestOfTags(root: EmailNode, id: string, tags: Set<string> | string[]): EmailNode | null;
35
+ /** First node of a tag, depth first. @param {EmailNode} root @param {string} tag @returns {EmailNode|null} */
36
+ export function findFirst(root: EmailNode, tag: string): EmailNode | null;
37
+ /** Every node, depth first. @param {EmailNode} root @param {(node: EmailNode) => void} visit */
38
+ export function walk(root: EmailNode, visit: (node: EmailNode) => void): void;
39
+ /**
40
+ * The tree with the node `id` replaced by `update(node)`.
41
+ * @param {EmailNode} root @param {string} id @param {(node: EmailNode) => EmailNode} update
42
+ * @returns {EmailNode}
43
+ */
44
+ export function updateNode(root: EmailNode, id: string, update: (node: EmailNode) => EmailNode): EmailNode;
45
+ /**
46
+ * Sets (string) or removes (null, '') attributes.
47
+ * @param {EmailNode} root @param {string} id @param {Record<string, string|null|undefined>} attributes
48
+ */
49
+ export function setAttributes(root: EmailNode, id: string, attributes: Record<string, string | null | undefined>): EmailNode;
50
+ /** @param {EmailNode} root @param {string} id @param {string} content */
51
+ export function setContent(root: EmailNode, id: string, content: string): EmailNode;
52
+ /**
53
+ * Inserts nodes into `parentId` at `index` (clamped).
54
+ * @param {EmailNode} root @param {string} parentId @param {number} index @param {EmailNode[]} nodes
55
+ */
56
+ export function insertNodes(root: EmailNode, parentId: string, index: number, nodes: EmailNode[]): EmailNode;
57
+ /** @param {EmailNode} root @param {string} id */
58
+ export function removeNode(root: EmailNode, id: string): EmailNode;
59
+ /**
60
+ * Moves a node under `parentId` at `index`, where `index` counts the target's
61
+ * children BEFORE the node leaves its place (what a drop indicator shows).
62
+ * @param {EmailNode} root @param {string} id @param {string} parentId @param {number} index
63
+ */
64
+ export function moveNode(root: EmailNode, id: string, parentId: string, index: number): EmailNode;
65
+ /**
66
+ * Index of a node among its siblings, and their count.
67
+ * @param {EmailNode} root @param {string} id
68
+ */
69
+ export function positionOf(root: EmailNode, id: string): {
70
+ parent: null;
71
+ index: number;
72
+ count: number;
73
+ } | {
74
+ parent: EmailNode;
75
+ index: number;
76
+ count: number;
77
+ };
78
+ /** @param {EmailNode} root */
79
+ export function toProject(root: EmailNode): {
80
+ format: number;
81
+ root: any;
82
+ };
83
+ /**
84
+ * Reads a stored project back into a tree. Ids are kept when they are valid
85
+ * and unique, replaced otherwise. Throws on anything that is not a project of
86
+ * this format.
87
+ * @param {unknown} project
88
+ * @returns {EmailNode}
89
+ */
90
+ export function fromProject(project: unknown): EmailNode;
91
+ /**
92
+ * THE EMAIL MODEL — the builder's own document, an MJML tree with ids.
93
+ *
94
+ * { id, tag: 'mj-text', attributes: { color: '#2b3038' }, children: [], content: 'Hello' }
95
+ *
96
+ * One node per MJML element. "Ending" elements (`mj-text`, `mj-button`…)
97
+ * keep their inner HTML in `content`; the others hold `children`. Ids are
98
+ * opaque, unique within a tree, made of `[a-z0-9]` so they can live in a CSS
99
+ * class (the canvas finds an element by them, see `canvasMjml.js`).
100
+ *
101
+ * Every operation is PURE: it returns a new tree and never changes the one it
102
+ * got, sharing the untouched branches. The undo history is then just a list
103
+ * of trees.
104
+ *
105
+ * Dependency-free: runs in the browser, on the server and in unit tests.
106
+ */
107
+ /**
108
+ * @typedef {{
109
+ * id: string,
110
+ * tag: string,
111
+ * attributes: Record<string, string>,
112
+ * children: EmailNode[],
113
+ * content: string|null,
114
+ * }} EmailNode
115
+ */
116
+ /** MJML elements whose inner content is HTML (MJML calls them "ending tags"). */
117
+ export const ENDING_TAGS: Set<string>;
118
+ /** Elements that hold a whole section's place in the body. */
119
+ export const SECTION_TAGS: Set<string>;
120
+ /** Elements that are never removed, moved or duplicated. */
121
+ export const LOCKED_TAGS: Set<string>;
122
+ /** Shape version of the stored project (`{ format, root }`). */
123
+ export const PROJECT_FORMAT: 1;
124
+ export type EmailNode = {
125
+ id: string;
126
+ tag: string;
127
+ attributes: Record<string, string>;
128
+ children: EmailNode[];
129
+ content: string | null;
130
+ };
@@ -0,0 +1,69 @@
1
+ /**
2
+ * @typedef {{parentId: string, index: number, wrap: boolean}} Placement
3
+ * `wrap`: the content must be wrapped in a new section first.
4
+ */
5
+ /**
6
+ * Click placement.
7
+ * @param {EmailNode} root
8
+ * @param {'content'|'layout'} kind
9
+ * @param {string|null} targetId selected element, if any
10
+ * @returns {Placement}
11
+ */
12
+ export function placementFor(root: EmailNode, kind: "content" | "layout", targetId: string | null): Placement;
13
+ /**
14
+ * The body-level section holding a node (itself if it is one).
15
+ * @param {EmailNode} root @param {string} id @param {EmailNode} body
16
+ */
17
+ export function sectionOf(root: EmailNode, id: string, body: EmailNode): import("./model.js").EmailNode | null;
18
+ /**
19
+ * @typedef {{top: number, bottom: number, left: number, right: number}} Box
20
+ * @typedef {Placement & {line: {x: number, y: number, width: number}}} DropTarget
21
+ * `line`: where the canvas draws the drop indicator (frame coordinates).
22
+ */
23
+ /**
24
+ * Drop placement for a pointer at (x, y), in canvas frame coordinates.
25
+ *
26
+ * @param {EmailNode} root
27
+ * @param {{kind: 'content'|'layout', nodeId?: string|null}} dragged
28
+ * `nodeId`: the element being moved (it is skipped when measuring, and
29
+ * cannot be dropped into itself)
30
+ * @param {{x: number, y: number}} point
31
+ * @param {string|null} hitId the marked element under the pointer
32
+ * @param {(id: string) => Box|null} boxOf its box in the frame
33
+ * @returns {DropTarget|null}
34
+ */
35
+ export function dropTargetFor(root: EmailNode, dragged: {
36
+ kind: "content" | "layout";
37
+ nodeId?: string | null;
38
+ }, point: {
39
+ x: number;
40
+ y: number;
41
+ }, hitId: string | null, boxOf: (id: string) => Box | null): DropTarget | null;
42
+ /** @typedef {import('./model.js').EmailNode} EmailNode */
43
+ /** Containers whose children are content blocks. */
44
+ export const CONTENT_CONTAINERS: Set<string>;
45
+ /**
46
+ * `wrap`: the content must be wrapped in a new section first.
47
+ */
48
+ export type Placement = {
49
+ parentId: string;
50
+ index: number;
51
+ wrap: boolean;
52
+ };
53
+ export type Box = {
54
+ top: number;
55
+ bottom: number;
56
+ left: number;
57
+ right: number;
58
+ };
59
+ /**
60
+ * `line`: where the canvas draws the drop indicator (frame coordinates).
61
+ */
62
+ export type DropTarget = Placement & {
63
+ line: {
64
+ x: number;
65
+ y: number;
66
+ width: number;
67
+ };
68
+ };
69
+ export type EmailNode = import("./model.js").EmailNode;
@@ -0,0 +1,15 @@
1
+ /**
2
+ * @param {string} html
3
+ * @param {Document} document any document (to parse without running anything)
4
+ */
5
+ export function cleanRichText(html: string, document: Document): string;
6
+ /**
7
+ * Plain text, escaped, for a label (button, menu link).
8
+ * @param {string} text
9
+ */
10
+ export function escapeText(text: string): string;
11
+ /**
12
+ * The text of an HTML fragment (`<br>` as a space), entities decoded.
13
+ * @param {string} html
14
+ */
15
+ export function textOfHtml(html: string): string;
@@ -0,0 +1,29 @@
1
+ /**
2
+ * A dynamically imported module → the value it exports by default.
3
+ *
4
+ * `mjml-browser` (the canvas compiler) is a CommonJS/UMD module. How
5
+ * `import()` hands such a module over depends on the bundler:
6
+ *
7
+ * Vite, esbuild { default: value }
8
+ * webpack (Next.js), from an { default: { __esModule: true, default: value } }
9
+ * ES module package
10
+ *
11
+ * so a single `.default` once returned an object instead of the function under
12
+ * Next.js (0.1.1: "mjmlPlugin is not a function"). Every interop layer is peeled —
13
+ * a module namespace or an `__esModule` object — never the value itself.
14
+ *
15
+ * @param {any} mod
16
+ * @returns {any}
17
+ */
18
+ export function unwrapModule(mod: any): any;
19
+ /**
20
+ * A module whose export is a FUNCTION (`mjml-browser`) → that function, under
21
+ * any bundler. On top of `unwrapModule`, Vite's dynamic import of a CommonJS
22
+ * module hands over a plain object `{ ...props, default: fn }` (no module
23
+ * flag): since a function is expected, `default` is followed while the value
24
+ * is not one.
25
+ *
26
+ * @param {any} mod
27
+ * @returns {Function|null}
28
+ */
29
+ export function functionExport(mod: any): Function | null;
@@ -0,0 +1,4 @@
1
+ export default function BrandButton({ context }: {
2
+ context: any;
3
+ }): import("react").JSX.Element;
4
+ export type Brand = import("./brand.js").Brand;
@@ -0,0 +1,108 @@
1
+ /**
2
+ * A CSS color as RGB(A), or null: #rgb, #rgba, #rrggbb, #rrggbbaa, rgb(),
3
+ * rgba(), hsl(), hsla(), and the keywords white and black.
4
+ * @param {unknown} value
5
+ * @returns {{r: number, g: number, b: number, a: number}|null}
6
+ */
7
+ export function parseColor(value: unknown): {
8
+ r: number;
9
+ g: number;
10
+ b: number;
11
+ a: number;
12
+ } | null;
13
+ /** Hue (0-360), saturation and lightness (0-1). @param {{r: number, g: number, b: number}} color */
14
+ export function toHsl({ r, g, b }: {
15
+ r: number;
16
+ g: number;
17
+ b: number;
18
+ }): {
19
+ h: number;
20
+ s: number;
21
+ l: number;
22
+ };
23
+ /** WCAG contrast ratio of two colors (1 to 21). @param {string} a @param {string} b */
24
+ export function contrastRatio(a: string, b: string): number;
25
+ /** The text color that reads on `background`: white or ink. @param {string} background */
26
+ export function readableOn(background: string): string;
27
+ /**
28
+ * A color meant to be noticed (a banner, a filled button), as opposed to
29
+ * white, light tints, grays and near-black.
30
+ * @param {unknown} value
31
+ */
32
+ export function isColored(value: unknown): boolean;
33
+ /**
34
+ * A brand as stored and used: unknown fields dropped, colors as #rrggbb,
35
+ * the font one of the builder's stacks, the logo an https or site-relative
36
+ * image. Whatever is invalid is left out (the builder's default is used).
37
+ * @param {unknown} input
38
+ * @returns {Brand}
39
+ */
40
+ export function cleanBrand(input: unknown): Brand;
41
+ /**
42
+ * The sender the templates use: the brand's name and logo over the host's
43
+ * sender (whose postal address stays).
44
+ * @param {import('../templates/starter.js').StarterSender|null|undefined} sender
45
+ * @param {Brand|null|undefined} brand
46
+ */
47
+ export function brandedSender(sender: import("../templates/starter.js").StarterSender | null | undefined, brand: Brand | null | undefined): {
48
+ name?: string;
49
+ address?: string;
50
+ logoUrl?: string;
51
+ logoWidth?: number;
52
+ };
53
+ /**
54
+ * An email in the brand's look.
55
+ * @param {string} mjml
56
+ * @param {Brand|null|undefined} brand
57
+ * @param {{header?: boolean, previous?: {names?: (string|undefined)[], logos?: (string|undefined)[]}}} [options]
58
+ * `header`: also put the logo (or name) at the top and the name in the
59
+ * footer, replacing what `previous` lists (the names and logos the email
60
+ * may still show: the host's sender, the brand before, "Your company").
61
+ * @returns {string}
62
+ */
63
+ export function applyBrand(mjml: string, brand: Brand | null | undefined, { header, previous }?: {
64
+ header?: boolean;
65
+ previous?: {
66
+ names?: (string | undefined)[];
67
+ logos?: (string | undefined)[];
68
+ };
69
+ }): string;
70
+ /**
71
+ * A fragment (a block about to be inserted) in the brand's look.
72
+ * @param {string} mjml @param {Brand|null|undefined} brand
73
+ */
74
+ export function applyBrandToFragment(mjml: string, brand: Brand | null | undefined): string;
75
+ /**
76
+ * A ready-made email (the starter, a gallery template) in the brand's look,
77
+ * its header and footer naming the brand rather than the host's sender or
78
+ * the "Your company" placeholder.
79
+ * @param {string} mjml
80
+ * @param {Brand|null|undefined} brand
81
+ * @param {{sender?: import('../templates/starter.js').StarterSender|null, t?: (key: string) => string}} [context]
82
+ */
83
+ export function brandTemplate(mjml: string, brand: Brand | null | undefined, { sender, t }?: {
84
+ sender?: import("../templates/starter.js").StarterSender | null;
85
+ t?: (key: string) => string;
86
+ }): string;
87
+ export const MAX_LOGO_WIDTH: 240;
88
+ export function toHex({ r, g, b }: {
89
+ r: number;
90
+ g: number;
91
+ b: number;
92
+ }): string;
93
+ export function hasBrand(brand: Brand | null | undefined): boolean;
94
+ /**
95
+ * `primary`: buttons, banners, links; `heading`: headings; colors as
96
+ * `#rrggbb`. `font`: one of the builder's web-safe stacks (FONT_OPTIONS).
97
+ * `website`: where it was read from, to read it again.
98
+ */
99
+ export type Brand = {
100
+ name?: string;
101
+ logoUrl?: string;
102
+ logoWidth?: number;
103
+ primary?: string;
104
+ heading?: string;
105
+ font?: string;
106
+ website?: string;
107
+ };
108
+ export type EmailNode = import("../../engine/model.js").EmailNode;