best-email-builder 0.3.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/CHANGELOG.md +133 -0
- package/README.md +214 -19
- package/dist/builder.js +4 -4
- package/dist/{chunk-ZSUHOCCE.js → chunk-AT73Y7JA.js} +159 -83
- package/dist/chunk-AT73Y7JA.js.map +1 -0
- package/dist/chunk-HHLOE6OZ.js +1682 -0
- package/dist/chunk-HHLOE6OZ.js.map +1 -0
- package/dist/chunk-JNHFZXDL.js +1776 -0
- package/dist/chunk-JNHFZXDL.js.map +1 -0
- package/dist/chunk-MY6WPDT4.js +6110 -0
- package/dist/chunk-MY6WPDT4.js.map +1 -0
- package/dist/{chunk-JOC752I3.js → chunk-NAEGU6PR.js} +13 -4
- package/dist/{chunk-JOC752I3.js.map → chunk-NAEGU6PR.js.map} +1 -1
- package/dist/{chunk-BVOVAEEJ.js → chunk-TGDZ4NFI.js} +44 -3
- package/dist/chunk-TGDZ4NFI.js.map +1 -0
- package/dist/index.js +45 -8
- package/dist/index.js.map +1 -1
- package/dist/library.js +3 -3
- package/dist/server.js +544 -204
- package/dist/server.js.map +1 -1
- package/dist/styles.css +1 -1
- package/dist/types/blocks/shared.d.ts +20 -0
- package/dist/types/core/EmailEditor.d.ts +72 -19
- package/dist/types/core/constants.d.ts +1 -1
- package/dist/types/core/mjmlDefaults.d.ts +13 -0
- package/dist/types/core/scrollbars.d.ts +1 -1
- package/dist/types/defaultEditor.d.ts +4 -1
- package/dist/types/engine/NativeAdapter.d.ts +28 -0
- package/dist/types/engine/canvas/CanvasView.d.ts +88 -0
- package/dist/types/engine/canvas/canvasStyles.d.ts +3 -0
- package/dist/types/engine/canvas/icons.d.ts +14 -0
- package/dist/types/engine/canvas/morph.d.ts +21 -0
- package/dist/types/engine/canvasMjml.d.ts +30 -0
- package/dist/types/engine/mjml.d.ts +31 -0
- package/dist/types/engine/model.d.ts +130 -0
- package/dist/types/engine/placement.d.ts +69 -0
- package/dist/types/engine/richText.d.ts +15 -0
- package/dist/types/engine/unwrapModule.d.ts +29 -0
- package/dist/types/features/brand/BrandButton.d.ts +4 -0
- package/dist/types/features/brand/brand.d.ts +108 -0
- package/dist/types/features/brand/extract.d.ts +61 -0
- package/dist/types/features/brand/index.d.ts +2 -0
- package/dist/types/features/brand/messages.d.ts +13 -0
- package/dist/types/features/brand/useBrand.d.ts +22 -0
- package/dist/types/features/generate/GenerateButton.d.ts +3 -0
- package/dist/types/features/generate/RewriteText.d.ts +5 -0
- package/dist/types/features/generate/SubjectIdeas.d.ts +16 -0
- package/dist/types/features/generate/chip.d.ts +1 -0
- package/dist/types/features/generate/claude.d.ts +21 -0
- package/dist/types/features/generate/draft.d.ts +122 -0
- package/dist/types/features/generate/generator.d.ts +118 -0
- package/dist/types/features/generate/index.d.ts +2 -0
- package/dist/types/features/generate/prompt.d.ts +76 -0
- package/dist/types/features/generate/purposes.d.ts +48 -0
- package/dist/types/features/generate/rewrite.d.ts +83 -0
- package/dist/types/features/generate/subjects.d.ts +83 -0
- package/dist/types/features/preview/PreviewButton.d.ts +1 -1
- package/dist/types/features/preview/darkMode.d.ts +48 -0
- package/dist/types/index.d.ts +10 -1
- package/dist/types/server/brand.d.ts +32 -0
- package/dist/types/server/brandImport.d.ts +93 -0
- package/dist/types/server/compileMjml.d.ts +2 -0
- package/dist/types/server/handler.d.ts +25 -0
- package/dist/types/server/index.d.ts +11 -0
- package/dist/types/storage/client.d.ts +75 -3
- package/dist/types/ui/fields.d.ts +2 -1
- package/dist/types/ui/primitives/button.d.ts +1 -1
- package/dist/types/ui/primitives/color-picker.d.ts +2 -1
- package/dist/types/ui/useLayoutWidth.d.ts +1 -1
- package/package.json +13 -7
- package/dist/chunk-3M36VVXS.js +0 -665
- package/dist/chunk-3M36VVXS.js.map +0 -1
- package/dist/chunk-57CTQNTK.js +0 -56
- package/dist/chunk-57CTQNTK.js.map +0 -1
- package/dist/chunk-BVOVAEEJ.js.map +0 -1
- package/dist/chunk-ILRAQTHS.js +0 -3677
- package/dist/chunk-ILRAQTHS.js.map +0 -1
- package/dist/chunk-ZSUHOCCE.js.map +0 -1
- package/dist/types/adapters/grapesjs/GrapesJsAdapter.d.ts +0 -33
- package/dist/types/adapters/grapesjs/engineConfig.d.ts +0 -148
- package/dist/types/adapters/grapesjs/unwrapModule.d.ts +0 -18
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* project JSON). Both are written with every template, so a document read by
|
|
7
7
|
* a newer version knows whether it must be migrated (see `core/document.js`).
|
|
8
8
|
*/
|
|
9
|
-
export const EMAIL_BUILDER_VERSION: "0.
|
|
9
|
+
export const EMAIL_BUILDER_VERSION: "0.4.0";
|
|
10
10
|
export const EMAIL_DOCUMENT_SCHEMA_VERSION: 1;
|
|
11
11
|
/** @typedef {'desktop'|'mobile'} EmailDevice */
|
|
12
12
|
/** @type {EmailDevice[]} */
|
|
@@ -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>>;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Scrollbars inside the builder's frames (the
|
|
2
|
+
* Scrollbars inside the builder's frames (the canvas, the preview),
|
|
3
3
|
* the same as in the rest of the builder: a 5.6 px gutter, a 2.8 px thumb,
|
|
4
4
|
* no arrows, shown only while the pointer is over the scrolling block. A frame is a
|
|
5
5
|
* document of its own, so `styles.css` does not reach it — the same rules
|
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* An editor wired to the default engine and the built-in registries.
|
|
3
3
|
*
|
|
4
|
-
* @param {{locale?: string}} [options]
|
|
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'`).
|
|
5
7
|
*/
|
|
6
8
|
export function createDefaultEmailEditor(options?: {
|
|
7
9
|
locale?: string;
|
|
10
|
+
t?: import("./i18n/index.js").Translate;
|
|
8
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,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;
|