@standhigher/react-email-editor 0.4.3 → 0.5.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/dist/index.js CHANGED
@@ -44,7 +44,7 @@ function usePluginEditor() {
44
44
  }
45
45
 
46
46
  // src/shell/EmailEditorShell.tsx
47
- import { useState as useState18 } from "react";
47
+ import { useState as useState20 } from "react";
48
48
 
49
49
  // src/editor-dnd/EditorDnd.tsx
50
50
  import { useLayoutEffect as useLayoutEffect5, useMemo, useRef as useRef7, useState as useState7 } from "react";
@@ -142,7 +142,7 @@ var DEFAULT_EMAIL_BODY = {
142
142
  textColor: "#000000",
143
143
  backgroundColor: "#ffffff",
144
144
  contentWidth: CANVAS_DEVICE_WIDTH.desktop,
145
- fontFamily: "Arial, Helvetica, sans-serif",
145
+ fontFamily: "",
146
146
  fontWeight: FONT_WEIGHTS.normal,
147
147
  linkColor: "#005bd3",
148
148
  linkDecoration: LINK_DECORATIONS.underline,
@@ -379,7 +379,7 @@ var DEFAULT_TYPOGRAPHY = Object.freeze({
379
379
  function typographyWithBody(body) {
380
380
  return __spreadValues(__spreadValues({}, DEFAULT_TYPOGRAPHY), body ? {
381
381
  color: body.textColor,
382
- fontFamily: body.fontFamily,
382
+ fontFamily: cssFont(body.fontFamily, DEFAULT_TYPOGRAPHY.fontFamily),
383
383
  fontWeight: body.fontWeight,
384
384
  linkColor: body.linkColor,
385
385
  linkDecoration: body.linkDecoration
@@ -2661,9 +2661,34 @@ function createEmailDocument(blocks = []) {
2661
2661
  function createEmptyDocument() {
2662
2662
  return { version: DOCUMENT_VERSION, subject: "", body: resolveEmailBody(), blocks: [] };
2663
2663
  }
2664
+ function isRecord(value) {
2665
+ return value !== null && typeof value === "object" && !Array.isArray(value);
2666
+ }
2664
2667
  function ensureEmailDocument(document) {
2665
- if (document.version !== DOCUMENT_VERSION) {
2666
- throw new Error(`Unsupported email document version: ${document.version}. Expected ${DOCUMENT_VERSION}.`);
2668
+ if (!isRecord(document)) {
2669
+ throw new TypeError("Invalid email document: expected an object.");
2670
+ }
2671
+ if (!Array.isArray(document.blocks)) {
2672
+ throw new TypeError("Invalid email document: blocks must be an array.");
2673
+ }
2674
+ if (document.body != null && !isRecord(document.body)) {
2675
+ throw new TypeError("Invalid email document: body must be an object when provided.");
2676
+ }
2677
+ if (document.subject !== void 0 && typeof document.subject !== "string") {
2678
+ throw new TypeError("Invalid email document: subject must be a string when provided.");
2679
+ }
2680
+ for (const [index, block] of document.blocks.entries()) {
2681
+ const path = `Invalid email document: blocks[${index}]`;
2682
+ if (!isRecord(block)) throw new TypeError(`${path} must be an object.`);
2683
+ if (typeof block.id !== "string" || !block.id.trim()) {
2684
+ throw new TypeError(`${path}.id must be a non-empty string.`);
2685
+ }
2686
+ if (typeof block.type !== "string" || !block.type.trim()) {
2687
+ throw new TypeError(`${path}.type must be a non-empty string.`);
2688
+ }
2689
+ if (!isRecord(block.controls)) {
2690
+ throw new TypeError(`${path}.controls must be an object.`);
2691
+ }
2667
2692
  }
2668
2693
  return __spreadProps(__spreadValues({}, document), { body: resolveEmailBody(document.body) });
2669
2694
  }
@@ -2807,7 +2832,7 @@ var EmailControls = {
2807
2832
 
2808
2833
  // src/plugins/define-block.ts
2809
2834
  function defineBlock(input) {
2810
- var _a, _b, _c;
2835
+ var _a, _b, _c, _d;
2811
2836
  const options = snapshotValue(input);
2812
2837
  if (!options.type.trim() || !options.title.trim()) throw new Error("Block type and title are required.");
2813
2838
  const schema = (_a = options.controls) != null ? _a : {};
@@ -2825,7 +2850,7 @@ function defineBlock(input) {
2825
2850
  type: options.type,
2826
2851
  title: options.title,
2827
2852
  description: options.description,
2828
- icon: options.icon,
2853
+ icon: (_c = options.icon) != null ? _c : esc(Array.from(options.title.trim())[0]),
2829
2854
  override: options.override,
2830
2855
  schema,
2831
2856
  controls: Object.entries(schema).map(([name, spec]) => __spreadProps(__spreadValues({}, spec), { name })),
@@ -2834,7 +2859,7 @@ function defineBlock(input) {
2834
2859
  inspectorSections: options.inspectorSections,
2835
2860
  defaultStyle: options.defaultStyle,
2836
2861
  defaultData: options.defaultData,
2837
- inlineControl: options.Canvas ? void 0 : (_c = inline[0]) == null ? void 0 : _c[0],
2862
+ inlineControl: options.Canvas ? void 0 : (_d = inline[0]) == null ? void 0 : _d[0],
2838
2863
  canvas: bindPluginComponent(options.Canvas),
2839
2864
  inspector: bindPluginComponent(options.Inspector),
2840
2865
  renderEmail: (ctx) => {
@@ -3471,7 +3496,7 @@ var BODY_CONTROL_SECTIONS = [
3471
3496
  { title: "General", controls: [
3472
3497
  { id: "textColor", group: "typography", label: "Text color", control: "color" },
3473
3498
  { id: "linkColor", group: "typography", label: "Link color", control: "color" },
3474
- { id: "fontFamily", group: "typography", label: "Font family", control: "select", options: copyFontFamilies() },
3499
+ { id: "fontFamily", group: "typography", label: "Font family", control: "select", options: [{ label: "Default", value: "" }, ...copyFontFamilies()] },
3475
3500
  __spreadProps(__spreadValues({}, FONT_WEIGHT), { id: "fontWeight", label: "Font weight" })
3476
3501
  ] },
3477
3502
  { title: "Content", controls: [
@@ -4589,12 +4614,11 @@ function Inspector({
4589
4614
  ] });
4590
4615
  }
4591
4616
 
4592
- // src/stage/canvas/Canvas.tsx
4593
- import { useDroppable } from "@dnd-kit/core";
4594
- import {
4595
- SortableContext,
4596
- verticalListSortingStrategy
4597
- } from "@dnd-kit/sortable";
4617
+ // src/stage/Stage.tsx
4618
+ import { useMemo as useMemo4, useState as useState18 } from "react";
4619
+
4620
+ // src/stage/preview/usePreviewBuffer.ts
4621
+ import { useCallback, useEffect as useEffect6, useLayoutEffect as useLayoutEffect9, useReducer, useRef as useRef13, useState as useState16 } from "react";
4598
4622
 
4599
4623
  // src/render/layout.ts
4600
4624
  function surfaceStyle(body) {
@@ -4611,27 +4635,214 @@ function viewportWidth(device, body) {
4611
4635
  return `${emailViewportWidth(device, body.contentWidth)}px`;
4612
4636
  }
4613
4637
 
4638
+ // src/stage/preview/buffer.ts
4639
+ function createPreviewBuffer() {
4640
+ return { frames: [null, null], front: null, failure: null };
4641
+ }
4642
+ function previewBufferReducer(state, action) {
4643
+ var _a, _b, _c;
4644
+ switch (action.type) {
4645
+ case "generated": {
4646
+ const match = action.force ? -1 : state.frames.findIndex((frame) => frame && frame.status !== "failed" && frame.html === action.snapshot.html && frame.width === action.snapshot.width);
4647
+ const target = match < 0 ? state.front === 0 ? 1 : 0 : match;
4648
+ const frames = [...state.frames];
4649
+ frames[target] = match < 0 ? action.snapshot : __spreadProps(__spreadValues({}, frames[target]), { source: action.snapshot.source });
4650
+ const ready = ((_a = frames[target]) == null ? void 0 : _a.status) === "ready";
4651
+ return { frames, front: ready ? target : state.front, failure: ready ? null : state.failure };
4652
+ }
4653
+ case "ready": {
4654
+ const index = state.frames.findIndex((frame) => frame && frame.id === action.id && frame.status === "loading");
4655
+ if (index < 0) return state;
4656
+ const frames = [...state.frames];
4657
+ frames[index] = __spreadProps(__spreadValues({}, frames[index]), { status: "ready" });
4658
+ const current = ((_b = frames[index]) == null ? void 0 : _b.source) === action.source;
4659
+ return { frames, front: current ? index : state.front, failure: current ? null : state.failure };
4660
+ }
4661
+ case "failed": {
4662
+ if (action.id === void 0) {
4663
+ const frames2 = [state.frames[0], state.frames[1]];
4664
+ for (const index2 of [0, 1]) {
4665
+ const frame = frames2[index2];
4666
+ if ((frame == null ? void 0 : frame.status) === "loading") frames2[index2] = __spreadProps(__spreadValues({}, frame), { status: "failed" });
4667
+ }
4668
+ return __spreadProps(__spreadValues({}, state), { frames: frames2, failure: action.failure });
4669
+ }
4670
+ const index = state.frames.findIndex((frame) => frame && frame.id === action.id && frame.status === "loading");
4671
+ if (index < 0) return state;
4672
+ const frames = [...state.frames];
4673
+ frames[index] = __spreadProps(__spreadValues({}, frames[index]), { status: "failed" });
4674
+ return __spreadProps(__spreadValues({}, state), { frames, failure: ((_c = frames[index]) == null ? void 0 : _c.source) === action.failure.source ? action.failure : state.failure });
4675
+ }
4676
+ }
4677
+ }
4678
+
4679
+ // src/stage/preview/constants.ts
4680
+ var PREVIEW_DEBOUNCE_MS = 400;
4681
+ var PREVIEW_MAX_WAIT_MS = 2e3;
4682
+ var PREVIEW_LOAD_TIMEOUT_MS = 15e3;
4683
+ var PREVIEW_BUFFER_SLOTS = [0, 1];
4684
+
4685
+ // src/stage/preview/usePreviewBuffer.ts
4686
+ function usePreviewBuffer(source, requested) {
4687
+ var _a;
4688
+ const [state, dispatch] = useReducer(previewBufferReducer, void 0, createPreviewBuffer);
4689
+ const [request, setRequest] = useState16({ key: requested ? 1 : 0, force: false });
4690
+ const previousRequested = useRef13(requested);
4691
+ const sourceRef = useRef13(source);
4692
+ const pendingSince = useRef13(null);
4693
+ const attempted = useRef13(null);
4694
+ const revision = useRef13(0);
4695
+ useLayoutEffect9(() => {
4696
+ sourceRef.current = source;
4697
+ }, [source]);
4698
+ const flush = useCallback(() => setRequest((value) => __spreadProps(__spreadValues({}, value), { key: value.key + 1, force: false })), []);
4699
+ useEffect6(() => {
4700
+ var _a2, _b, _c;
4701
+ const enteringPreview = requested && !previousRequested.current;
4702
+ previousRequested.current = requested;
4703
+ const newRequest = request.key > 0 && ((_a2 = attempted.current) == null ? void 0 : _a2.requestKey) !== request.key;
4704
+ const force = newRequest && request.force;
4705
+ if (((_b = attempted.current) == null ? void 0 : _b.source) === source && !force) {
4706
+ attempted.current.requestKey = request.key;
4707
+ return;
4708
+ }
4709
+ const immediate = enteringPreview || newRequest;
4710
+ (_c = pendingSince.current) != null ? _c : pendingSince.current = Date.now();
4711
+ const remaining = Math.max(0, PREVIEW_MAX_WAIT_MS - (Date.now() - pendingSince.current));
4712
+ let idle;
4713
+ let cancelled = false;
4714
+ const generate = () => {
4715
+ if (cancelled || sourceRef.current !== source) return;
4716
+ attempted.current = { source, requestKey: request.key };
4717
+ pendingSince.current = null;
4718
+ try {
4719
+ const html = source.config.toHtml(source.document, { resolveHtmlTemplates: true, variables: source.variables });
4720
+ dispatch({ type: "generated", snapshot: { id: ++revision.current, source, html, width: viewportWidth(source.device, source.document.body), status: "loading" }, force });
4721
+ } catch (error) {
4722
+ dispatch({ type: "failed", failure: { source, message: error instanceof Error ? error.message : String(error) || "Email preview failed." } });
4723
+ }
4724
+ };
4725
+ const delay = immediate ? 0 : Math.min(PREVIEW_DEBOUNCE_MS, remaining);
4726
+ const timer = setTimeout(() => {
4727
+ if (immediate || typeof requestIdleCallback !== "function") generate();
4728
+ else idle = requestIdleCallback(generate, { timeout: Math.max(1, remaining - delay) });
4729
+ }, delay);
4730
+ return () => {
4731
+ cancelled = true;
4732
+ clearTimeout(timer);
4733
+ if (idle !== void 0) cancelIdleCallback(idle);
4734
+ };
4735
+ }, [source, requested, request]);
4736
+ const markReady = useCallback((id) => {
4737
+ dispatch({ type: "ready", id, source: sourceRef.current });
4738
+ }, []);
4739
+ const markFailed = useCallback((id, message) => {
4740
+ dispatch({ type: "failed", id, failure: { source: sourceRef.current, message } });
4741
+ }, []);
4742
+ const retry = useCallback(() => setRequest((value) => __spreadProps(__spreadValues({}, value), { key: value.key + 1, force: true })), []);
4743
+ const front = state.front === null ? null : state.frames[state.front];
4744
+ const failure = ((_a = state.failure) == null ? void 0 : _a.source) === source ? state.failure : null;
4745
+ const displayFailure = failure != null ? failure : state.front === null ? state.failure : null;
4746
+ return __spreadProps(__spreadValues({}, state), { failure, displayFailure, ready: (front == null ? void 0 : front.source) === source, markReady, markFailed, flush, retry });
4747
+ }
4748
+
4749
+ // src/stage/canvas/Canvas.tsx
4750
+ import { useDroppable } from "@dnd-kit/core";
4751
+ import {
4752
+ SortableContext,
4753
+ verticalListSortingStrategy
4754
+ } from "@dnd-kit/sortable";
4755
+
4614
4756
  // src/stage/canvas/CanvasBlock.tsx
4615
- import { useId as useId13, useMemo as useMemo3, useState as useState16 } from "react";
4757
+ import { useId as useId13, useMemo as useMemo3, useState as useState17 } from "react";
4616
4758
  import { useSortable } from "@dnd-kit/sortable";
4617
4759
  import { CSS } from "@dnd-kit/utilities";
4618
4760
 
4619
4761
  // src/stage/canvas/components/PluginCanvasContent.tsx
4620
4762
  import { useMemo as useMemo2 } from "react";
4621
4763
 
4622
- // src/render/template-preview.ts
4623
- var TOKEN_PATTERN = /\{\{-?([\s\S]*?)-?\}\}/g;
4764
+ // src/render/default-template-engine.ts
4765
+ var TOKEN_PATTERN = /\{\{(-[ \t\r\n])?([\s\S]*?)([ \t\r\n]-)?\}\}/g;
4766
+ var UNSUPPORTED_IDENTIFIERS = /* @__PURE__ */ new Set([
4767
+ "and",
4768
+ "or",
4769
+ "not",
4770
+ "len",
4771
+ "index",
4772
+ "slice",
4773
+ "call",
4774
+ "print",
4775
+ "printf",
4776
+ "println",
4777
+ "html",
4778
+ "js",
4779
+ "urlquery",
4780
+ "eq",
4781
+ "ne",
4782
+ "lt",
4783
+ "le",
4784
+ "gt",
4785
+ "ge",
4786
+ "break",
4787
+ "continue",
4788
+ "else",
4789
+ "end"
4790
+ ]);
4791
+ var PATH_PATTERN = /^(?:\.|\$|(?:\$\.|\.)?[\p{L}_][\p{L}\p{Nd}_]*(?:\.[\p{L}_][\p{L}\p{Nd}_]*)*)$/u;
4792
+ function supportsExpression(expression) {
4793
+ if (PATH_PATTERN.test(expression) && !UNSUPPORTED_IDENTIFIERS.has(expression)) return true;
4794
+ if (/^-?\d+(?:\.\d+)?$/.test(expression)) return true;
4795
+ if (/^"(?:[^"\\]|\\.)*"$/.test(expression)) {
4796
+ try {
4797
+ JSON.parse(expression);
4798
+ return true;
4799
+ } catch (e) {
4800
+ return false;
4801
+ }
4802
+ }
4803
+ return false;
4804
+ }
4805
+ function supportsTokens(tokens) {
4806
+ var _a;
4807
+ const branches = [];
4808
+ for (const token of tokens) {
4809
+ if (token.text !== void 0) {
4810
+ if (token.text.includes("{{")) return false;
4811
+ continue;
4812
+ }
4813
+ const tag = (_a = token.tag) != null ? _a : "";
4814
+ const branch = /^(if|range|with)\s+([\s\S]+)$/.exec(tag);
4815
+ if (branch) {
4816
+ if (!supportsExpression(branch[2].trim())) return false;
4817
+ branches.push({ kind: branch[1], hasElse: false });
4818
+ } else if (tag === "end") {
4819
+ if (!branches.pop()) return false;
4820
+ } else if (tag === "else" || /^else\s+if\s+/.test(tag)) {
4821
+ const current = branches.at(-1);
4822
+ if (!current || current.hasElse) return false;
4823
+ if (tag === "else") current.hasElse = true;
4824
+ else if (current.kind !== "if" || !supportsExpression(tag.replace(/^else\s+if\s+/, ""))) return false;
4825
+ } else if (!supportsExpression(tag)) return false;
4826
+ }
4827
+ return branches.length === 0;
4828
+ }
4624
4829
  function tokenize(template) {
4625
4830
  var _a;
4626
4831
  const tokens = [];
4627
4832
  let lastIndex = 0;
4833
+ let trimNext = false;
4628
4834
  for (const match of template.matchAll(TOKEN_PATTERN)) {
4629
4835
  const index = (_a = match.index) != null ? _a : 0;
4630
- if (index > lastIndex) tokens.push({ text: template.slice(lastIndex, index) });
4631
- tokens.push({ tag: match[1].trim() });
4836
+ let text = template.slice(lastIndex, index);
4837
+ if (trimNext) text = text.replace(/^[ \t\r\n]+/, "");
4838
+ if (match[1]) text = text.replace(/[ \t\r\n]+$/, "");
4839
+ if (text) tokens.push({ text });
4840
+ tokens.push({ tag: match[2].trim() });
4841
+ trimNext = Boolean(match[3]);
4632
4842
  lastIndex = index + match[0].length;
4633
4843
  }
4634
- if (lastIndex < template.length) tokens.push({ text: template.slice(lastIndex) });
4844
+ const tail = template.slice(lastIndex);
4845
+ if (tail) tokens.push({ text: trimNext ? tail.replace(/^[ \t\r\n]+/, "") : tail });
4635
4846
  return tokens;
4636
4847
  }
4637
4848
  function parseNodes(tokens, startIndex) {
@@ -4646,11 +4857,11 @@ function parseNodes(tokens, startIndex) {
4646
4857
  continue;
4647
4858
  }
4648
4859
  const tag = (_a = token.tag) != null ? _a : "";
4649
- if (tag === "else" || tag.startsWith("else if ")) {
4860
+ if (tag === "else" || /^else\s+if\s+/.test(tag)) {
4650
4861
  return { nodes, nextIndex: index, stopTag: "else" };
4651
4862
  }
4652
4863
  if (tag === "end") return { nodes, nextIndex: index, stopTag: "end" };
4653
- const branchMatch = /^(if|range|with)\s+(.+)$/.exec(tag);
4864
+ const branchMatch = /^(if|range|with)\s+([\s\S]+)$/.exec(tag);
4654
4865
  if (branchMatch) {
4655
4866
  const branch = parseBranch(
4656
4867
  tokens,
@@ -4674,7 +4885,7 @@ function parseBranch(tokens, startIndex, kind, expression) {
4674
4885
  let nextIndex = body.nextIndex;
4675
4886
  if (body.stopTag === "else") {
4676
4887
  const elseTag = (_b = (_a = tokens[body.nextIndex]) == null ? void 0 : _a.tag) != null ? _b : "";
4677
- const elseIfMatch = /^else\s+if\s+(.+)$/.exec(elseTag);
4888
+ const elseIfMatch = /^else\s+if\s+([\s\S]+)$/.exec(elseTag);
4678
4889
  if (elseIfMatch) {
4679
4890
  const nested = parseBranch(
4680
4891
  tokens,
@@ -4698,42 +4909,44 @@ function parseBranch(tokens, startIndex, kind, expression) {
4698
4909
  function readPath(value, path) {
4699
4910
  if (path === "") return value;
4700
4911
  return path.split(".").reduce((current, key) => {
4701
- if (current && typeof current === "object") return current[key];
4912
+ if (current && typeof current === "object" && Object.hasOwn(current, key)) return current[key];
4702
4913
  return void 0;
4703
4914
  }, value);
4704
4915
  }
4705
4916
  function resolveExpression(expression, root, current) {
4706
4917
  const trimmed = expression.trim();
4707
4918
  if (trimmed === ".") return current;
4919
+ if (trimmed === "$") return root;
4708
4920
  if (trimmed === "true") return true;
4709
4921
  if (trimmed === "false") return false;
4710
4922
  if (trimmed === "nil") return null;
4711
- if (/^\d+(?:\.\d+)?$/.test(trimmed)) return Number(trimmed);
4712
- if (trimmed.startsWith('"') && trimmed.endsWith('"') || trimmed.startsWith("'") && trimmed.endsWith("'")) {
4713
- return trimmed.slice(1, -1);
4714
- }
4923
+ if (/^-?\d+(?:\.\d+)?$/.test(trimmed)) return Number(trimmed);
4924
+ if (trimmed.startsWith('"')) return JSON.parse(trimmed);
4715
4925
  if (trimmed.startsWith("$.")) return readPath(root, trimmed.slice(2));
4716
4926
  if (trimmed.startsWith(".")) return readPath(current, trimmed.slice(1));
4717
4927
  return readPath(current, trimmed);
4718
4928
  }
4719
4929
  function isTruthy(value) {
4720
4930
  if (value == null || value === false || value === "" || value === 0) return false;
4721
- return !Array.isArray(value) || value.length > 0;
4722
- }
4723
- function escapeHtml(value) {
4724
- return value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;").replaceAll("'", "&#39;");
4931
+ if (Array.isArray(value)) return value.length > 0;
4932
+ if (value instanceof Map || value instanceof Set) return value.size > 0;
4933
+ if (typeof value === "object") {
4934
+ const prototype = Object.getPrototypeOf(value);
4935
+ if (prototype === Object.prototype || prototype === null) return Object.keys(value).length > 0;
4936
+ }
4937
+ return true;
4725
4938
  }
4726
4939
  function renderNodes(nodes, root, current) {
4727
4940
  return nodes.map((node) => {
4728
4941
  if (node.kind === "text") return node.value;
4729
4942
  if (node.kind === "expression") {
4730
4943
  const value2 = resolveExpression(node.expression, root, current);
4731
- return value2 == null ? "" : escapeHtml(String(value2));
4944
+ return value2 == null ? "" : esc(String(value2)).replaceAll("'", "&#39;");
4732
4945
  }
4733
4946
  const value = resolveExpression(node.expression, root, current);
4734
4947
  if (node.kind === "range") {
4735
- const items = Array.isArray(value) ? value : value && typeof value === "object" ? Object.values(value) : [];
4736
- return items.map((item) => renderNodes(node.children, root, item)).join("");
4948
+ const items = Array.isArray(value) ? value : value && typeof value === "object" ? Object.keys(value).sort().map((key) => value[key]) : [];
4949
+ return items.length > 0 ? items.map((item) => renderNodes(node.children, root, item)).join("") : renderNodes(node.alternate, root, current);
4737
4950
  }
4738
4951
  if (!isTruthy(value)) {
4739
4952
  return renderNodes(node.alternate, root, current);
@@ -4742,9 +4955,40 @@ function renderNodes(nodes, root, current) {
4742
4955
  return renderNodes(node.children, root, childContext);
4743
4956
  }).join("");
4744
4957
  }
4745
- function renderTemplatePreview(html, defaultData) {
4746
- if (defaultData === void 0) return html;
4747
- return renderNodes(parseNodes(tokenize(html), 0).nodes, defaultData, defaultData);
4958
+ var defaultTemplateEngine = Object.freeze({
4959
+ render(template, data) {
4960
+ const tokens = tokenize(template);
4961
+ if (!supportsTokens(tokens)) return template;
4962
+ return renderNodes(parseNodes(tokens, 0).nodes, data, data);
4963
+ }
4964
+ });
4965
+
4966
+ // src/render/template-preview.ts
4967
+ function normalizeEngineError(error) {
4968
+ return error instanceof Error ? error : new Error(String(error) || "Template engine failed.", { cause: error });
4969
+ }
4970
+ function renderTemplatePreview(template, data, options = {}) {
4971
+ var _a;
4972
+ const engine = (_a = options.templateEngine) != null ? _a : defaultTemplateEngine;
4973
+ if (data === void 0 && engine === defaultTemplateEngine) return template;
4974
+ try {
4975
+ const html = engine.render(template, structuredClone(data != null ? data : {}));
4976
+ if (typeof html !== "string") throw new TypeError("TemplateEngine.render must return a string synchronously.");
4977
+ return html;
4978
+ } catch (error) {
4979
+ throw normalizeEngineError(error);
4980
+ }
4981
+ }
4982
+ function validateTemplate(template, options = {}) {
4983
+ var _a, _b;
4984
+ const engine = (_a = options.templateEngine) != null ? _a : defaultTemplateEngine;
4985
+ try {
4986
+ const issues = (_b = engine.validate) == null ? void 0 : _b.call(engine, template);
4987
+ if (issues !== void 0 && !Array.isArray(issues)) throw new TypeError("TemplateEngine.validate must return an array synchronously.");
4988
+ return issues;
4989
+ } catch (error) {
4990
+ throw normalizeEngineError(error);
4991
+ }
4748
4992
  }
4749
4993
 
4750
4994
  // src/render/block-html.ts
@@ -4769,7 +5013,7 @@ function variableControlName(name) {
4769
5013
  const token = /^\{\{-?\s*([\s\S]*?)\s*-?\}\}$/.exec(name.trim());
4770
5014
  const inner = (token ? token[1] : name).trim();
4771
5015
  const control = inner.startsWith(".") ? inner.slice(1) : inner;
4772
- if (!control || !/^[A-Za-z_][\w.]*$/.test(control)) return null;
5016
+ if (!control || !/^[\p{L}_][\p{L}\p{Nd}_]*(?:\.[\p{L}_][\p{L}\p{Nd}_]*)*$/u.test(control)) return null;
4773
5017
  return control;
4774
5018
  }
4775
5019
  function dottedListedToken(name) {
@@ -4835,12 +5079,12 @@ function renderBlockContent(node, definitions = DEFAULT_BLOCKS, mode = "email",
4835
5079
  }
4836
5080
  function resolveBlockContent(rendered, node, options) {
4837
5081
  var _a, _b;
4838
- if (!options.resolveHtmlTemplate) {
4839
- return applyListedVariableDots(rendered, (_a = options.variables) != null ? _a : []);
4840
- }
5082
+ const normalized = applyListedVariableDots(rendered, (_a = options.variables) != null ? _a : []);
5083
+ if (!options.resolveHtmlTemplate) return normalized;
4841
5084
  return renderTemplatePreview(
4842
- rendered,
4843
- (_b = options.templateData) != null ? _b : node.defaultData
5085
+ normalized,
5086
+ (_b = options.templateData) != null ? _b : node.defaultData,
5087
+ options
4844
5088
  );
4845
5089
  }
4846
5090
  function renderBlock(node, definitions = DEFAULT_BLOCKS, mode = "email", body = DEFAULT_EMAIL_BODY) {
@@ -4866,7 +5110,7 @@ function blockToHtml(node, definitions = DEFAULT_BLOCKS, mode = "email", options
4866
5110
  }
4867
5111
 
4868
5112
  // src/stage/canvas/components/HtmlCanvas.tsx
4869
- import { useCallback, useEffect as useEffect6, useRef as useRef13 } from "react";
5113
+ import { useCallback as useCallback2, useEffect as useEffect7, useRef as useRef14 } from "react";
4870
5114
 
4871
5115
  // src/render/head-styles.ts
4872
5116
  function extractHeadStyles(html) {
@@ -4887,10 +5131,10 @@ function frameHtml(html, baseCss) {
4887
5131
  }
4888
5132
  function HtmlCanvas({ html, baseCss = "", onSelect }) {
4889
5133
  const hasHtmlTag = /<\/?[a-z][\w:-]*(?:\s[^>]*)?>/i.test(html);
4890
- const containerRef = useRef13(null);
4891
- const frameRef = useRef13(null);
4892
- const cleanupRef = useRef13();
4893
- const observeContent = useCallback(() => {
5134
+ const containerRef = useRef14(null);
5135
+ const frameRef = useRef14(null);
5136
+ const cleanupRef = useRef14();
5137
+ const observeContent = useCallback2(() => {
4894
5138
  var _a;
4895
5139
  (_a = cleanupRef.current) == null ? void 0 : _a.call(cleanupRef);
4896
5140
  const container = containerRef.current;
@@ -4931,7 +5175,7 @@ function HtmlCanvas({ html, baseCss = "", onSelect }) {
4931
5175
  window2.cancelAnimationFrame(animationFrame);
4932
5176
  };
4933
5177
  }, []);
4934
- useEffect6(() => () => {
5178
+ useEffect7(() => () => {
4935
5179
  var _a;
4936
5180
  return (_a = cleanupRef.current) == null ? void 0 : _a.call(cleanupRef);
4937
5181
  }, [hasHtmlTag, html]);
@@ -4982,15 +5226,17 @@ function HtmlCanvas({ html, baseCss = "", onSelect }) {
4982
5226
 
4983
5227
  // src/stage/canvas/components/PluginCanvasContent.tsx
4984
5228
  import { jsx as jsx49, jsxs as jsxs38 } from "react/jsx-runtime";
4985
- function PluginCanvasContent({ block, body, definition, variables, selected, toolbarId, toolbarCollapsed, select, onCommitControl, onCommitControls, onCommitStyle }) {
5229
+ function PluginCanvasContent({ block, body, definition, variables, templateEngine, selected, toolbarId, toolbarCollapsed, select, onCommitControl, onCommitControls, onCommitStyle }) {
4986
5230
  var _a, _b;
4987
5231
  const variableSnapshot = useMemo2(() => structuredClone(variables), [variables]);
4988
5232
  const context = useMemo2(() => definition ? buildRenderContext(block, "canvas", body, definition) : null, [block, body, definition]);
4989
5233
  const renderedHtml = useMemo2(() => definition && context ? renderBlockContentFromContext(context, definition, {
4990
5234
  resolveHtmlTemplate: true,
5235
+ templateEngine,
5236
+ variables,
4991
5237
  templateData: mergeTemplateData(variables, block.defaultData),
4992
5238
  body
4993
- }) : "", [block.defaultData, body, context, definition, variables]);
5239
+ }) : "", [block.defaultData, body, context, definition, variables, templateEngine]);
4994
5240
  if (!definition || !context) return /* @__PURE__ */ jsxs38("div", { className: "ree-plugin-error", role: "alert", children: [
4995
5241
  "Missing block plugin: ",
4996
5242
  block.type,
@@ -5101,12 +5347,14 @@ function canvasBlockStyle(block, body) {
5101
5347
  // src/stage/canvas/CanvasBlock.tsx
5102
5348
  import { jsx as jsx51, jsxs as jsxs40 } from "react/jsx-runtime";
5103
5349
  function CanvasBlock({
5350
+ active = true,
5104
5351
  block: sourceBlock,
5105
5352
  body,
5106
5353
  definition,
5107
5354
  selected,
5108
5355
  dropEdge,
5109
5356
  variables,
5357
+ templateEngine,
5110
5358
  onSelect,
5111
5359
  onCommitControl,
5112
5360
  onCommitControls,
@@ -5116,10 +5364,11 @@ function CanvasBlock({
5116
5364
  }) {
5117
5365
  var _a, _b, _c;
5118
5366
  const block = useMemo3(() => __spreadProps(__spreadValues({}, sourceBlock), { style: resolveBlockStyle(sourceBlock.style, definition) }), [sourceBlock, definition]);
5367
+ const renderKey = useMemo3(() => ({ definition, templateEngine, variables }), [definition, templateEngine, variables]);
5119
5368
  const toolbarId = useId13();
5120
- const [toolbarCollapsed, setToolbarCollapsed] = useState16(false);
5369
+ const [toolbarCollapsed, setToolbarCollapsed] = useState17(false);
5121
5370
  const dragData = { kind: "canvas" };
5122
- const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({ id: block.id, data: dragData });
5371
+ const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({ id: block.id, data: dragData, disabled: !active });
5123
5372
  const select = (event) => {
5124
5373
  event == null ? void 0 : event.stopPropagation();
5125
5374
  onSelect();
@@ -5177,7 +5426,7 @@ function CanvasBlock({
5177
5426
  {
5178
5427
  className: `ree-block-content${(definition == null ? void 0 : definition.inlineControl) ? " is-inline" : ""}`,
5179
5428
  style: canvasBlockStyle(block, body),
5180
- children: /* @__PURE__ */ jsx51(BlockErrorBoundary, { identity: sourceBlock, resetKey: definition, children: /* @__PURE__ */ jsx51(PluginCanvasContent, { block, body, definition, variables, selected, toolbarId, toolbarCollapsed, select: onSelect, onCommitControl, onCommitControls, onCommitStyle }) })
5429
+ children: /* @__PURE__ */ jsx51(BlockErrorBoundary, { identity: sourceBlock, resetKey: renderKey, children: /* @__PURE__ */ jsx51(PluginCanvasContent, { block, body, definition, variables, templateEngine, selected, toolbarId, toolbarCollapsed, select: onSelect, onCommitControl, onCommitControls, onCommitStyle }) })
5181
5430
  }
5182
5431
  ),
5183
5432
  definition ? /* @__PURE__ */ jsx51("span", { className: "ree-block-badge", children: definition.title }) : null
@@ -5189,6 +5438,7 @@ function CanvasBlock({
5189
5438
  // src/stage/canvas/Canvas.tsx
5190
5439
  import { jsx as jsx52 } from "react/jsx-runtime";
5191
5440
  function Canvas({
5441
+ active = true,
5192
5442
  blocks,
5193
5443
  body,
5194
5444
  definitions,
@@ -5196,6 +5446,7 @@ function Canvas({
5196
5446
  dropTargetId,
5197
5447
  dropEdge,
5198
5448
  variables,
5449
+ templateEngine,
5199
5450
  device,
5200
5451
  onSelect,
5201
5452
  onCommitControl,
@@ -5206,6 +5457,7 @@ function Canvas({
5206
5457
  }) {
5207
5458
  const { setNodeRef, isOver } = useDroppable({
5208
5459
  id: CANVAS_DROP_ID,
5460
+ disabled: !active,
5209
5461
  data: { kind: "canvas" }
5210
5462
  });
5211
5463
  return /* @__PURE__ */ jsx52(
@@ -5220,9 +5472,9 @@ function Canvas({
5220
5472
  if (!ownerWindow || !(target instanceof ownerWindow.Element)) return;
5221
5473
  if (target.closest(".ree-block")) return;
5222
5474
  onSelect(null);
5223
- const active = ownerDocument.activeElement;
5224
- if (active instanceof ownerWindow.HTMLElement && active.closest(".ree-rte")) {
5225
- active.blur();
5475
+ const active2 = ownerDocument.activeElement;
5476
+ if (active2 instanceof ownerWindow.HTMLElement && active2.closest(".ree-rte")) {
5477
+ active2.blur();
5226
5478
  }
5227
5479
  },
5228
5480
  children: /* @__PURE__ */ jsx52(
@@ -5257,11 +5509,13 @@ function Canvas({
5257
5509
  CanvasBlock,
5258
5510
  {
5259
5511
  block,
5512
+ active,
5260
5513
  body,
5261
5514
  definition: getBlockDefinition(block.type, definitions),
5262
5515
  selected: block.id === selectedId,
5263
5516
  dropEdge: dropTargetId === block.id ? dropEdge : null,
5264
5517
  variables,
5518
+ templateEngine,
5265
5519
  onSelect: () => onSelect(block.id),
5266
5520
  onCommitControl,
5267
5521
  onCommitControls,
@@ -5284,135 +5538,134 @@ function Canvas({
5284
5538
  }
5285
5539
 
5286
5540
  // src/stage/preview/Preview.tsx
5287
- import { useCallback as useCallback2, useEffect as useEffect7, useMemo as useMemo4, useRef as useRef14 } from "react";
5288
-
5289
- // src/render/document-html.ts
5290
- function documentToHtml(document, definitions = DEFAULT_BLOCKS, options = {}) {
5291
- const source = ensureEmailDocument(document);
5292
- const body = source.body;
5293
- const baseCss = emailBaseCss(body);
5294
- const rendered = source.blocks.map((node, index) => {
5295
- var _a;
5296
- const blockData = __spreadValues(__spreadValues({}, options.templateData), node.defaultData);
5297
- const block = blockToHtml(node, definitions, "email", {
5298
- resolveHtmlTemplate: options.resolveHtmlTemplates,
5299
- body,
5300
- variables: options.variables,
5301
- templateData: mergeTemplateData(
5302
- (_a = options.variables) != null ? _a : [],
5303
- Object.keys(blockData).length > 0 ? blockData : void 0
5304
- )
5305
- });
5306
- const gap = index < source.blocks.length - 1 ? spacerHtml(body.blockGap) : "";
5307
- return block + gap;
5308
- }).join("\n");
5309
- const { html: content, styles } = extractHeadStyles(rendered);
5310
- return [
5311
- "<!DOCTYPE html>",
5312
- '<html lang="en">',
5313
- "<head>",
5314
- '<meta charset="utf-8" />',
5315
- '<meta name="viewport" content="width=device-width, initial-scale=1" />',
5316
- "<title>Email</title>",
5317
- styles,
5318
- "</head>",
5319
- `<body style="margin:0;padding:0;${baseCss};background-color:#ffffff;word-spacing:normal;">`,
5320
- `<table role="presentation" width="100%" cellpadding="0" cellspacing="0" border="0" style="${baseCss};background-color:#ffffff;table-layout:fixed;">`,
5321
- "<tr>",
5322
- `<td align="center" style="padding:0;${baseCss};">`,
5323
- `<table role="presentation" align="center" width="${body.contentWidth}" cellpadding="0" cellspacing="0" border="0" style="width:100%;max-width:${body.contentWidth}px;margin-left:auto;margin-right:auto;table-layout:fixed;">`,
5324
- "<tr>",
5325
- `<td style="box-sizing:border-box;${styleToCss(surfaceStyle(body))};">`,
5326
- content,
5327
- "</td>",
5328
- "</tr>",
5329
- "</table>",
5330
- "</td>",
5331
- "</tr>",
5332
- "</table>",
5333
- "</body>",
5334
- "</html>"
5335
- ].join("\n");
5336
- }
5541
+ import { useRef as useRef16 } from "react";
5337
5542
 
5338
- // src/stage/preview/Preview.tsx
5543
+ // src/stage/preview/PreviewFrame.tsx
5544
+ import { useLayoutEffect as useLayoutEffect10, useRef as useRef15 } from "react";
5339
5545
  import { jsx as jsx53 } from "react/jsx-runtime";
5340
- function Preview({ document, definitions, variables, device }) {
5341
- const frameRef = useRef14(null);
5342
- const cleanupRef = useRef14();
5343
- const html = useMemo4(
5344
- () => documentToHtml(document, definitions, {
5345
- resolveHtmlTemplates: true,
5346
- variables
5347
- }),
5348
- [document, definitions, variables]
5349
- );
5350
- const observeContent = useCallback2(() => {
5351
- var _a;
5352
- (_a = cleanupRef.current) == null ? void 0 : _a.call(cleanupRef);
5546
+ function PreviewFrame({ snapshot, onReady, onFailed }) {
5547
+ const frameRef = useRef15(null);
5548
+ const { id, html } = snapshot;
5549
+ useLayoutEffect10(() => {
5353
5550
  const frame = frameRef.current;
5354
- const frameDocument = frame == null ? void 0 : frame.contentDocument;
5355
- const frameWindow = frameDocument == null ? void 0 : frameDocument.defaultView;
5356
- if (!frame || !(frameDocument == null ? void 0 : frameDocument.body) || !frameWindow) return;
5357
- let animationFrame = 0;
5358
- const fit = () => {
5359
- frame.style.height = `${frameDocument.body.getBoundingClientRect().height}px`;
5551
+ const ownerWindow = frame == null ? void 0 : frame.ownerDocument.defaultView;
5552
+ if (!frame || !ownerWindow) return;
5553
+ let status = "loading";
5554
+ let stopObserving;
5555
+ const fail = (message) => {
5556
+ if (status !== "loading") return;
5557
+ status = "failed";
5558
+ clearTimeout(timer);
5559
+ stopObserving == null ? void 0 : stopObserving();
5560
+ stopObserving = void 0;
5561
+ onFailed(id, message);
5360
5562
  };
5361
- const scheduleFit = () => {
5362
- frameWindow.cancelAnimationFrame(animationFrame);
5363
- animationFrame = frameWindow.requestAnimationFrame(fit);
5563
+ const timer = setTimeout(() => fail("Email preview timed out. Please retry."), PREVIEW_LOAD_TIMEOUT_MS);
5564
+ const observeContent = () => {
5565
+ if (status !== "loading") return;
5566
+ stopObserving == null ? void 0 : stopObserving();
5567
+ stopObserving = void 0;
5568
+ try {
5569
+ const frameDocument = frame.contentDocument;
5570
+ const frameWindow = frameDocument == null ? void 0 : frameDocument.defaultView;
5571
+ if (!(frameDocument == null ? void 0 : frameDocument.body) || !frameWindow) throw new Error("Email preview could not be measured. Please retry.");
5572
+ let animationFrame = 0;
5573
+ let readyFrame = 0;
5574
+ const fit = () => {
5575
+ try {
5576
+ const height = frameDocument.body.getBoundingClientRect().height;
5577
+ if (!Number.isFinite(height) || height < 0) throw new Error("Email preview returned an invalid height. Please retry.");
5578
+ frame.style.height = `${Math.max(1, height)}px`;
5579
+ return true;
5580
+ } catch (error) {
5581
+ fail(error instanceof Error ? error.message : "Email preview could not be measured. Please retry.");
5582
+ return false;
5583
+ }
5584
+ };
5585
+ const scheduleFit = () => {
5586
+ ownerWindow.cancelAnimationFrame(animationFrame);
5587
+ animationFrame = ownerWindow.requestAnimationFrame(fit);
5588
+ };
5589
+ const observer = new frameWindow.ResizeObserver(scheduleFit);
5590
+ stopObserving = () => {
5591
+ observer.disconnect();
5592
+ frameDocument.removeEventListener("load", scheduleFit, true);
5593
+ ownerWindow.cancelAnimationFrame(animationFrame);
5594
+ ownerWindow.cancelAnimationFrame(readyFrame);
5595
+ };
5596
+ observer.observe(frameDocument.body);
5597
+ frameDocument.addEventListener("load", scheduleFit, true);
5598
+ if (!fit()) return;
5599
+ readyFrame = ownerWindow.requestAnimationFrame(() => {
5600
+ if (status !== "loading" || !fit()) return;
5601
+ status = "ready";
5602
+ clearTimeout(timer);
5603
+ onReady(id);
5604
+ });
5605
+ } catch (error) {
5606
+ fail(error instanceof Error ? error.message : "Email preview could not be loaded. Please retry.");
5607
+ }
5364
5608
  };
5365
- const observer = new frameWindow.ResizeObserver(scheduleFit);
5366
- observer.observe(frameDocument.body);
5367
- frameDocument.addEventListener("load", scheduleFit, true);
5368
- fit();
5369
- cleanupRef.current = () => {
5370
- observer.disconnect();
5371
- frameDocument.removeEventListener("load", scheduleFit, true);
5372
- frameWindow.cancelAnimationFrame(animationFrame);
5609
+ const loadError = () => fail("Email preview could not be loaded. Please retry.");
5610
+ frame.addEventListener("load", observeContent);
5611
+ frame.addEventListener("error", loadError);
5612
+ return () => {
5613
+ status = "disposed";
5614
+ clearTimeout(timer);
5615
+ stopObserving == null ? void 0 : stopObserving();
5616
+ frame.removeEventListener("load", observeContent);
5617
+ frame.removeEventListener("error", loadError);
5373
5618
  };
5374
- }, []);
5375
- useEffect7(() => () => {
5376
- var _a;
5377
- return (_a = cleanupRef.current) == null ? void 0 : _a.call(cleanupRef);
5378
- }, [html]);
5379
- return /* @__PURE__ */ jsx53("div", { className: "ree-preview-stage", children: /* @__PURE__ */ jsx53(
5380
- "div",
5381
- {
5382
- className: "ree-email-viewport",
5383
- style: {
5384
- width: viewportWidth(device, document.body),
5385
- marginLeft: "auto",
5386
- marginRight: "auto"
5387
- },
5388
- children: /* @__PURE__ */ jsx53(
5389
- "iframe",
5390
- {
5391
- ref: frameRef,
5392
- className: "ree-preview-frame",
5393
- title: "Email preview",
5394
- srcDoc: html,
5395
- onLoad: observeContent,
5396
- style: {
5397
- backgroundColor: "#ffffff"
5398
- }
5399
- }
5400
- )
5401
- }
5402
- ) });
5619
+ }, [id, onReady, onFailed]);
5620
+ return /* @__PURE__ */ jsx53("iframe", { ref: frameRef, className: "ree-preview-frame", title: "Email preview", sandbox: "allow-same-origin", srcDoc: html });
5403
5621
  }
5404
5622
 
5405
- // src/stage/StageHeader.tsx
5623
+ // src/stage/preview/Preview.tsx
5406
5624
  import { jsx as jsx54, jsxs as jsxs41 } from "react/jsx-runtime";
5625
+ function Preview({ frames, front, failure, onReady, onFailed, onRetry }) {
5626
+ const scrollRef = useRef16({ top: 0, left: 0 });
5627
+ return /* @__PURE__ */ jsxs41("div", { className: "ree-preview-buffer", children: [
5628
+ PREVIEW_BUFFER_SLOTS.map((index) => {
5629
+ const snapshot = frames[index];
5630
+ const visible = front === index;
5631
+ return /* @__PURE__ */ jsx54(
5632
+ "div",
5633
+ __spreadProps(__spreadValues({
5634
+ className: `ree-preview-stage${visible ? "" : " is-buffered"}`,
5635
+ "aria-hidden": !visible
5636
+ }, !visible ? { inert: "" } : {}), {
5637
+ ref: (node) => {
5638
+ if (node && visible) {
5639
+ node.scrollTop = scrollRef.current.top;
5640
+ node.scrollLeft = scrollRef.current.left;
5641
+ }
5642
+ },
5643
+ onScroll: (event) => {
5644
+ if (visible) scrollRef.current = { top: event.currentTarget.scrollTop, left: event.currentTarget.scrollLeft };
5645
+ },
5646
+ children: /* @__PURE__ */ jsx54("div", { className: "ree-email-viewport", style: { width: snapshot == null ? void 0 : snapshot.width, marginLeft: "auto", marginRight: "auto" }, children: snapshot ? /* @__PURE__ */ jsx54(PreviewFrame, { snapshot, onReady, onFailed }, snapshot.id) : null })
5647
+ }),
5648
+ index
5649
+ );
5650
+ }),
5651
+ failure ? /* @__PURE__ */ jsxs41("div", { className: "ree-plugin-error ree-preview-error", role: "alert", children: [
5652
+ /* @__PURE__ */ jsx54("p", { children: failure.message }),
5653
+ /* @__PURE__ */ jsx54(Button, { onClick: onRetry, children: "Retry" })
5654
+ ] }) : null
5655
+ ] });
5656
+ }
5657
+
5658
+ // src/stage/StageHeader.tsx
5659
+ import { jsx as jsx55, jsxs as jsxs42 } from "react/jsx-runtime";
5407
5660
  function StageHeader({
5408
5661
  viewMode,
5409
5662
  device,
5410
5663
  onViewModeChange,
5411
5664
  onDeviceChange
5412
5665
  }) {
5413
- return /* @__PURE__ */ jsxs41("div", { className: "ree-col-head ree-stage-head", children: [
5414
- /* @__PURE__ */ jsxs41("div", { className: "ree-segment", role: "group", "aria-label": "Editor mode", children: [
5415
- /* @__PURE__ */ jsx54(
5666
+ return /* @__PURE__ */ jsxs42("div", { className: "ree-col-head ree-stage-head", children: [
5667
+ /* @__PURE__ */ jsxs42("div", { className: "ree-segment", role: "group", "aria-label": "Editor mode", children: [
5668
+ /* @__PURE__ */ jsx55(
5416
5669
  "button",
5417
5670
  {
5418
5671
  type: "button",
@@ -5421,14 +5674,14 @@ function StageHeader({
5421
5674
  "aria-label": "Edit",
5422
5675
  title: "Edit",
5423
5676
  onClick: () => onViewModeChange("edit"),
5424
- children: /* @__PURE__ */ jsxs41("svg", { viewBox: "0 0 24 24", width: "20", height: "20", fill: "none", stroke: "currentColor", strokeWidth: "1.8", strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": "true", children: [
5425
- /* @__PURE__ */ jsx54("path", { d: "M12 20h9" }),
5426
- /* @__PURE__ */ jsx54("path", { d: "M16.5 3.5a2.1 2.1 0 0 1 3 3L8 18l-4 1 1-4 11.5-11.5Z" })
5677
+ children: /* @__PURE__ */ jsxs42("svg", { viewBox: "0 0 24 24", width: "20", height: "20", fill: "none", stroke: "currentColor", strokeWidth: "1.8", strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": "true", children: [
5678
+ /* @__PURE__ */ jsx55("path", { d: "M12 20h9" }),
5679
+ /* @__PURE__ */ jsx55("path", { d: "M16.5 3.5a2.1 2.1 0 0 1 3 3L8 18l-4 1 1-4 11.5-11.5Z" })
5427
5680
  ] })
5428
5681
  }
5429
5682
  ),
5430
- /* @__PURE__ */ jsx54("span", { className: "ree-segment-divider", "aria-hidden": "true" }),
5431
- /* @__PURE__ */ jsx54(
5683
+ /* @__PURE__ */ jsx55("span", { className: "ree-segment-divider", "aria-hidden": "true" }),
5684
+ /* @__PURE__ */ jsx55(
5432
5685
  "button",
5433
5686
  {
5434
5687
  type: "button",
@@ -5437,15 +5690,15 @@ function StageHeader({
5437
5690
  "aria-label": "Preview",
5438
5691
  title: "Preview",
5439
5692
  onClick: () => onViewModeChange("preview"),
5440
- children: /* @__PURE__ */ jsxs41("svg", { viewBox: "0 0 24 24", width: "20", height: "20", fill: "none", stroke: "currentColor", strokeWidth: "1.8", strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": "true", children: [
5441
- /* @__PURE__ */ jsx54("path", { d: "M2.5 12s3.5-6 9.5-6 9.5 6 9.5 6-3.5 6-9.5 6-9.5-6-9.5-6Z" }),
5442
- /* @__PURE__ */ jsx54("circle", { cx: "12", cy: "12", r: "2.5" })
5693
+ children: /* @__PURE__ */ jsxs42("svg", { viewBox: "0 0 24 24", width: "20", height: "20", fill: "none", stroke: "currentColor", strokeWidth: "1.8", strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": "true", children: [
5694
+ /* @__PURE__ */ jsx55("path", { d: "M2.5 12s3.5-6 9.5-6 9.5 6 9.5 6-3.5 6-9.5 6-9.5-6-9.5-6Z" }),
5695
+ /* @__PURE__ */ jsx55("circle", { cx: "12", cy: "12", r: "2.5" })
5443
5696
  ] })
5444
5697
  }
5445
5698
  )
5446
5699
  ] }),
5447
- /* @__PURE__ */ jsxs41("div", { className: "ree-segment", role: "group", "aria-label": "Canvas device", children: [
5448
- /* @__PURE__ */ jsx54(
5700
+ /* @__PURE__ */ jsxs42("div", { className: "ree-segment", role: "group", "aria-label": "Canvas device", children: [
5701
+ /* @__PURE__ */ jsx55(
5449
5702
  "button",
5450
5703
  {
5451
5704
  type: "button",
@@ -5454,15 +5707,15 @@ function StageHeader({
5454
5707
  "aria-label": "Desktop",
5455
5708
  title: "Desktop",
5456
5709
  onClick: () => onDeviceChange("desktop"),
5457
- children: /* @__PURE__ */ jsxs41("svg", { viewBox: "0 0 24 24", width: "20", height: "20", fill: "none", stroke: "currentColor", strokeWidth: "1.8", strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": "true", children: [
5458
- /* @__PURE__ */ jsx54("rect", { x: "3", y: "4", width: "18", height: "12", rx: "1.5" }),
5459
- /* @__PURE__ */ jsx54("path", { d: "M8 20h8" }),
5460
- /* @__PURE__ */ jsx54("path", { d: "M12 16v4" })
5710
+ children: /* @__PURE__ */ jsxs42("svg", { viewBox: "0 0 24 24", width: "20", height: "20", fill: "none", stroke: "currentColor", strokeWidth: "1.8", strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": "true", children: [
5711
+ /* @__PURE__ */ jsx55("rect", { x: "3", y: "4", width: "18", height: "12", rx: "1.5" }),
5712
+ /* @__PURE__ */ jsx55("path", { d: "M8 20h8" }),
5713
+ /* @__PURE__ */ jsx55("path", { d: "M12 16v4" })
5461
5714
  ] })
5462
5715
  }
5463
5716
  ),
5464
- /* @__PURE__ */ jsx54("span", { className: "ree-segment-divider", "aria-hidden": "true" }),
5465
- /* @__PURE__ */ jsx54(
5717
+ /* @__PURE__ */ jsx55("span", { className: "ree-segment-divider", "aria-hidden": "true" }),
5718
+ /* @__PURE__ */ jsx55(
5466
5719
  "button",
5467
5720
  {
5468
5721
  type: "button",
@@ -5471,9 +5724,9 @@ function StageHeader({
5471
5724
  "aria-label": "Mobile",
5472
5725
  title: "Mobile",
5473
5726
  onClick: () => onDeviceChange("mobile"),
5474
- children: /* @__PURE__ */ jsxs41("svg", { viewBox: "0 0 24 24", width: "20", height: "20", fill: "none", stroke: "currentColor", strokeWidth: "1.8", strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": "true", children: [
5475
- /* @__PURE__ */ jsx54("rect", { x: "7", y: "2.5", width: "10", height: "19", rx: "2" }),
5476
- /* @__PURE__ */ jsx54("path", { d: "M11 18.5h2" })
5727
+ children: /* @__PURE__ */ jsxs42("svg", { viewBox: "0 0 24 24", width: "20", height: "20", fill: "none", stroke: "currentColor", strokeWidth: "1.8", strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": "true", children: [
5728
+ /* @__PURE__ */ jsx55("rect", { x: "7", y: "2.5", width: "10", height: "19", rx: "2" }),
5729
+ /* @__PURE__ */ jsx55("path", { d: "M11 18.5h2" })
5477
5730
  ] })
5478
5731
  }
5479
5732
  )
@@ -5482,7 +5735,7 @@ function StageHeader({
5482
5735
  }
5483
5736
 
5484
5737
  // src/stage/Stage.tsx
5485
- import { jsx as jsx55, jsxs as jsxs42 } from "react/jsx-runtime";
5738
+ import { jsx as jsx56, jsxs as jsxs43 } from "react/jsx-runtime";
5486
5739
  function Stage({
5487
5740
  session,
5488
5741
  viewMode,
@@ -5491,62 +5744,70 @@ function Stage({
5491
5744
  onDeviceChange,
5492
5745
  onSelectBlock
5493
5746
  }) {
5747
+ const source = useMemo4(() => ({ document: session.document, config: session.config, variables: session.variables, device }), [session.document, session.config, session.variables, device]);
5748
+ const requested = viewMode === "preview";
5749
+ const preview = usePreviewBuffer(source, requested);
5750
+ const [previewVisible, setPreviewVisible] = useState18(false);
5751
+ if (!requested && previewVisible) setPreviewVisible(false);
5752
+ if (requested && !previewVisible && (preview.ready || preview.failure)) setPreviewVisible(true);
5753
+ const editing = !requested;
5494
5754
  const { targetId, dropEdge } = useEditorDragState();
5495
- return /* @__PURE__ */ jsxs42("div", { className: "ree-col ree-col-center", children: [
5496
- /* @__PURE__ */ jsx55(
5755
+ return /* @__PURE__ */ jsxs43("div", { className: "ree-col ree-col-center", children: [
5756
+ /* @__PURE__ */ jsx56(
5497
5757
  StageHeader,
5498
5758
  {
5499
5759
  viewMode,
5500
5760
  device,
5501
- onViewModeChange,
5761
+ onViewModeChange: (mode) => {
5762
+ if (mode === "preview") preview.flush();
5763
+ onViewModeChange(mode);
5764
+ },
5502
5765
  onDeviceChange
5503
5766
  }
5504
5767
  ),
5505
- viewMode === "edit" ? /* @__PURE__ */ jsx55("div", { className: "ree-stage-view", children: /* @__PURE__ */ jsx55(
5506
- Canvas,
5507
- {
5508
- blocks: session.document.blocks,
5509
- body: session.document.body,
5510
- definitions: session.definitions,
5511
- selectedId: session.selectedId,
5512
- dropTargetId: targetId,
5513
- dropEdge,
5514
- variables: session.variables,
5515
- device,
5516
- onSelect: onSelectBlock,
5517
- onCommitControl: session.updateControl,
5518
- onCommitControls: session.updateControls,
5519
- onCommitStyle: session.updateStyle,
5520
- onDuplicate: session.duplicateBlock,
5521
- onRemove: session.removeBlock
5522
- }
5523
- ) }) : /* @__PURE__ */ jsx55("div", { className: "ree-stage-view", children: /* @__PURE__ */ jsx55(BlockErrorBoundary, { identity: session.document, resetKey: session.definitions, children: /* @__PURE__ */ jsx55(
5524
- Preview,
5525
- {
5526
- document: session.document,
5527
- definitions: session.definitions,
5528
- variables: session.variables,
5529
- device
5530
- }
5531
- ) }) })
5768
+ /* @__PURE__ */ jsxs43("div", { className: "ree-stage-content", "aria-busy": requested && !preview.ready && !preview.failure, children: [
5769
+ /* @__PURE__ */ jsx56("div", __spreadProps(__spreadValues({ className: `ree-stage-view${previewVisible ? " is-buffered" : ""}`, "aria-hidden": previewVisible }, !editing ? { inert: "" } : {}), { children: /* @__PURE__ */ jsx56(
5770
+ Canvas,
5771
+ {
5772
+ blocks: session.document.blocks,
5773
+ body: session.document.body,
5774
+ definitions: session.definitions,
5775
+ active: editing,
5776
+ selectedId: editing ? session.selectedId : null,
5777
+ dropTargetId: targetId,
5778
+ dropEdge,
5779
+ variables: session.variables,
5780
+ templateEngine: session.config.templateEngine,
5781
+ device,
5782
+ onSelect: onSelectBlock,
5783
+ onCommitControl: session.updateControl,
5784
+ onCommitControls: session.updateControls,
5785
+ onCommitStyle: session.updateStyle,
5786
+ onDuplicate: session.duplicateBlock,
5787
+ onRemove: session.removeBlock
5788
+ }
5789
+ ) })),
5790
+ /* @__PURE__ */ jsx56("div", __spreadProps(__spreadValues({ className: `ree-stage-view${previewVisible ? "" : " is-buffered"}`, "aria-hidden": !previewVisible }, !previewVisible ? { inert: "" } : {}), { children: /* @__PURE__ */ jsx56(Preview, { frames: preview.frames, front: preview.front, failure: preview.displayFailure, onReady: preview.markReady, onFailed: preview.markFailed, onRetry: preview.retry }) })),
5791
+ requested && !preview.ready && !preview.failure ? /* @__PURE__ */ jsx56("div", { className: "ree-preview-pending", role: "status", children: "Preparing preview\u2026" }) : null
5792
+ ] })
5532
5793
  ] });
5533
5794
  }
5534
5795
 
5535
5796
  // src/shell/SubjectBar.tsx
5536
- import { useId as useId14, useRef as useRef15 } from "react";
5797
+ import { useId as useId14, useRef as useRef17 } from "react";
5537
5798
 
5538
5799
  // src/shell/constants.ts
5539
5800
  var EMPTY_VARIABLES = [];
5540
5801
  var PERSONALIZATION_HINT = "Personalization fields will appear blank if no data is available.";
5541
5802
 
5542
5803
  // src/shell/SubjectBar.tsx
5543
- import { Fragment as Fragment5, jsx as jsx56, jsxs as jsxs43 } from "react/jsx-runtime";
5804
+ import { Fragment as Fragment5, jsx as jsx57, jsxs as jsxs44 } from "react/jsx-runtime";
5544
5805
  function SubjectBar({
5545
5806
  subject,
5546
5807
  variables,
5547
5808
  onSubjectChange
5548
5809
  }) {
5549
- const inputRef = useRef15(null);
5810
+ const inputRef = useRef17(null);
5550
5811
  const subjectId = useId14();
5551
5812
  const countId = useId14();
5552
5813
  const insertVariable = (token) => {
@@ -5566,10 +5827,10 @@ function SubjectBar({
5566
5827
  (_b2 = inputRef.current) == null ? void 0 : _b2.setSelectionRange(cursor, cursor);
5567
5828
  });
5568
5829
  };
5569
- return /* @__PURE__ */ jsxs43("div", { className: "ree-subject-bar", children: [
5570
- /* @__PURE__ */ jsx56("label", { className: "ree-subject-label", htmlFor: subjectId, children: "Email subject" }),
5571
- /* @__PURE__ */ jsxs43("div", { className: "ree-subject-field", children: [
5572
- /* @__PURE__ */ jsx56(
5830
+ return /* @__PURE__ */ jsxs44("div", { className: "ree-subject-bar", children: [
5831
+ /* @__PURE__ */ jsx57("label", { className: "ree-subject-label", htmlFor: subjectId, children: "Email subject" }),
5832
+ /* @__PURE__ */ jsxs44("div", { className: "ree-subject-field", children: [
5833
+ /* @__PURE__ */ jsx57(
5573
5834
  TextInput,
5574
5835
  {
5575
5836
  id: subjectId,
@@ -5581,21 +5842,21 @@ function SubjectBar({
5581
5842
  onChange: (event) => onSubjectChange(event.target.value)
5582
5843
  }
5583
5844
  ),
5584
- /* @__PURE__ */ jsxs43("span", { id: countId, className: "ree-subject-count", children: [
5845
+ /* @__PURE__ */ jsxs44("span", { id: countId, className: "ree-subject-count", children: [
5585
5846
  subject.length,
5586
5847
  "/",
5587
5848
  SUBJECT_MAX_LENGTH
5588
5849
  ] })
5589
5850
  ] }),
5590
- variables.length > 0 ? /* @__PURE__ */ jsxs43(Fragment5, { children: [
5591
- /* @__PURE__ */ jsx56(PersonalizationMenu, { variables, onInsert: insertVariable }),
5592
- /* @__PURE__ */ jsx56("p", { className: "ree-subject-hint", children: PERSONALIZATION_HINT })
5851
+ variables.length > 0 ? /* @__PURE__ */ jsxs44(Fragment5, { children: [
5852
+ /* @__PURE__ */ jsx57(PersonalizationMenu, { variables, onInsert: insertVariable }),
5853
+ /* @__PURE__ */ jsx57("p", { className: "ree-subject-hint", children: PERSONALIZATION_HINT })
5593
5854
  ] }) : null
5594
5855
  ] });
5595
5856
  }
5596
5857
 
5597
5858
  // src/shell/useEditorSession.ts
5598
- import { useCallback as useCallback3, useState as useState17, useSyncExternalStore } from "react";
5859
+ import { useCallback as useCallback3, useState as useState19, useSyncExternalStore } from "react";
5599
5860
 
5600
5861
  // src/utils/id.ts
5601
5862
  function generateId(prefix = "block") {
@@ -5694,9 +5955,59 @@ function createBlockNode(type, definitions) {
5694
5955
  };
5695
5956
  }
5696
5957
 
5958
+ // src/render/document-html.ts
5959
+ function documentToHtml(document, definitions = DEFAULT_BLOCKS, options = {}) {
5960
+ const source = ensureEmailDocument(document);
5961
+ const body = source.body;
5962
+ const baseCss = emailBaseCss(body);
5963
+ const rendered = source.blocks.map((node, index) => {
5964
+ var _a;
5965
+ const blockData = __spreadValues(__spreadValues({}, options.templateData), node.defaultData);
5966
+ const block = blockToHtml(node, definitions, "email", {
5967
+ resolveHtmlTemplate: options.resolveHtmlTemplates,
5968
+ templateEngine: options.templateEngine,
5969
+ body,
5970
+ variables: options.variables,
5971
+ templateData: mergeTemplateData(
5972
+ (_a = options.variables) != null ? _a : [],
5973
+ Object.keys(blockData).length > 0 ? blockData : void 0
5974
+ )
5975
+ });
5976
+ const gap = index < source.blocks.length - 1 ? spacerHtml(body.blockGap) : "";
5977
+ return block + gap;
5978
+ }).join("\n");
5979
+ const { html: content, styles } = extractHeadStyles(rendered);
5980
+ return [
5981
+ "<!DOCTYPE html>",
5982
+ '<html lang="en">',
5983
+ "<head>",
5984
+ '<meta charset="utf-8" />',
5985
+ '<meta name="viewport" content="width=device-width, initial-scale=1" />',
5986
+ "<title>Email</title>",
5987
+ styles,
5988
+ "</head>",
5989
+ `<body style="margin:0;padding:0;${baseCss};background-color:#ffffff;word-spacing:normal;">`,
5990
+ `<table role="presentation" width="100%" cellpadding="0" cellspacing="0" border="0" style="${baseCss};background-color:#ffffff;table-layout:fixed;">`,
5991
+ "<tr>",
5992
+ `<td align="center" style="padding:0;${baseCss};">`,
5993
+ `<table role="presentation" align="center" width="${body.contentWidth}" cellpadding="0" cellspacing="0" border="0" style="width:100%;max-width:${body.contentWidth}px;margin-left:auto;margin-right:auto;table-layout:fixed;">`,
5994
+ "<tr>",
5995
+ `<td style="box-sizing:border-box;${styleToCss(surfaceStyle(body))};">`,
5996
+ content,
5997
+ "</td>",
5998
+ "</tr>",
5999
+ "</table>",
6000
+ "</td>",
6001
+ "</tr>",
6002
+ "</table>",
6003
+ "</body>",
6004
+ "</html>"
6005
+ ].join("\n");
6006
+ }
6007
+
5697
6008
  // src/plugins/config.ts
5698
6009
  function buildEmailEditorConfig(options = {}) {
5699
- var _a, _b, _c;
6010
+ var _a, _b, _c, _d;
5700
6011
  const definitions = new Map(DEFAULT_BLOCKS.map((block) => [block.type, block]));
5701
6012
  const customTypes = /* @__PURE__ */ new Set();
5702
6013
  for (const plugin of (_a = options.plugins) != null ? _a : []) {
@@ -5711,16 +6022,21 @@ function buildEmailEditorConfig(options = {}) {
5711
6022
  for (const type of visibleBuiltins) if (!builtinTypes.has(type)) throw new Error(`Unknown builtin block: ${type}`);
5712
6023
  const all = Object.freeze([...definitions.values()].map((plugin) => snapshotValue(plugin)));
5713
6024
  const registered = new Map(all.map((plugin) => [plugin.type, plugin]));
6025
+ const templateEngine = (_c = options.templateEngine) != null ? _c : defaultTemplateEngine;
5714
6026
  return Object.freeze({
6027
+ templateEngine,
5715
6028
  definitions: all,
5716
- library: Object.freeze([...visibleBuiltins.map((type) => registered.get(type)), ...((_c = options.plugins) != null ? _c : []).filter((plugin) => !builtinTypes.has(plugin.type)).map((plugin) => registered.get(plugin.type))]),
6029
+ library: Object.freeze([...visibleBuiltins.map((type) => registered.get(type)), ...((_d = options.plugins) != null ? _d : []).filter((plugin) => !builtinTypes.has(plugin.type)).map((plugin) => registered.get(plugin.type))]),
5717
6030
  pickImage: options.pickImage,
5718
6031
  createBlock: (type) => {
5719
6032
  const block = createBlockNode(type, all);
5720
6033
  if (!block) throw new Error(`Unknown block plugin: ${type}`);
5721
6034
  return block;
5722
6035
  },
5723
- toHtml: (document, htmlOptions) => documentToHtml(document, all, htmlOptions),
6036
+ toHtml: (document, htmlOptions) => {
6037
+ var _a2;
6038
+ return documentToHtml(document, all, __spreadProps(__spreadValues({}, htmlOptions), { templateEngine: (_a2 = htmlOptions == null ? void 0 : htmlOptions.templateEngine) != null ? _a2 : templateEngine }));
6039
+ },
5724
6040
  validate: (document) => validateDocument(document, all)
5725
6041
  });
5726
6042
  }
@@ -5855,8 +6171,8 @@ function useEditorSession(editor) {
5855
6171
  const store = getEditorStore(editor);
5856
6172
  const { document, config, variables, resetKey } = useSyncExternalStore(store.subscribe, store.getSnapshot, store.getSnapshot);
5857
6173
  const definitions = config.definitions;
5858
- const [selectedId, setSelectedId] = useState17(null);
5859
- const [previousReset, setPreviousReset] = useState17({ editor, resetKey });
6174
+ const [selectedId, setSelectedId] = useState19(null);
6175
+ const [previousReset, setPreviousReset] = useState19({ editor, resetKey });
5860
6176
  if (previousReset.editor !== editor || previousReset.resetKey !== resetKey) {
5861
6177
  setPreviousReset({ editor, resetKey });
5862
6178
  setSelectedId(null);
@@ -5957,21 +6273,21 @@ function useEditorSession(editor) {
5957
6273
  }
5958
6274
 
5959
6275
  // src/shell/EmailEditorShell.tsx
5960
- import { jsx as jsx57, jsxs as jsxs44 } from "react/jsx-runtime";
6276
+ import { jsx as jsx58, jsxs as jsxs45 } from "react/jsx-runtime";
5961
6277
  function EmailEditorShell({ editor, height = "100%" }) {
5962
6278
  var _a;
5963
6279
  const session = useEditorSession(editor);
5964
- const [activeRichText, setActiveRichText] = useState18(null);
5965
- const [viewMode, setViewMode] = useState18("edit");
5966
- const [device, setDevice] = useState18("desktop");
5967
- const [inspectorPanel, setInspectorPanel] = useState18("body");
5968
- const [previousReset, setPreviousReset] = useState18({ editor, key: session.resetKey });
6280
+ const [activeRichText, setActiveRichText] = useState20(null);
6281
+ const [viewMode, setViewMode] = useState20("edit");
6282
+ const [device, setDevice] = useState20("desktop");
6283
+ const [inspectorPanel, setInspectorPanel] = useState20("body");
6284
+ const [previousReset, setPreviousReset] = useState20({ editor, key: session.resetKey });
5969
6285
  if (previousReset.editor !== editor || previousReset.key !== session.resetKey) {
5970
6286
  setPreviousReset({ editor, key: session.resetKey });
5971
6287
  setActiveRichText(null);
5972
6288
  setInspectorPanel("body");
5973
6289
  }
5974
- const [previousSelectedId, setPreviousSelectedId] = useState18(session.selectedId);
6290
+ const [previousSelectedId, setPreviousSelectedId] = useState20(session.selectedId);
5975
6291
  if (previousSelectedId !== session.selectedId) {
5976
6292
  setPreviousSelectedId(session.selectedId);
5977
6293
  if (!session.selectedId) setInspectorPanel("body");
@@ -5995,8 +6311,8 @@ function EmailEditorShell({ editor, height = "100%" }) {
5995
6311
  selectBlock(null);
5996
6312
  };
5997
6313
  const showCanvas = () => setViewMode("edit");
5998
- return /* @__PURE__ */ jsx57(EditorConfigContext.Provider, { value: session.config, children: /* @__PURE__ */ jsx57(RichTextFocusContext.Provider, { value: { active: activeRichText, activate: setActiveRichText }, children: /* @__PURE__ */ jsxs44("div", { className: "ree-root", style: { height }, onKeyDown: handleKeyDown, children: [
5999
- /* @__PURE__ */ jsx57(
6314
+ return /* @__PURE__ */ jsx58(EditorConfigContext.Provider, { value: session.config, children: /* @__PURE__ */ jsx58(RichTextFocusContext.Provider, { value: { active: activeRichText, activate: setActiveRichText }, children: /* @__PURE__ */ jsxs45("div", { className: "ree-root", style: { height }, onKeyDown: handleKeyDown, children: [
6315
+ /* @__PURE__ */ jsx58(
6000
6316
  SubjectBar,
6001
6317
  {
6002
6318
  subject: (_a = session.document.subject) != null ? _a : "",
@@ -6004,8 +6320,8 @@ function EmailEditorShell({ editor, height = "100%" }) {
6004
6320
  onSubjectChange: session.setSubject
6005
6321
  }
6006
6322
  ),
6007
- /* @__PURE__ */ jsxs44("div", { className: "ree-body", children: [
6008
- /* @__PURE__ */ jsxs44(
6323
+ /* @__PURE__ */ jsxs45("div", { className: "ree-body", children: [
6324
+ /* @__PURE__ */ jsxs45(
6009
6325
  EditorDnd,
6010
6326
  {
6011
6327
  blocks: session.document.blocks,
@@ -6017,7 +6333,7 @@ function EmailEditorShell({ editor, height = "100%" }) {
6017
6333
  onReorder: session.reorderBlocks,
6018
6334
  onLibraryDragStart: showCanvas,
6019
6335
  children: [
6020
- /* @__PURE__ */ jsx57(
6336
+ /* @__PURE__ */ jsx58(
6021
6337
  BlockLibrary,
6022
6338
  {
6023
6339
  definitions: session.config.library,
@@ -6027,7 +6343,7 @@ function EmailEditorShell({ editor, height = "100%" }) {
6027
6343
  }
6028
6344
  }
6029
6345
  ),
6030
- /* @__PURE__ */ jsx57(
6346
+ /* @__PURE__ */ jsx58(
6031
6347
  Stage,
6032
6348
  {
6033
6349
  session,
@@ -6041,7 +6357,7 @@ function EmailEditorShell({ editor, height = "100%" }) {
6041
6357
  ]
6042
6358
  }
6043
6359
  ),
6044
- /* @__PURE__ */ jsx57(
6360
+ /* @__PURE__ */ jsx58(
6045
6361
  Inspector,
6046
6362
  {
6047
6363
  panel: inspectorPanel,
@@ -6061,23 +6377,23 @@ function EmailEditorShell({ editor, height = "100%" }) {
6061
6377
  }
6062
6378
 
6063
6379
  // src/EmailEditor.tsx
6064
- import { jsx as jsx58 } from "react/jsx-runtime";
6380
+ import { jsx as jsx59 } from "react/jsx-runtime";
6065
6381
  function EmailEditor(props) {
6066
- return /* @__PURE__ */ jsx58(EmailEditorShell, __spreadValues({}, props));
6382
+ return /* @__PURE__ */ jsx59(EmailEditorShell, __spreadValues({}, props));
6067
6383
  }
6068
6384
 
6069
6385
  // src/plugins/useEmailEditorConfig.ts
6070
6386
  import { useMemo as useMemo5 } from "react";
6071
6387
  function useEmailEditorConfig(options = {}) {
6072
- const { plugins, builtins, pickImage } = options;
6073
- return useMemo5(() => buildEmailEditorConfig({ plugins, builtins, pickImage }), [plugins, builtins, pickImage]);
6388
+ const { plugins, builtins, pickImage, templateEngine } = options;
6389
+ return useMemo5(() => buildEmailEditorConfig({ plugins, builtins, pickImage, templateEngine }), [plugins, builtins, pickImage, templateEngine]);
6074
6390
  }
6075
6391
 
6076
6392
  // src/shell/useEmailEditor.ts
6077
- import { useEffect as useEffect8, useInsertionEffect, useLayoutEffect as useLayoutEffect9, useState as useState19 } from "react";
6078
- var useOptionsEffect = typeof window === "undefined" ? useEffect8 : useLayoutEffect9;
6393
+ import { useEffect as useEffect8, useInsertionEffect, useLayoutEffect as useLayoutEffect11, useState as useState21 } from "react";
6394
+ var useOptionsEffect = typeof window === "undefined" ? useEffect8 : useLayoutEffect11;
6079
6395
  function useEmailEditor(options = {}) {
6080
- const [store] = useState19(() => createEditorStore(options));
6396
+ const [store] = useState21(() => createEditorStore(options));
6081
6397
  useInsertionEffect(() => {
6082
6398
  store.updateOptions(options);
6083
6399
  });
@@ -6093,12 +6409,12 @@ function useEmailEditorState(editor) {
6093
6409
  }
6094
6410
 
6095
6411
  // src/plugins/components/ControlEditor.tsx
6096
- import { jsx as jsx59 } from "react/jsx-runtime";
6412
+ import { jsx as jsx60 } from "react/jsx-runtime";
6097
6413
  function ControlEditor({ control }) {
6098
6414
  const context = usePluginEditor();
6099
6415
  const definition = context.controlDefinitions.find((item) => item.name === control);
6100
6416
  if (!definition) throw new Error(`Unknown plugin control: ${control}`);
6101
- return /* @__PURE__ */ jsx59(ControlRenderer, { control: definition, value: context.controls[control], body: context.body, variables: context.variables, onChange: (value) => context.setControl(control, value) });
6417
+ return /* @__PURE__ */ jsx60(ControlRenderer, { control: definition, value: context.controls[control], body: context.body, variables: context.variables, onChange: (value) => context.setControl(control, value) });
6102
6418
  }
6103
6419
  export {
6104
6420
  BLOCK_TYPES,
@@ -6130,6 +6446,7 @@ export {
6130
6446
  createDimensionControls,
6131
6447
  createEmailDocument,
6132
6448
  createTypographyControls,
6449
+ defaultTemplateEngine,
6133
6450
  defineBlock,
6134
6451
  documentToHtml,
6135
6452
  generateId,
@@ -6142,5 +6459,6 @@ export {
6142
6459
  useEmailEditor,
6143
6460
  useEmailEditorConfig,
6144
6461
  useEmailEditorState,
6462
+ validateTemplate,
6145
6463
  variablesToTemplateData
6146
6464
  };