@marlinjai/email-editor 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/react.d.mts CHANGED
@@ -1,10 +1,57 @@
1
1
  import * as react from 'react';
2
- import { TemplateSnapshotIn, TemplateSnapshotOut, BlockDefinition } from '@marlinjai/email-editor-core';
2
+ import { CSSProperties } from 'react';
3
+ import { TemplateSnapshotOut, TemplateSnapshotIn, BlockDefinition } from '@marlinjai/email-editor-core';
3
4
  export { BlockDefinition, TemplateSnapshotIn, TemplateSnapshotOut } from '@marlinjai/email-editor-core';
4
5
  import { OnRequestImage, OnSaveSection } from '@marlinjai/email-editor-ui';
5
6
  export { ImageRequest, OnRequestImage, OnSaveSection, RequestedImage } from '@marlinjai/email-editor-ui';
6
7
  import { b as EditorTheme, S as SavedSectionInput } from './types-G7Ak2dWQ.mjs';
7
8
 
9
+ /**
10
+ * Two things about the editor are easy to get wrong, and getting either wrong
11
+ * loses somebody's work silently, so they live here rather than in each screen
12
+ * that embeds the editor:
13
+ *
14
+ * 1. **The editor reports a snapshot on mount.** It normalises what it was
15
+ * given, and says so. That first report is the baseline, not an edit: a
16
+ * screen that treated it as one would show "unsaved changes" on every open
17
+ * and would ask before leaving a document nobody touched. `touched` is set
18
+ * by a real interaction (a pointer or a key inside the editor), and only
19
+ * then does a report mean a change.
20
+ *
21
+ * 2. **The editor reports changes debounced.** The document it last reported
22
+ * may be one edit behind, so reading it the moment somebody presses Save
23
+ * would store the previous document and drop the edit just made. `settled()`
24
+ * waits out that window since the last interaction before reading.
25
+ */
26
+ /** The store's own `onChange` debounce, plus a little room. */
27
+ declare const EDITOR_CHANGE_DEBOUNCE_MS = 300;
28
+ /** The document as a service stores it: the editor's snapshot without its local id. */
29
+ declare function toStoredDocument(snapshot: TemplateSnapshotOut | Record<string, unknown>): Record<string, unknown>;
30
+ type EditorDocument = {
31
+ /** Whether the person has changed anything since the last save. */
32
+ dirty: boolean;
33
+ setDirty: (dirty: boolean) => void;
34
+ /** Hand to the editor's `onChange`. */
35
+ onChange: (snapshot: TemplateSnapshotOut) => void;
36
+ /** Hand to the host element's `onPointerDownCapture` and `onKeyDownCapture`. */
37
+ interacted: () => void;
38
+ /** The current document, once the editor's debounce window has passed. */
39
+ settled: () => Promise<Record<string, unknown>>;
40
+ /** Replaces what is held, for a document loaded or rewritten from outside the editor. */
41
+ replace: (document: Record<string, unknown>, opts?: {
42
+ touched?: boolean;
43
+ }) => void;
44
+ /** What is held right now, without waiting. Prefer `settled()` before storing it. */
45
+ current: () => Record<string, unknown>;
46
+ };
47
+ declare function useEditorDocument(initial: Record<string, unknown>): EditorDocument;
48
+
49
+ /**
50
+ * CSS custom properties for the editor's root element. Returns an empty
51
+ * object when there is no theme, so the stylesheet defaults apply.
52
+ */
53
+ declare function themeToStyle(theme: EditorTheme | undefined): CSSProperties;
54
+
8
55
  interface EmailEditorReactProps {
9
56
  /** Initial template data (uncontrolled) */
10
57
  initialTemplate?: TemplateSnapshotIn;
@@ -48,4 +95,4 @@ interface EmailEditorReactProps {
48
95
  */
49
96
  declare function EmailEditorReact({ initialTemplate, onChange, theme, blocks, onSave, onExport, onNavigateBack, onRequestImage, savedSections, onSaveSection, builtInSections, placeholderBase, }: EmailEditorReactProps): react.JSX.Element;
50
97
 
51
- export { EditorTheme, EmailEditorReact, SavedSectionInput };
98
+ export { EDITOR_CHANGE_DEBOUNCE_MS, type EditorDocument, EditorTheme, EmailEditorReact, type EmailEditorReactProps, SavedSectionInput, themeToStyle, toStoredDocument, useEditorDocument };
package/dist/react.d.ts CHANGED
@@ -1,10 +1,57 @@
1
1
  import * as react from 'react';
2
- import { TemplateSnapshotIn, TemplateSnapshotOut, BlockDefinition } from '@marlinjai/email-editor-core';
2
+ import { CSSProperties } from 'react';
3
+ import { TemplateSnapshotOut, TemplateSnapshotIn, BlockDefinition } from '@marlinjai/email-editor-core';
3
4
  export { BlockDefinition, TemplateSnapshotIn, TemplateSnapshotOut } from '@marlinjai/email-editor-core';
4
5
  import { OnRequestImage, OnSaveSection } from '@marlinjai/email-editor-ui';
5
6
  export { ImageRequest, OnRequestImage, OnSaveSection, RequestedImage } from '@marlinjai/email-editor-ui';
6
7
  import { b as EditorTheme, S as SavedSectionInput } from './types-G7Ak2dWQ.js';
7
8
 
9
+ /**
10
+ * Two things about the editor are easy to get wrong, and getting either wrong
11
+ * loses somebody's work silently, so they live here rather than in each screen
12
+ * that embeds the editor:
13
+ *
14
+ * 1. **The editor reports a snapshot on mount.** It normalises what it was
15
+ * given, and says so. That first report is the baseline, not an edit: a
16
+ * screen that treated it as one would show "unsaved changes" on every open
17
+ * and would ask before leaving a document nobody touched. `touched` is set
18
+ * by a real interaction (a pointer or a key inside the editor), and only
19
+ * then does a report mean a change.
20
+ *
21
+ * 2. **The editor reports changes debounced.** The document it last reported
22
+ * may be one edit behind, so reading it the moment somebody presses Save
23
+ * would store the previous document and drop the edit just made. `settled()`
24
+ * waits out that window since the last interaction before reading.
25
+ */
26
+ /** The store's own `onChange` debounce, plus a little room. */
27
+ declare const EDITOR_CHANGE_DEBOUNCE_MS = 300;
28
+ /** The document as a service stores it: the editor's snapshot without its local id. */
29
+ declare function toStoredDocument(snapshot: TemplateSnapshotOut | Record<string, unknown>): Record<string, unknown>;
30
+ type EditorDocument = {
31
+ /** Whether the person has changed anything since the last save. */
32
+ dirty: boolean;
33
+ setDirty: (dirty: boolean) => void;
34
+ /** Hand to the editor's `onChange`. */
35
+ onChange: (snapshot: TemplateSnapshotOut) => void;
36
+ /** Hand to the host element's `onPointerDownCapture` and `onKeyDownCapture`. */
37
+ interacted: () => void;
38
+ /** The current document, once the editor's debounce window has passed. */
39
+ settled: () => Promise<Record<string, unknown>>;
40
+ /** Replaces what is held, for a document loaded or rewritten from outside the editor. */
41
+ replace: (document: Record<string, unknown>, opts?: {
42
+ touched?: boolean;
43
+ }) => void;
44
+ /** What is held right now, without waiting. Prefer `settled()` before storing it. */
45
+ current: () => Record<string, unknown>;
46
+ };
47
+ declare function useEditorDocument(initial: Record<string, unknown>): EditorDocument;
48
+
49
+ /**
50
+ * CSS custom properties for the editor's root element. Returns an empty
51
+ * object when there is no theme, so the stylesheet defaults apply.
52
+ */
53
+ declare function themeToStyle(theme: EditorTheme | undefined): CSSProperties;
54
+
8
55
  interface EmailEditorReactProps {
9
56
  /** Initial template data (uncontrolled) */
10
57
  initialTemplate?: TemplateSnapshotIn;
@@ -48,4 +95,4 @@ interface EmailEditorReactProps {
48
95
  */
49
96
  declare function EmailEditorReact({ initialTemplate, onChange, theme, blocks, onSave, onExport, onNavigateBack, onRequestImage, savedSections, onSaveSection, builtInSections, placeholderBase, }: EmailEditorReactProps): react.JSX.Element;
50
97
 
51
- export { EditorTheme, EmailEditorReact, SavedSectionInput };
98
+ export { EDITOR_CHANGE_DEBOUNCE_MS, type EditorDocument, EditorTheme, EmailEditorReact, type EmailEditorReactProps, SavedSectionInput, themeToStyle, toStoredDocument, useEditorDocument };
package/dist/react.js CHANGED
@@ -20,11 +20,15 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
20
20
  // src/react.tsx
21
21
  var react_exports = {};
22
22
  __export(react_exports, {
23
- EmailEditorReact: () => EmailEditorReact
23
+ EDITOR_CHANGE_DEBOUNCE_MS: () => EDITOR_CHANGE_DEBOUNCE_MS,
24
+ EmailEditorReact: () => EmailEditorReact,
25
+ themeToStyle: () => themeToStyle,
26
+ toStoredDocument: () => toStoredDocument,
27
+ useEditorDocument: () => useEditorDocument
24
28
  });
25
29
  module.exports = __toCommonJS(react_exports);
26
- var import_react = require("react");
27
- var import_email_editor_core2 = require("@marlinjai/email-editor-core");
30
+ var import_react2 = require("react");
31
+ var import_email_editor_core3 = require("@marlinjai/email-editor-core");
28
32
  var import_email_editor_blocks = require("@marlinjai/email-editor-blocks");
29
33
  var import_email_editor_ui = require("@marlinjai/email-editor-ui");
30
34
 
@@ -70,6 +74,47 @@ function assertSupportedBlocks(blocks) {
70
74
  }
71
75
  }
72
76
 
77
+ // src/editorDocument.ts
78
+ var import_react = require("react");
79
+ var import_email_editor_core2 = require("@marlinjai/email-editor-core");
80
+ var EDITOR_CHANGE_DEBOUNCE_MS = import_email_editor_core2.DEFAULT_ON_CHANGE_DEBOUNCE_MS;
81
+ var SETTLE_MARGIN_MS = 100;
82
+ function toStoredDocument(snapshot) {
83
+ const { id: _id, ...rest } = snapshot;
84
+ return rest;
85
+ }
86
+ function useEditorDocument(initial) {
87
+ const [dirty, setDirty] = (0, import_react.useState)(false);
88
+ const latest = (0, import_react.useRef)(toStoredDocument(initial));
89
+ const touched = (0, import_react.useRef)(false);
90
+ const lastInteraction = (0, import_react.useRef)(0);
91
+ const onChange = (0, import_react.useCallback)((snapshot) => {
92
+ latest.current = toStoredDocument(snapshot);
93
+ if (touched.current) setDirty(true);
94
+ }, []);
95
+ const interacted = (0, import_react.useCallback)(() => {
96
+ touched.current = true;
97
+ lastInteraction.current = Date.now();
98
+ }, []);
99
+ const settled = (0, import_react.useCallback)(async () => {
100
+ const wait = lastInteraction.current + EDITOR_CHANGE_DEBOUNCE_MS + SETTLE_MARGIN_MS - Date.now();
101
+ if (wait > 0) await new Promise((r) => setTimeout(r, wait));
102
+ return latest.current;
103
+ }, []);
104
+ const replace = (0, import_react.useCallback)((document, opts = {}) => {
105
+ latest.current = toStoredDocument(document);
106
+ if (opts.touched) {
107
+ touched.current = true;
108
+ setDirty(true);
109
+ } else {
110
+ touched.current = false;
111
+ setDirty(false);
112
+ }
113
+ }, []);
114
+ const current = (0, import_react.useCallback)(() => latest.current, []);
115
+ return { dirty, setDirty, onChange, interacted, settled, replace, current };
116
+ }
117
+
73
118
  // src/react.tsx
74
119
  var import_jsx_runtime = require("react/jsx-runtime");
75
120
  function EmailEditorReact({
@@ -86,7 +131,7 @@ function EmailEditorReact({
86
131
  builtInSections = true,
87
132
  placeholderBase
88
133
  }) {
89
- const [registry] = (0, import_react.useState)(() => {
134
+ const [registry] = (0, import_react2.useState)(() => {
90
135
  assertSupportedBlocks(blocks);
91
136
  const reg = (0, import_email_editor_blocks.createStandardBlockRegistry)({ placeholderBase });
92
137
  blocks.forEach((block) => {
@@ -95,13 +140,13 @@ function EmailEditorReact({
95
140
  });
96
141
  return reg;
97
142
  });
98
- const [prebuiltRegistry] = (0, import_react.useState)(() => {
99
- const reg = builtInSections ? (0, import_email_editor_blocks.createStandardPrebuiltRegistry)({ placeholderBase }) : (0, import_email_editor_core2.createPrebuiltTemplateRegistry)();
143
+ const [prebuiltRegistry] = (0, import_react2.useState)(() => {
144
+ const reg = builtInSections ? (0, import_email_editor_blocks.createStandardPrebuiltRegistry)({ placeholderBase }) : (0, import_email_editor_core3.createPrebuiltTemplateRegistry)();
100
145
  registerSavedSections(reg, savedSections ?? []);
101
146
  return reg;
102
147
  });
103
- const savedSectionNames = (0, import_react.useMemo)(() => (savedSections ?? []).map((s) => s.name), [savedSections]);
104
- const style = (0, import_react.useMemo)(() => themeToStyle(theme), [theme]);
148
+ const savedSectionNames = (0, import_react2.useMemo)(() => (savedSections ?? []).map((s) => s.name), [savedSections]);
149
+ const style = (0, import_react2.useMemo)(() => themeToStyle(theme), [theme]);
105
150
  return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
106
151
  import_email_editor_ui.EmailEditor,
107
152
  {
@@ -121,5 +166,9 @@ function EmailEditorReact({
121
166
  }
122
167
  // Annotate the CommonJS export names for ESM import in node:
123
168
  0 && (module.exports = {
124
- EmailEditorReact
169
+ EDITOR_CHANGE_DEBOUNCE_MS,
170
+ EmailEditorReact,
171
+ themeToStyle,
172
+ toStoredDocument,
173
+ useEditorDocument
125
174
  });
package/dist/react.mjs CHANGED
@@ -5,10 +5,53 @@ import {
5
5
  } from "./chunk-EFHKH3BN.mjs";
6
6
 
7
7
  // src/react.tsx
8
- import { useMemo, useState } from "react";
8
+ import { useMemo, useState as useState2 } from "react";
9
9
  import { createPrebuiltTemplateRegistry } from "@marlinjai/email-editor-core";
10
10
  import { createStandardBlockRegistry, createStandardPrebuiltRegistry } from "@marlinjai/email-editor-blocks";
11
11
  import { EmailEditor } from "@marlinjai/email-editor-ui";
12
+
13
+ // src/editorDocument.ts
14
+ import { useCallback, useRef, useState } from "react";
15
+ import { DEFAULT_ON_CHANGE_DEBOUNCE_MS } from "@marlinjai/email-editor-core";
16
+ var EDITOR_CHANGE_DEBOUNCE_MS = DEFAULT_ON_CHANGE_DEBOUNCE_MS;
17
+ var SETTLE_MARGIN_MS = 100;
18
+ function toStoredDocument(snapshot) {
19
+ const { id: _id, ...rest } = snapshot;
20
+ return rest;
21
+ }
22
+ function useEditorDocument(initial) {
23
+ const [dirty, setDirty] = useState(false);
24
+ const latest = useRef(toStoredDocument(initial));
25
+ const touched = useRef(false);
26
+ const lastInteraction = useRef(0);
27
+ const onChange = useCallback((snapshot) => {
28
+ latest.current = toStoredDocument(snapshot);
29
+ if (touched.current) setDirty(true);
30
+ }, []);
31
+ const interacted = useCallback(() => {
32
+ touched.current = true;
33
+ lastInteraction.current = Date.now();
34
+ }, []);
35
+ const settled = useCallback(async () => {
36
+ const wait = lastInteraction.current + EDITOR_CHANGE_DEBOUNCE_MS + SETTLE_MARGIN_MS - Date.now();
37
+ if (wait > 0) await new Promise((r) => setTimeout(r, wait));
38
+ return latest.current;
39
+ }, []);
40
+ const replace = useCallback((document, opts = {}) => {
41
+ latest.current = toStoredDocument(document);
42
+ if (opts.touched) {
43
+ touched.current = true;
44
+ setDirty(true);
45
+ } else {
46
+ touched.current = false;
47
+ setDirty(false);
48
+ }
49
+ }, []);
50
+ const current = useCallback(() => latest.current, []);
51
+ return { dirty, setDirty, onChange, interacted, settled, replace, current };
52
+ }
53
+
54
+ // src/react.tsx
12
55
  import { jsx } from "react/jsx-runtime";
13
56
  function EmailEditorReact({
14
57
  initialTemplate,
@@ -24,7 +67,7 @@ function EmailEditorReact({
24
67
  builtInSections = true,
25
68
  placeholderBase
26
69
  }) {
27
- const [registry] = useState(() => {
70
+ const [registry] = useState2(() => {
28
71
  assertSupportedBlocks(blocks);
29
72
  const reg = createStandardBlockRegistry({ placeholderBase });
30
73
  blocks.forEach((block) => {
@@ -33,7 +76,7 @@ function EmailEditorReact({
33
76
  });
34
77
  return reg;
35
78
  });
36
- const [prebuiltRegistry] = useState(() => {
79
+ const [prebuiltRegistry] = useState2(() => {
37
80
  const reg = builtInSections ? createStandardPrebuiltRegistry({ placeholderBase }) : createPrebuiltTemplateRegistry();
38
81
  registerSavedSections(reg, savedSections ?? []);
39
82
  return reg;
@@ -58,5 +101,9 @@ function EmailEditorReact({
58
101
  );
59
102
  }
60
103
  export {
61
- EmailEditorReact
104
+ EDITOR_CHANGE_DEBOUNCE_MS,
105
+ EmailEditorReact,
106
+ themeToStyle,
107
+ toStoredDocument,
108
+ useEditorDocument
62
109
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@marlinjai/email-editor",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "Embeddable visual email editor (React component and vanilla factory) that produces an MJML-compilable document",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -47,9 +47,9 @@
47
47
  "./styles.css": "./dist/styles.css"
48
48
  },
49
49
  "dependencies": {
50
- "@marlinjai/email-editor-blocks": "^0.2.0",
51
- "@marlinjai/email-editor-ui": "^0.2.0",
52
- "@marlinjai/email-editor-core": "^0.2.0"
50
+ "@marlinjai/email-editor-blocks": "^0.3.0",
51
+ "@marlinjai/email-editor-ui": "^0.3.0",
52
+ "@marlinjai/email-editor-core": "^0.3.0"
53
53
  },
54
54
  "peerDependencies": {
55
55
  "react": "^18.2.0 || ^19.0.0",