@maildeno/editor 0.1.0 → 0.1.2

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/README.md CHANGED
@@ -1,6 +1,6 @@
1
- # @maildeno/editor — Vue
1
+ # @maildeno/editor
2
2
 
3
- A drag-and-drop email template editor for Vue 3 apps.
3
+ A drag-and-drop email template editor.
4
4
 
5
5
  ## Install
6
6
 
@@ -11,7 +11,7 @@ npm install @maildeno/editor
11
11
  One package, no peer dependencies, no CSS import. The editor injects its own
12
12
  styling when it mounts.
13
13
 
14
- ## Minimal usage
14
+ ## Minimal usage Vue
15
15
 
16
16
  ```vue
17
17
  <script setup lang="ts">
@@ -23,52 +23,6 @@ import { EmailEditor } from "@maildeno/editor";
23
23
  </template>
24
24
  ```
25
25
 
26
- ## Full example
27
-
28
- ```vue
29
- <script setup lang="ts">
30
- import { ref } from "vue";
31
- import { EmailEditor } from "@maildeno/editor";
32
- import { myAdapter } from "./adapter";
33
-
34
- const editor = ref<InstanceType<typeof EmailEditor> | null>(null);
35
-
36
- function handleSave({ templateId }: { templateId: string | null }) {
37
- // Persist which template is open, however you track it
38
- router.replace(`/editor/${templateId}`);
39
- }
40
-
41
- async function handleSendTestEmail(payload: {
42
- to: string; subject: string; html: string;
43
- }) {
44
- await fetch("/api/send-test", { method: "POST", body: JSON.stringify(payload) });
45
- }
46
-
47
- async function publish() {
48
- const html = editor.value?.getHtml();
49
- if (!html) return; // empty canvas
50
- await fetch("/api/campaigns", { method: "POST", body: JSON.stringify({ html }) });
51
- }
52
- </script>
53
-
54
- <template>
55
- <div style="display: flex; flex-direction: column; height: 100vh">
56
- <button @click="publish">Publish</button>
57
-
58
- <EmailEditor
59
- ref="editor"
60
- style="flex: 1; min-height: 0"
61
- template-id="welcome_email"
62
- :storage-adapter="myAdapter"
63
- :theme="{ primaryColor: '#6366f1' }"
64
- :capabilities="{ export: ['html', 'json'] }"
65
- :on-send-test-email="handleSendTestEmail"
66
- @save="handleSave"
67
- />
68
- </div>
69
- </template>
70
- ```
71
-
72
26
  ## Props and events
73
27
 
74
28
  | Prop | Type |
@@ -93,66 +47,51 @@ editor.value.getReactEmail();
93
47
  editor.value.getJson();
94
48
  ```
95
49
 
96
- Theming is reactive through the `theme` prop — change it and the editor
97
- re-themes live; no imperative call needed.
98
-
99
- ## Alternative: `init()`
100
-
101
- The same API the React and vanilla guides use also works in Vue:
50
+ ## Minimal usage React
102
51
 
103
- ```vue
104
- <script setup lang="ts">
105
- import { onMounted, onBeforeUnmount, ref } from "vue";
52
+ ```tsx
53
+ import { useEffect, useRef } from "react";
106
54
  import { init, type EditorHandle } from "@maildeno/editor/init";
107
55
 
108
- const container = ref<HTMLDivElement | null>(null);
109
- let handle: EditorHandle | null = null;
56
+ export default function Editor() {
57
+ const container = useRef<HTMLDivElement>(null);
58
+ const handle = useRef<EditorHandle | null>(null);
110
59
 
111
- onMounted(async () => {
112
- handle = await init({ container: container.value! });
113
- });
114
- onBeforeUnmount(() => handle?.destroy());
115
- </script>
60
+ useEffect(() => {
61
+ let cancelled = false;
116
62
 
117
- <template>
118
- <div ref="container" />
119
- </template>
120
- ```
121
-
122
- **Prefer the component unless you specifically need style isolation.** `init()`
123
- bundles its own Vue so it can run on framework-free pages — calling it from a
124
- Vue app ships two Vue runtimes (~60 kB gzipped plus a second reactivity graph).
125
- The component uses the Vue you already have, and gives you reactive props,
126
- template refs, normal event binding and devtools visibility.
127
-
128
- The one case where `init()` wins: it renders in a shadow root, so your app's
129
- global CSS can't reach the editor. If your stylesheets conflict with it, that
130
- isolation is worth the duplicate runtime.
63
+ init({ container: container.current! }).then((h) => {
64
+ // React 18 StrictMode mounts effects twice in development. Without
65
+ // this guard the first instance is orphaned and never destroyed.
66
+ if (cancelled) return h.destroy();
67
+ handle.current = h;
68
+ });
131
69
 
