@marlinjai/email-editor 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 marlinjai
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,328 @@
1
+ # @marlinjai/email-editor
2
+
3
+ A visual, drag-and-drop email editor you embed in your own app. It produces a JSON document; your server compiles that document to email-safe HTML with MJML (the Mailjet Markup Language, a markup that compiles to HTML which renders consistently across mail clients).
4
+
5
+ - React component (`EmailEditorReact`) and a framework-agnostic factory (`createEditor`)
6
+ - 14 block types (text, image, button, hero, social, navbar, table and more) and 35 pre-built sections
7
+ - Containers around several sections (MJML's `mj-wrapper`): one background, border, radius and padding for a group of sections, edited visually
8
+ - Your own image picker through the `onRequestImage` hook
9
+ - A prebuilt stylesheet scoped to the editor, safe beside Tailwind CSS 4 or any other host styles
10
+ - Server-side compilation through `@marlinjai/email-editor-core/server`
11
+
12
+ Peer ranges accept React 18 and 19; the integration is verified on React 19.2, Next.js 16.3 and Tailwind CSS 4. Node.js 20.19 or newer.
13
+
14
+ ## Install
15
+
16
+ ```bash
17
+ pnpm add @marlinjai/email-editor @marlinjai/email-editor-core react react-dom
18
+ ```
19
+
20
+ `@marlinjai/email-editor-core` is only needed directly if you compile or validate documents on your server (you almost certainly do).
21
+
22
+ ## Use it in React
23
+
24
+ ```tsx
25
+ 'use client';
26
+
27
+ import { useState } from 'react';
28
+ import { EmailEditorReact, type TemplateSnapshotOut } from '@marlinjai/email-editor/react';
29
+ import '@marlinjai/email-editor/styles.css';
30
+
31
+ export function Composer({ initial }: { initial?: TemplateSnapshotOut }) {
32
+ const [doc, setDoc] = useState(initial);
33
+
34
+ return (
35
+ <div style={{ height: '80vh' }}>
36
+ <EmailEditorReact initialTemplate={initial} onChange={setDoc} onSave={() => save(doc)} />
37
+ </div>
38
+ );
39
+ }
40
+ ```
41
+
42
+ - The editor fills its container, so give the container a height.
43
+ - `initialTemplate` is read once, on mount (the editor is uncontrolled). To load a different document, remount it with a new `key`.
44
+ - `onChange` receives the full document, debounced by 300 ms. Persist it as JSON.
45
+ - A document's `id` is optional. When the document you pass has none, the editor assigns one on mount (on its own copy: your object is not changed), and every document it hands back (`onChange`, `onExport`) carries that id. Save what you are handed and the id stays stable from then on.
46
+
47
+ ### Props
48
+
49
+ | Prop | Type | What it does |
50
+ |------|------|--------------|
51
+ | `initialTemplate` | `TemplateSnapshotIn` | Document to open. Omit for an empty email. |
52
+ | `onChange` | `(doc) => void` | Called with the whole document after edits (debounced). |
53
+ | `onSave` | `() => void` | Shows a Save button in the toolbar and calls this on click. |
54
+ | `onExport` | `(doc) => void` | Shows an Export button; compile the document on your server. |
55
+ | `onNavigateBack` | `() => void` | Shows a back arrow in the toolbar. |
56
+ | `onRequestImage` | `(request) => Promise<{ url, alt? } \| null>` | Your image picker, see below. |
57
+ | `blocks` | `BlockDefinition[]` | Redefine standard block types (label, icon, category, default props). A new block type is refused with an error. |
58
+ | `theme` | `EditorTheme` | Brand colors and font of the editor chrome, see below. |
59
+ | `savedSections` | `SavedSectionInput[]` | Sections this workspace saved, offered in the picker under their own group. |
60
+ | `onSaveSection` | `(section, name) => Promise<void>` | Enables "Save as a section" on a section's controls. Reject with a readable message. |
61
+ | `builtInSections` | `boolean` | Offer the 35 sections that ship with this package. `true` by default. |
62
+ | `placeholderBase` | `string` | Where placeholder images are served from, see below. |
63
+
64
+ ## Next.js (App Router)
65
+
66
+ The editor runs in the browser only (drag and drop, rich text, MobX state), so load it on the client and skip server rendering:
67
+
68
+ ```tsx
69
+ // app/compose/page.tsx
70
+ 'use client';
71
+
72
+ import dynamic from 'next/dynamic';
73
+ import '@marlinjai/email-editor/styles.css';
74
+
75
+ const EmailEditorReact = dynamic(
76
+ () => import('@marlinjai/email-editor/react').then((mod) => mod.EmailEditorReact),
77
+ { ssr: false, loading: () => <p>Loading editor...</p> }
78
+ );
79
+
80
+ export default function ComposePage() {
81
+ return (
82
+ <div style={{ height: '100vh' }}>
83
+ <EmailEditorReact onChange={(doc) => console.log(doc)} />
84
+ </div>
85
+ );
86
+ }
87
+ ```
88
+
89
+ `next.config.ts`:
90
+
91
+ ```ts
92
+ import type { NextConfig } from 'next';
93
+
94
+ const nextConfig: NextConfig = {
95
+ // MJML is Node-only and loads files at runtime: keep it out of the server bundle.
96
+ serverExternalPackages: ['mjml', 'mjml-core', 'mjml-parser-xml', 'mjml-preset-core', 'mjml-validator'],
97
+ };
98
+
99
+ export default nextConfig;
100
+ ```
101
+
102
+ `transpilePackages` is **not** needed: the packages ship compiled ESM and CommonJS. This setup is verified on Next.js 16.3 with React 19.2 and Tailwind CSS 4 by the repository's example app (`examples/nextjs`).
103
+
104
+ ## Compile on the server
105
+
106
+ Compilation happens on your server, never in the browser (MJML is a large Node.js dependency). Validate the document first with `migrateTemplate`:
107
+
108
+ ```ts
109
+ // app/api/compile/route.ts
110
+ import { migrateTemplate, isTemplateMigrationError } from '@marlinjai/email-editor-core';
111
+ import { createMJMLCompiler } from '@marlinjai/email-editor-core/server';
112
+
113
+ export async function POST(request: Request) {
114
+ try {
115
+ const doc = migrateTemplate(await request.json());
116
+ // { webFonts: false } leaves out MJML's automatic Google Fonts imports
117
+ // (for fonts such as Roboto or Lato), when your mails must load nothing
118
+ // from third parties.
119
+ const { html, mjml, errors } = createMJMLCompiler().compile(doc);
120
+ return Response.json({ html, mjml, errors });
121
+ } catch (error) {
122
+ if (isTemplateMigrationError(error)) {
123
+ const status = error.code === 'NEWER_VERSION' ? 422 : 400;
124
+ return Response.json({ error: error.message, code: error.code, issues: error.issues }, { status });
125
+ }
126
+ throw error;
127
+ }
128
+ }
129
+ ```
130
+
131
+ Never import `@marlinjai/email-editor-core/server` from client code.
132
+
133
+ ## Export as MJML or HTML, import existing MJML
134
+
135
+ Both directions run on your server, next to the compiler, and never in the browser bundle.
136
+
137
+ **Export.** The same `compile` call gives both files: `mjml` is the MJML source of the document, `html` the finished mail. Serve whichever the person asked for as a download:
138
+
139
+ ```ts
140
+ // app/api/export/route.ts
141
+ import { migrateTemplate } from '@marlinjai/email-editor-core';
142
+ import { createMJMLCompiler } from '@marlinjai/email-editor-core/server';
143
+
144
+ export async function POST(request: Request) {
145
+ const format = new URL(request.url).searchParams.get('format') === 'mjml' ? 'mjml' : 'html';
146
+ const { html, mjml, errors } = createMJMLCompiler().compile(migrateTemplate(await request.json()));
147
+ return new Response(format === 'mjml' ? mjml : html, {
148
+ headers: {
149
+ 'content-type': format === 'mjml' ? 'text/plain; charset=utf-8' : 'text/html; charset=utf-8',
150
+ 'content-disposition': `attachment; filename="email.${format}"`,
151
+ // errors: MJML's validation messages, worth showing next to the download
152
+ },
153
+ });
154
+ }
155
+ ```
156
+
157
+ The editor's `getHTML()` and `getMJML()` cannot compile in the browser; call your route with `getValue()` instead. The editor has no export button of its own on purpose: a download belongs in your app's chrome (where the Lumitra Mail dashboard puts its Export menu), and only your server can compile.
158
+
159
+ **Import.** `importMjml(source)` reads an MJML document into the editor's document model:
160
+
161
+ ```ts
162
+ import { importMjml, isMjmlImportError } from '@marlinjai/email-editor-core/server';
163
+
164
+ try {
165
+ const { document, warnings } = importMjml(mjmlSource);
166
+ // `document` passed migrateTemplate: open it in the editor, or store it.
167
+ // `warnings`: what could not become an editable block, with where and why.
168
+ } catch (error) {
169
+ if (isMjmlImportError(error)) {
170
+ // error.code: invalid_xml | not_mjml | include_not_supported | too_large | too_deep | too_many_elements | invalid_document
171
+ // error.line, error.column: where, when it is one place
172
+ }
173
+ throw error;
174
+ }
175
+ ```
176
+
177
+ What maps, and what does not:
178
+
179
+ - Every standard component with the attributes its block has becomes that block: text, image, button, divider, spacer, navbar, carousel, accordion, raw HTML, sections, columns and groups of columns.
180
+ - An `mj-wrapper` becomes a container holding its sections, with every wrapper attribute as a field (background, border and each side's border, radius, padding, full width, `css-class`, `gap`, `text-align`). A child the editor cannot read as a section (an `mj-hero`, an `mj-raw`, a section with conditional comments) stays inside the container, in place, as raw HTML, so nothing in a wrapper is dropped. Attributes the block has no field for (a `css-class` your `mj-style` rules target, `font-weight`, `mj-class`, ...) are kept on the block and emitted again, and the document keeps its `mj-attributes`, so the mail compiles as the source did.
181
+ - The editor's own export imports back exactly, ids included.
182
+ - What the editor cannot hold as a block is compiled in place and kept as a Raw HTML block that renders exactly as before, with a `kept_as_html` warning carrying the MJML: an `mj-hero` with content, `mj-social` (the editor's Social block draws its own icons), a hand-written `mj-table`, a section with conditional comments between its columns.
183
+ - A component MJML does not know renders nothing in MJML either; its source is kept in a comment in a Raw block, with an `unknown_component` warning.
184
+ - `mj-include` is refused (`include_not_supported`): an import has no files next to it, and the importer never reads the disk.
185
+
186
+ Importing is synchronous and CPU-bound. Limits (`MAX_MJML_BYTES`, `MAX_MJML_DEPTH`, `MAX_MJML_ELEMENTS`) bound one call; for untrusted input run it off your request thread with a deadline, as the Lumitra Mail service does in its compile worker pool.
187
+
188
+ ## Containers (wrappers)
189
+
190
+ A container is MJML's `mj-wrapper`: several sections sharing one background (colour, gradient or image), border (all sides or each side), corner radius and padding, with an optional `gap` between the sections inside. In the document it sits at the top level next to sections, `{ type: 'wrapper', sections: [...] }`; containers never nest and never sit inside a section.
191
+
192
+ - Add one from the Layout tab (Add Container), or select a section and choose Wrap in container (canvas toolbar, inspector, or the Layers panel). A section next to a container can join it from the inspector; Move out takes it back to the top level.
193
+ - In the Layers panel a container's sections are nested one level in. Drag sections into, out of and between containers (pointer or keyboard: Space, arrow keys, Space), and drag containers to reorder them.
194
+ - The inspector edits every container attribute; the background image goes through your `onRequestImage` hook (`blockType: 'wrapper'`). The canvas draws background, border, radius, MJML's default padding (`20px 0`) and the gap as the mail will.
195
+ - Inside a container a section's Full Width has no visible effect (and a full-width container draws its sections at standard width), so the section inspector explains that instead of offering it. Outlook on Windows cannot show a section's background image inside a container that has one; the inspector warns.
196
+ - Delete (the key, the toolbar or the Layers panel) asks in the editor's own dialog whether to keep the sections or delete everything. Every container action is one undo step.
197
+
198
+ ## Stored documents and `migrateTemplate`
199
+
200
+ Every document carries a schema `version` (today `"1.1"`, exported as `CURRENT_TEMPLATE_VERSION`; 1.1 added containers). Run stored documents through `migrateTemplate(doc)` when you load them: it returns the document at the current version. For a `1.1` document it is the identity (the same object comes back, validated); a `1.0` document comes back as a new object whose only change is the version, except that a 1.0 section flagged `isWrapper` (a wrapper around one section, written by the first MJML import) becomes a container around that section. The input is never changed, and a 1.0 document without that flag compiles to exactly the same mail. The editor opens 1.0 documents the same way and emits 1.1. A build that only knows 1.0 refuses a 1.1 document with `NEWER_VERSION`. It throws a `TemplateMigrationError` whose `code` is one of:
201
+
202
+ | `code` | Meaning |
203
+ |--------|---------|
204
+ | `INVALID_INPUT` | Not an object, so not a document. |
205
+ | `MISSING_VERSION` | No `version` string. |
206
+ | `UNSUPPORTED_VERSION` | Malformed version, or an older one with no migration. |
207
+ | `NEWER_VERSION` | Written by a newer editor. Upgrade these packages to open it. |
208
+ | `INVALID_DOCUMENT` | The version is known but the document fails its schema; `issues` lists where. |
209
+
210
+ Store the version next to the document (for example a `schema_version` column), so you can find documents that need upgrading after a future schema change.
211
+
212
+ ## Placeholder images: `placeholderBase`
213
+
214
+ Every built-in section, and the Image, Hero and Carousel blocks, shows a grey
215
+ rectangle where no image has been chosen yet. Where that rectangle comes from is
216
+ your decision, and it matters more than it looks:
217
+
218
+ - **Without `placeholderBase`** each one is an inline `data:` image. It renders
219
+ in the editor's canvas and needs nothing behind it, which is the right default
220
+ for trying the package out. But Gmail and Outlook.com do not render `data:`
221
+ images at all, so one left in a real email is an invisible gap rather than an
222
+ obvious mistake.
223
+ - **With `placeholderBase`** each becomes `<base>/p/<width>x<height>.png`, a
224
+ real image from your own origin. It renders everywhere, and it passes a
225
+ recipient-privacy policy that only allows images from your own host.
226
+
227
+ Set it whenever the documents this editor produces are going to be sent:
228
+
229
+ ```tsx
230
+ <EmailEditorReact placeholderBase="https://mail.example.com" />
231
+ ```
232
+
233
+ Your server serves the sizes the sections ask for. Each is a flat grey
234
+ rectangle with a border at exactly the width and height in the path, and the
235
+ aspect ratio matters: a block that sets only a width lays out from the image's
236
+ own ratio, so one square image scaled by the browser would distort every
237
+ text-and-image layout.
238
+
239
+ **Do not point it at a third-party placeholder service.** An image address in an
240
+ email is an instruction to every recipient's mail client to call that host,
241
+ which hands a stranger the reader's address and the moment they opened it.
242
+
243
+ Whatever you choose, treat a placeholder that survives to send time as a
244
+ mistake: warn, or refuse. Lumitra Mail refuses the send.
245
+
246
+ ## Your own image picker: `onRequestImage`
247
+
248
+ Without the hook, the image block's inspector (and the background image of a section or container) shows a plain URL field. With it, the inspector shows a Choose image (or Replace image) button that calls your function; for a background, `blockId` is the section's or container's id and `blockType` is `section` or `wrapper`:
249
+
250
+ ```tsx
251
+ <EmailEditorReact
252
+ onRequestImage={async ({ blockId, currentUrl, currentAlt }) => {
253
+ const picked = await openMyMediaLibrary({ currentUrl }); // your UI
254
+ if (!picked) return null; // cancelled: the block is left unchanged
255
+ return { url: picked.publicUrl, alt: picked.description };
256
+ }}
257
+ />
258
+ ```
259
+
260
+ - Resolve with `{ url, alt? }` to set the image. `url` must be publicly reachable by your recipients' mail clients. `alt` is optional; without it the block keeps its alt text.
261
+ - Resolve with `null` to cancel. Nothing changes.
262
+ - Reject (throw) to show the error's message inline under the button, for example an upload that failed. The user can try again.
263
+ - While your promise is pending the button is disabled, so a second request cannot start. If the user deletes the block before you resolve, the result is dropped.
264
+
265
+ Use an in-page dialog for the picker, not `window.prompt`.
266
+
267
+ ## Styles, Tailwind CSS 4 and theming
268
+
269
+ Import `@marlinjai/email-editor/styles.css` once. Every rule in it is scoped under the editor's root element (`.ee-root`), including its CSS reset, so it does not restyle your page, and its keyframes are prefixed `ee-`. It is deliberately not inside a CSS cascade layer, so a Tailwind CSS 4 host's own reset (in `@layer base`) cannot leak into the editor either. Nothing in your Tailwind configuration needs to change, and you should not add the editor's files to Tailwind's content sources.
270
+
271
+ Theme the editor chrome with the `theme` prop:
272
+
273
+ ```tsx
274
+ <EmailEditorReact
275
+ theme={{
276
+ colors: { primary: '#0f766e', primaryHover: '#115e59', surface: '#ffffff', text: '#0f172a', border: '#e2e8f0' },
277
+ fonts: { body: 'Inter, system-ui, sans-serif' },
278
+ }}
279
+ />
280
+ ```
281
+
282
+ Each value sets a design token on the editor's root element only. For finer control, override any `--ee-*` token in your own CSS; put the rule outside any `@layer`, because the editor's unlayered stylesheet wins over layered rules:
283
+
284
+ ```css
285
+ .ee-root {
286
+ --ee-midnight-2: #0b1220; /* toolbar background */
287
+ }
288
+ ```
289
+
290
+ The tokens are listed at the top of the stylesheet (`--ee-midnight-*` for the dark chrome, `--ee-canvas-*` for light surfaces, `--ee-text-*`, `--ee-border-*`, `--ee-accent*`, `--ee-success*`, `--ee-danger*`, `--ee-font-sans`).
291
+
292
+ ## Without React: `createEditor`
293
+
294
+ ```ts
295
+ import { createEditor } from '@marlinjai/email-editor';
296
+ import '@marlinjai/email-editor/styles.css';
297
+
298
+ const editor = createEditor({
299
+ container: document.getElementById('editor')!,
300
+ initialValue: storedDoc,
301
+ onChange: (doc) => save(doc),
302
+ onRequestImage: async () => ({ url: 'https://cdn.example.com/hero.png' }),
303
+ theme: { colors: { primary: '#0f766e' } },
304
+ });
305
+
306
+ // load another document (the editor remounts on it; undo history starts over)
307
+ editor.setValue(otherDoc);
308
+
309
+ // the document as it stands, with its id
310
+ editor.getValue();
311
+
312
+ // later
313
+ editor.destroy();
314
+ ```
315
+
316
+ `initialValue` and `setValue` accept a document without an `id`: the editor opens it on a copy with a fresh id, and `getValue`, `onChange` and `onSave` return that id from the start, before any edit.
317
+
318
+ `createEditor` still needs `react` and `react-dom` installed (the editor is built with React), but your app does not have to use React.
319
+
320
+ ## Related packages
321
+
322
+ - `@marlinjai/email-editor-core`: document schema, `migrateTemplate`, store, and the server-side MJML compiler
323
+ - `@marlinjai/email-editor-blocks`: the standard blocks and pre-built sections
324
+ - `@marlinjai/email-editor-ui`: the React UI, for hosts that assemble the editor themselves
325
+
326
+ ## License
327
+
328
+ MIT
@@ -0,0 +1,47 @@
1
+ // src/theme.ts
2
+ function themeToStyle(theme) {
3
+ const style = {};
4
+ const set = (token, value) => {
5
+ if (typeof value === "string" && value.trim()) style[token] = value.trim();
6
+ };
7
+ set("--ee-accent", theme?.colors?.primary);
8
+ set("--ee-accent-hover", theme?.colors?.primaryHover ?? theme?.colors?.primary);
9
+ set("--ee-canvas-2", theme?.colors?.surface);
10
+ set("--ee-text-dark", theme?.colors?.text);
11
+ set("--ee-border-light", theme?.colors?.border);
12
+ set("--ee-font-sans", theme?.fonts?.body);
13
+ return style;
14
+ }
15
+
16
+ // src/blocks.ts
17
+ import { BlockType } from "@marlinjai/email-editor-core";
18
+ var STANDARD_TYPES = new Set(Object.values(BlockType));
19
+ function assertSupportedBlocks(blocks) {
20
+ const unsupported = blocks.map((block) => block?.type).filter((type) => !STANDARD_TYPES.has(type));
21
+ if (unsupported.length > 0) {
22
+ throw new Error(
23
+ `Unsupported block type${unsupported.length > 1 ? "s" : ""}: ${unsupported.map((type) => JSON.stringify(type)).join(", ")}. The blocks option can only redefine a standard block type (${[...STANDARD_TYPES].join(", ")}).`
24
+ );
25
+ }
26
+ }
27
+
28
+ // src/savedSections.ts
29
+ var savedSectionId = (id) => `saved:${id}`;
30
+ function registerSavedSections(registry, sections) {
31
+ for (const saved of sections) {
32
+ if (typeof saved?.section !== "object" || saved.section === null) continue;
33
+ registry.register({
34
+ id: savedSectionId(saved.id),
35
+ name: saved.name,
36
+ category: "saved",
37
+ description: saved.description,
38
+ section: saved.section
39
+ });
40
+ }
41
+ }
42
+
43
+ export {
44
+ themeToStyle,
45
+ assertSupportedBlocks,
46
+ registerSavedSections
47
+ };
@@ -0,0 +1,12 @@
1
+ import { E as EditorOptions, a as EditorInstance } from './types-G7Ak2dWQ.mjs';
2
+ export { b as EditorTheme, S as SavedSectionInput } from './types-G7Ak2dWQ.mjs';
3
+ export { ImageRequest, OnRequestImage, OnSaveSection, RequestedImage } from '@marlinjai/email-editor-ui';
4
+ export { Block, BlockDefinition, Column, EmailTemplate, Section } from '@marlinjai/email-editor-core';
5
+
6
+ /**
7
+ * Create an email editor instance
8
+ * Framework-agnostic public API
9
+ */
10
+ declare function createEditor(options: EditorOptions): EditorInstance;
11
+
12
+ export { EditorInstance, EditorOptions, createEditor };
@@ -0,0 +1,12 @@
1
+ import { E as EditorOptions, a as EditorInstance } from './types-G7Ak2dWQ.js';
2
+ export { b as EditorTheme, S as SavedSectionInput } from './types-G7Ak2dWQ.js';
3
+ export { ImageRequest, OnRequestImage, OnSaveSection, RequestedImage } from '@marlinjai/email-editor-ui';
4
+ export { Block, BlockDefinition, Column, EmailTemplate, Section } from '@marlinjai/email-editor-core';
5
+
6
+ /**
7
+ * Create an email editor instance
8
+ * Framework-agnostic public API
9
+ */
10
+ declare function createEditor(options: EditorOptions): EditorInstance;
11
+
12
+ export { EditorInstance, EditorOptions, createEditor };
package/dist/index.js ADDED
@@ -0,0 +1,173 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/index.ts
21
+ var index_exports = {};
22
+ __export(index_exports, {
23
+ createEditor: () => createEditor
24
+ });
25
+ module.exports = __toCommonJS(index_exports);
26
+
27
+ // src/createEditor.ts
28
+ var import_client = require("react-dom/client");
29
+ var import_react = require("react");
30
+ var import_email_editor_core2 = require("@marlinjai/email-editor-core");
31
+ var import_email_editor_blocks = require("@marlinjai/email-editor-blocks");
32
+ var import_email_editor_ui = require("@marlinjai/email-editor-ui");
33
+
34
+ // src/theme.ts
35
+ function themeToStyle(theme) {
36
+ const style = {};
37
+ const set = (token, value) => {
38
+ if (typeof value === "string" && value.trim()) style[token] = value.trim();
39
+ };
40
+ set("--ee-accent", theme?.colors?.primary);
41
+ set("--ee-accent-hover", theme?.colors?.primaryHover ?? theme?.colors?.primary);
42
+ set("--ee-canvas-2", theme?.colors?.surface);
43
+ set("--ee-text-dark", theme?.colors?.text);
44
+ set("--ee-border-light", theme?.colors?.border);
45
+ set("--ee-font-sans", theme?.fonts?.body);
46
+ return style;
47
+ }
48
+
49
+ // src/blocks.ts
50
+ var import_email_editor_core = require("@marlinjai/email-editor-core");
51
+ var STANDARD_TYPES = new Set(Object.values(import_email_editor_core.BlockType));
52
+ function assertSupportedBlocks(blocks) {
53
+ const unsupported = blocks.map((block) => block?.type).filter((type) => !STANDARD_TYPES.has(type));
54
+ if (unsupported.length > 0) {
55
+ throw new Error(
56
+ `Unsupported block type${unsupported.length > 1 ? "s" : ""}: ${unsupported.map((type) => JSON.stringify(type)).join(", ")}. The blocks option can only redefine a standard block type (${[...STANDARD_TYPES].join(", ")}).`
57
+ );
58
+ }
59
+ }
60
+
61
+ // src/savedSections.ts
62
+ var savedSectionId = (id) => `saved:${id}`;
63
+ function registerSavedSections(registry, sections) {
64
+ for (const saved of sections) {
65
+ if (typeof saved?.section !== "object" || saved.section === null) continue;
66
+ registry.register({
67
+ id: savedSectionId(saved.id),
68
+ name: saved.name,
69
+ category: "saved",
70
+ description: saved.description,
71
+ section: saved.section
72
+ });
73
+ }
74
+ }
75
+
76
+ // src/createEditor.ts
77
+ function emptyTemplate() {
78
+ return {
79
+ version: import_email_editor_core2.CURRENT_TEMPLATE_VERSION,
80
+ metadata: { title: "New Email", subject: "", previewText: "" },
81
+ sections: []
82
+ };
83
+ }
84
+ function openable(template) {
85
+ return (0, import_email_editor_core2.withTemplateId)(template ?? emptyTemplate());
86
+ }
87
+ function createEditor(options) {
88
+ const {
89
+ container,
90
+ initialValue,
91
+ theme,
92
+ blocks = [],
93
+ onChange,
94
+ onSave,
95
+ onRequestImage,
96
+ savedSections = [],
97
+ onSaveSection,
98
+ builtInSections = true,
99
+ placeholderBase
100
+ } = options;
101
+ assertSupportedBlocks(blocks);
102
+ const registry = (0, import_email_editor_blocks.createStandardBlockRegistry)({ placeholderBase });
103
+ blocks.forEach((block) => {
104
+ registry.unregister(block.type);
105
+ registry.register(block);
106
+ });
107
+ const prebuiltRegistry = builtInSections ? (0, import_email_editor_blocks.createStandardPrebuiltRegistry)({ placeholderBase }) : (0, import_email_editor_core2.createPrebuiltTemplateRegistry)();
108
+ registerSavedSections(prebuiltRegistry, savedSections);
109
+ let currentTemplate = openable(initialValue);
110
+ let generation = 0;
111
+ const handleChange = (snapshot) => {
112
+ currentTemplate = snapshot;
113
+ onChange?.(currentTemplate);
114
+ };
115
+ const handleSave = () => {
116
+ onSave?.(currentTemplate);
117
+ };
118
+ let root = null;
119
+ const render = () => {
120
+ if (!root) {
121
+ root = (0, import_client.createRoot)(container);
122
+ }
123
+ root.render(
124
+ (0, import_react.createElement)(import_email_editor_ui.EmailEditor, {
125
+ key: generation,
126
+ initialTemplate: currentTemplate,
127
+ onChange: handleChange,
128
+ blockRegistry: registry,
129
+ prebuiltRegistry,
130
+ onSave: handleSave,
131
+ onRequestImage,
132
+ onSaveSection,
133
+ savedSectionNames: savedSections.map((s) => s.name),
134
+ style: themeToStyle(theme)
135
+ })
136
+ );
137
+ };
138
+ render();
139
+ return {
140
+ getValue() {
141
+ return currentTemplate;
142
+ },
143
+ setValue(template) {
144
+ currentTemplate = openable(template);
145
+ generation += 1;
146
+ render();
147
+ },
148
+ getHTML() {
149
+ console.warn("getHTML() requires a server-side compiler");
150
+ return "";
151
+ },
152
+ getMJML() {
153
+ console.warn("getMJML() requires a server-side compiler");
154
+ return "";
155
+ },
156
+ undo() {
157
+ console.warn("Undo via API not yet implemented");
158
+ },
159
+ redo() {
160
+ console.warn("Redo via API not yet implemented");
161
+ },
162
+ destroy() {
163
+ if (root) {
164
+ root.unmount();
165
+ root = null;
166
+ }
167
+ }
168
+ };
169
+ }
170
+ // Annotate the CommonJS export names for ESM import in node:
171
+ 0 && (module.exports = {
172
+ createEditor
173
+ });