132
- ## Feature reference
70
+ return () => {
71
+ cancelled = true;
72
+ handle.current?.destroy();
73
+ };
74
+ }, []);
133
75
 
134
- Everything below is identical across frameworks. Only the mounting differs.
76
+ return <div ref={container} />;
77
+ }
78
+ ```
135
79
 
136
- ### The editor at a glance
80
+ ## `EditorHandle`
137
81
 
138
- - **Drag-and-drop block editor** paragraph, heading, image, video, list, button, anchor, divider, spacer, menu, socials, plus custom blocks you register.
139
- - **Four export formats** — HTML, MJML, React Email (`.tsx`), JSON.
140
- - **ESP-aware conditional logic** — visibility rules compile to the right syntax for Klaviyo, Mailchimp, Braze, SFMC, HubSpot, Iterable, Handlebars and more.
141
- - **Merge tags** with an in-editor picker.
142
- - **Saved templates and saved rows** — reusable snippets and full documents.
143
- - **Desktop/mobile styling** — per-block mobile overrides.
144
- - **Undo/redo**, autosave draft recovery, and a preview with client-specific rendering checks.
145
- - **Zero backend required** — defaults to `localStorage`; bring an adapter to use your own.
82
+ `init()` resolves to this once the editor has mounted:
146
83
 
147
- ### Configuration
84
+ | Member | Description |
85
+ | --- | --- |
86
+ | `element` | The underlying `<maildeno-editor>` DOM node. |
87
+ | `on(event, fn)` / `off(event, fn)` | Subscribe to editor events. Currently `"save"`. |
88
+ | `setTheme(theme)` | Re-theme live, any time after mount. |
89
+ | `getHtml(mode?)` | Current template as an HTML email string, or `null` if empty. |
90
+ | `getMjml(mode?)` | Current template as MJML. |
91
+ | `getReactEmail(mode?)` | Current template as React Email `.tsx` source. |
92
+ | `getJson()` | Current template as a plain object. |
93
+ | `destroy()` | Unmount and clean up. Always call this on unmount. |
148
94
 
149
- | Option | Type | Purpose |
150
- | --- | --- | --- |
151
- | `templateId` | `string` | Load an existing template on mount. Omit to start blank. |
152
- | `storageAdapter` | `EditorStorageAdapter` | Where templates, rows and images persist. Omit for `localStorage`. |
153
- | `theme` | `{ primaryColor?, surfaceColor? }` | Brand colour, expanded into a full 50–950 shade scale. |
154
- | `capabilities` | `{ export?: Array<"html"\|"mjml"\|"react"\|"json"> }` | Restrict which export formats appear. Omit for all four. |
155
- | `onSendTestEmail` | `(payload) => Promise<void>` | Enables the "Send test" button. Hidden entirely when omitted. |
156
95
 
157
96
  ### Getting content out
158
97
 
@@ -179,6 +118,22 @@ if (!html) return; // nothing built yet
179
118
 
180
119
  `getJson()` returns the portable template format — `template_id`, `template_name`, `canvas`, `rows`, `schema_version`. Store it and pass it back via your adapter's `loadTemplate` to restore exactly.
181
120
 
121
+ ## Feature reference
122
+
123
+ Everything below is identical across frameworks. Only the mounting differs.
124
+
125
+ ### The editor at a glance
126
+
127
+ - **Drag-and-drop block editor** — paragraph, heading, image, video, list, button, anchor, divider, spacer, menu, socials, plus custom blocks you register.
128
+ - **Four export formats** — HTML, MJML, React Email (`.tsx`), JSON.
129
+ - **ESP-aware conditional logic** — visibility rules compile to the right syntax for Klaviyo, Mailchimp, Braze, SFMC, HubSpot, Iterable, Handlebars and more.
130
+ - **Merge tags** with an in-editor picker.
131
+ - **Saved templates and saved rows** — reusable snippets and full documents.
132
+ - **Desktop/mobile styling** — per-block mobile overrides.
133
+ - **Undo/redo**, autosave draft recovery, and a preview with client-specific rendering checks.
134
+ - **Zero backend required** — defaults to `localStorage`; bring an adapter to use your own.
135
+
136
+
182
137
  ### Storage adapter
183
138
 
184
139
  The editor never talks to your backend directly. Implement this and it uses your storage for everything:
@@ -420,7 +375,7 @@ Pass it back as `templateId` next time to reopen.
420
375
 
421
376
  ### Practical notes
422
377
 
423
- - **The container needs a real height.** The editor fills its parent; `height: 100vh` or a sized flex child both work. A container with no height renders nothing visible.
378
+ - **The container needs a real height.** The editor fills its parent; `height: 100vh` or a sized flex child both work.
424
379
  - **`transform`, `filter`, `perspective` or `will-change` on any ancestor** breaks `position: fixed` inside it — floating toolbars and pickers will land in the wrong place. Avoid them above the mount point.
425
380
  - **Desktop only.** The editor shows a notice on small screens rather than attempting a mobile drag-and-drop UI.
426
381
  - **Autosave** keeps a local draft, so a refresh mid-edit recovers. "New template" clears it deliberately.
@@ -1,4 +1,4 @@
1
- import { I as openBlock, R as renderList, V as resolveDynamicComponent, _ as createBlock, _t as toDisplayString, f as Fragment, g as createBaseVNode, gt as normalizeStyle, h as computed, v as createCommentVNode, w as defineComponent, x as createTextVNode, y as createElementBlock } from "./vue.runtime.esm-bundler-DwQEK7WO.js";
1
+ import { I as openBlock, R as renderList, V as resolveDynamicComponent, _ as createBlock, _t as toDisplayString, f as Fragment, g as createBaseVNode, gt as normalizeStyle, h as computed, v as createCommentVNode, w as defineComponent, x as createTextVNode, y as createElementBlock } from "./vue.runtime.esm-bundler-CfYyFhW6.js";
2
2
  //#region src/components/features/emailBuilder/product/RowPreviewLegacyComponent.vue?vue&type=script&setup=true&lang.ts
3
3
  var _hoisted_1 = ["innerHTML"];
4
4
  var _hoisted_2 = ["src", "alt"];
@@ -1,8 +1,8 @@
1
- import { C as defineAsyncComponent, I as openBlock, R as renderList, S as createVNode, f as Fragment, ft as unref, gt as normalizeStyle, h as computed, v as createCommentVNode, w as defineComponent, y as createElementBlock } from "./vue.runtime.esm-bundler-DwQEK7WO.js";
1
+ import { C as defineAsyncComponent, I as openBlock, R as renderList, S as createVNode, f as Fragment, ft as unref, gt as normalizeStyle, h as computed, v as createCommentVNode, w as defineComponent, y as createElementBlock } from "./vue.runtime.esm-bundler-CfYyFhW6.js";
2
2
  //#region src/components/features/emailBuilder/product/RowPreviewRow.vue?vue&type=script&setup=true&lang.ts
3
3
  var _hoisted_1 = {
4
4
  key: 0,
5
- class: "w-full border border-dashed border-gray-200 rounded",
5
+ class: "w-full border border-dashed border-(--md-border) rounded",
6
6
  style: { "min-height": "10px" }
7
7
  };
8
8
  //#endregion
@@ -15,7 +15,7 @@ var RowPreviewRow_default = /* @__PURE__ */ defineComponent({
15
15
  depth: {}
16
16
  },
17
17
  setup(__props) {
18
- const RowPreviewChildren = defineAsyncComponent(() => import("./element-DN22fZmQ.js").then((n) => n.o));
18
+ const RowPreviewChildren = defineAsyncComponent(() => import("./element-DLJCEP_z.js").then((n) => n.o));
19
19
  const props = __props;
20
20
  function resolveBackground(item) {
21
21
  const bg = item?.backgroundGradient;
@@ -2,7 +2,7 @@ import { Fragment, computed, createCommentVNode, createElementBlock, createVNode
2
2
  //#region src/components/features/emailBuilder/product/RowPreviewRow.vue?vue&type=script&setup=true&lang.ts
3
3
  var _hoisted_1 = {
4
4
  key: 0,
5
- class: "w-full border border-dashed border-gray-200 rounded",
5
+ class: "w-full border border-dashed border-(--md-border) rounded",
6
6
  style: { "min-height": "10px" }
7
7
  };
8
8
  //#endregion