@kontakto/email-template-editor 2.5.0 → 2.6.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/README.md CHANGED
@@ -223,6 +223,20 @@ function MyApp() {
223
223
  }
224
224
  ```
225
225
 
226
+ ### Keyboard shortcuts
227
+
228
+ The editor surfaces a few global shortcuts. Inside text fields or rich-text blocks the native browser behavior takes over — global undo/redo only fires when focus is on the canvas (not mid-typing).
229
+
230
+ | Action | macOS | Windows / Linux |
231
+ |---|---|---|
232
+ | Undo | ⌘ + Z | Ctrl + Z |
233
+ | Redo | ⌘ + Shift + Z | Ctrl + Shift + Z, Ctrl + Y |
234
+ | Bold (inside text) | ⌘ + B | Ctrl + B |
235
+ | Italic (inside text) | ⌘ + I | Ctrl + I |
236
+ | Link (inside text) | ⌘ + K | Ctrl + K |
237
+
238
+ The undo history tracks document mutations only (block insert/delete/move, content edits, style changes, variable edits). Rapid consecutive edits (dragging a slider, typing a burst) collapse into a single history entry. The ring buffer holds the last 100 entries and resets whenever a new template is loaded.
239
+
226
240
  ### Stand-alone version using Vite
227
241
 
228
242
  This project includes a standalone version that can be run using Vite:
package/dist/index.cjs CHANGED
@@ -11,6 +11,7 @@ var react = require('@lingui/react');
11
11
  var material = require('@mui/material');
12
12
  var core = require('@lingui/core');
13
13
  var zustand = require('zustand');
14
+ var zundo = require('zundo');
14
15
  var iconsMaterial = require('@mui/icons-material');
15
16
  var reactColorful = require('react-colorful');
16
17
  var hljs = require('highlight.js');
@@ -2056,20 +2057,48 @@ var EMPTY_DOCUMENT = {
2056
2057
  }
2057
2058
  }
2058
2059
  };
2059
- var editorStateStore = zustand.create(() => ({
2060
- document: EMPTY_DOCUMENT,
2061
- selectedBlockId: null,
2062
- selectedSidebarTab: "styles",
2063
- selectedMainTab: "editor",
2064
- selectedScreenSize: "desktop",
2065
- inspectorDrawerOpen: true,
2066
- samplesDrawerOpen: true,
2067
- persistenceEnabled: false,
2068
- lastFocusedEditable: null,
2069
- hoveredBlockId: null,
2070
- draggingBlock: null,
2071
- workspaceBackground: "checkerboard"
2072
- }));
2060
+ var COALESCE_MS = 300;
2061
+ function leadingThrottle(fn, wait) {
2062
+ let last = Number.NEGATIVE_INFINITY;
2063
+ return (...args) => {
2064
+ const now = Date.now();
2065
+ if (now - last >= wait) {
2066
+ last = now;
2067
+ fn(...args);
2068
+ }
2069
+ };
2070
+ }
2071
+ var editorStateStore = zustand.create()(
2072
+ zundo.temporal(
2073
+ () => ({
2074
+ document: EMPTY_DOCUMENT,
2075
+ selectedBlockId: null,
2076
+ selectedSidebarTab: "styles",
2077
+ selectedMainTab: "editor",
2078
+ selectedScreenSize: "desktop",
2079
+ inspectorDrawerOpen: true,
2080
+ samplesDrawerOpen: true,
2081
+ persistenceEnabled: false,
2082
+ lastFocusedEditable: null,
2083
+ hoveredBlockId: null,
2084
+ draggingBlock: null,
2085
+ workspaceBackground: "checkerboard"
2086
+ }),
2087
+ {
2088
+ limit: 100,
2089
+ // Only the document participates in history — selection, drawers, tabs
2090
+ // and other UI state are intentionally excluded.
2091
+ partialize: (state) => ({ document: state.document }),
2092
+ // Skip UI-only state changes: if the document reference is unchanged,
2093
+ // no history entry is recorded.
2094
+ equality: (a, b) => a.document === b.document,
2095
+ handleSet: (handleSet) => leadingThrottle(
2096
+ (pastState, replace, currentState) => handleSet(pastState, replace, currentState),
2097
+ COALESCE_MS
2098
+ )
2099
+ }
2100
+ )
2101
+ );
2073
2102
  function useDocument() {
2074
2103
  return editorStateStore((s) => s.document);
2075
2104
  }
@@ -2112,11 +2141,15 @@ function setSidebarTab(selectedSidebarTab) {
2112
2141
  return editorStateStore.setState({ selectedSidebarTab });
2113
2142
  }
2114
2143
  function resetDocument(document2) {
2115
- return editorStateStore.setState({
2144
+ const temporalApi = editorStateStore.temporal.getState();
2145
+ temporalApi.pause();
2146
+ editorStateStore.setState({
2116
2147
  document: document2,
2117
2148
  selectedSidebarTab: "styles",
2118
2149
  selectedBlockId: null
2119
2150
  });
2151
+ temporalApi.clear();
2152
+ temporalApi.resume();
2120
2153
  }
2121
2154
  function getDocument() {
2122
2155
  return editorStateStore.getState().document;
@@ -2127,6 +2160,9 @@ function setDocument(document2) {
2127
2160
  document: __spreadValues(__spreadValues({}, originalDocument), document2)
2128
2161
  });
2129
2162
  }
2163
+ function replaceDocument(document2) {
2164
+ editorStateStore.setState({ document: document2 });
2165
+ }
2130
2166
  function toggleInspectorDrawerOpen() {
2131
2167
  const inspectorDrawerOpen = !editorStateStore.getState().inspectorDrawerOpen;
2132
2168
  return editorStateStore.setState({ inspectorDrawerOpen });
@@ -2171,6 +2207,18 @@ function setWorkspaceBackground(workspaceBackground) {
2171
2207
  function setLastFocusedEditable(lastFocusedEditable) {
2172
2208
  return editorStateStore.setState({ lastFocusedEditable });
2173
2209
  }
2210
+ function undo() {
2211
+ editorStateStore.temporal.getState().undo();
2212
+ }
2213
+ function redo() {
2214
+ editorStateStore.temporal.getState().redo();
2215
+ }
2216
+ function useCanUndo() {
2217
+ return zustand.useStore(editorStateStore.temporal, (s) => s.pastStates.length > 0);
2218
+ }
2219
+ function useCanRedo() {
2220
+ return zustand.useStore(editorStateStore.temporal, (s) => s.futureStates.length > 0);
2221
+ }
2174
2222
 
2175
2223
  // src/app/save-payload.ts
2176
2224
  var ROOT_BLOCK_ID = "root";
@@ -6863,7 +6911,7 @@ function EmailLayoutEditor(props) {
6863
6911
  }
6864
6912
  }
6865
6913
  delete nDocument[selectedBlockId];
6866
- resetDocument(nDocument);
6914
+ replaceDocument(nDocument);
6867
6915
  }, [selectedBlockId, document2]);
6868
6916
  const handleCopy = React57.useCallback((e) => {
6869
6917
  if (!(e.metaKey || e.ctrlKey) || e.key !== "c") return;
@@ -6906,7 +6954,7 @@ function EmailLayoutEditor(props) {
6906
6954
  childrenIds: currentChildrenIds
6907
6955
  })
6908
6956
  };
6909
- resetDocument(doc);
6957
+ replaceDocument(doc);
6910
6958
  setSelectedBlockId(newRootId);
6911
6959
  }), [document2, childrenIds, selectedBlockId, currentBlockId]);
6912
6960
  React57.useEffect(() => {
@@ -6947,7 +6995,6 @@ function EmailLayoutEditor(props) {
6947
6995
  }
6948
6996
  }
6949
6997
  );
6950
- const WORKSPACE_BG = "#e7e8ec";
6951
6998
  const CARD_MAX_WIDTH = 664;
6952
6999
  const cardStyle = {
6953
7000
  maxWidth: CARD_MAX_WIDTH,
@@ -6963,7 +7010,6 @@ function EmailLayoutEditor(props) {
6963
7010
  setSelectedBlockId(null);
6964
7011
  },
6965
7012
  style: __spreadProps(__spreadValues({}, baseStyle), {
6966
- backgroundColor: WORKSPACE_BG,
6967
7013
  padding: "32px",
6968
7014
  width: "100%",
6969
7015
  minHeight: "100%"
@@ -6988,7 +7034,6 @@ function EmailLayoutEditor(props) {
6988
7034
  setSelectedBlockId(null);
6989
7035
  },
6990
7036
  style: __spreadProps(__spreadValues({}, baseStyle), {
6991
- backgroundColor: WORKSPACE_BG,
6992
7037
  padding: "32px 16px",
6993
7038
  width: "100%",
6994
7039
  minHeight: "100%"
@@ -8637,6 +8682,64 @@ function SaveBar({ loadTemplates, saveAs }) {
8637
8682
  }
8638
8683
  ));
8639
8684
  }
8685
+ function isMac() {
8686
+ if (typeof navigator === "undefined") return false;
8687
+ const platform = (navigator.platform || "").toLowerCase();
8688
+ if (platform.includes("mac")) return true;
8689
+ const ua = (navigator.userAgent || "").toLowerCase();
8690
+ return ua.includes("mac") && !ua.includes("windows");
8691
+ }
8692
+ function isEditableTarget(target) {
8693
+ if (!(target instanceof HTMLElement)) return false;
8694
+ const tag = target.tagName;
8695
+ if (tag === "INPUT" || tag === "TEXTAREA" || tag === "SELECT") return true;
8696
+ if (target.isContentEditable) return true;
8697
+ return false;
8698
+ }
8699
+ function UndoRedoButtons() {
8700
+ const canUndo = useCanUndo();
8701
+ const canRedo = useCanRedo();
8702
+ const mac = isMac();
8703
+ const modKey = mac ? "\u2318" : "Ctrl";
8704
+ const undoHint = `${modKey}+Z`;
8705
+ const redoHint = mac ? `${modKey}+\u21E7+Z` : `${modKey}+Shift+Z / ${modKey}+Y`;
8706
+ React57.useEffect(() => {
8707
+ const onKeyDown = (e) => {
8708
+ const mod = mac ? e.metaKey : e.ctrlKey;
8709
+ if (!mod) return;
8710
+ if (isEditableTarget(e.target)) return;
8711
+ const key = e.key.toLowerCase();
8712
+ if (key === "z" && !e.shiftKey) {
8713
+ e.preventDefault();
8714
+ undo();
8715
+ } else if (key === "z" && e.shiftKey || key === "y" && !mac) {
8716
+ e.preventDefault();
8717
+ redo();
8718
+ }
8719
+ };
8720
+ window.addEventListener("keydown", onKeyDown);
8721
+ return () => window.removeEventListener("keydown", onKeyDown);
8722
+ }, [mac]);
8723
+ return /* @__PURE__ */ React57__default.default.createElement(material.Stack, { direction: "row", spacing: 0.5, alignItems: "center" }, /* @__PURE__ */ React57__default.default.createElement(material.Tooltip, { title: `${t("undo.tooltip", "Undo")} (${undoHint})` }, /* @__PURE__ */ React57__default.default.createElement("span", null, /* @__PURE__ */ React57__default.default.createElement(
8724
+ material.IconButton,
8725
+ {
8726
+ size: "small",
8727
+ onClick: undo,
8728
+ disabled: !canUndo,
8729
+ "aria-label": t("undo.label", "Undo")
8730
+ },
8731
+ /* @__PURE__ */ React57__default.default.createElement(iconsMaterial.UndoOutlined, { fontSize: "small" })
8732
+ ))), /* @__PURE__ */ React57__default.default.createElement(material.Tooltip, { title: `${t("redo.tooltip", "Redo")} (${redoHint})` }, /* @__PURE__ */ React57__default.default.createElement("span", null, /* @__PURE__ */ React57__default.default.createElement(
8733
+ material.IconButton,
8734
+ {
8735
+ size: "small",
8736
+ onClick: redo,
8737
+ disabled: !canRedo,
8738
+ "aria-label": t("redo.label", "Redo")
8739
+ },
8740
+ /* @__PURE__ */ React57__default.default.createElement(iconsMaterial.RedoOutlined, { fontSize: "small" })
8741
+ ))));
8742
+ }
8640
8743
  function SubjectInput() {
8641
8744
  var _a;
8642
8745
  const document2 = useDocument();
@@ -8865,7 +8968,7 @@ function ImageDropPasteHandler({ enabled, children }) {
8865
8968
 
8866
8969
  // src/app/email-canvas/index.tsx
8867
8970
  var WORKSPACE_SOLID = "#e7e8ec";
8868
- var WORKSPACE_CHECKERBOARD = "repeating-conic-gradient(#eceef2 0% 25%, #dfe1e6 0% 50%) 50% / 24px 24px";
8971
+ var WORKSPACE_CHECKERBOARD = "repeating-conic-gradient(#eceef2 0% 25%, #dfe1e6 0% 50%) 50% / 12px 12px";
8869
8972
  function TemplatePanel2({ loadTemplates, saveAs, samplesDrawerEnabled = true }) {
8870
8973
  const document2 = useDocument();
8871
8974
  const selectedMainTab = useSelectedMainTab();
@@ -8935,7 +9038,7 @@ function TemplatePanel2({ loadTemplates, saveAs, samplesDrawerEnabled = true })
8935
9038
  alignItems: "center"
8936
9039
  },
8937
9040
  samplesDrawerEnabled && /* @__PURE__ */ React57__default.default.createElement(ToggleSamplesPanelButton, null),
8938
- /* @__PURE__ */ React57__default.default.createElement(material.Stack, { px: 2, direction: "row", gap: 2, width: "100%", justifyContent: "space-between", alignItems: "center" }, /* @__PURE__ */ React57__default.default.createElement(material.Stack, { direction: "row", spacing: 2 }, /* @__PURE__ */ React57__default.default.createElement(MainTabsGroup, null)), /* @__PURE__ */ React57__default.default.createElement(material.Stack, { direction: "row", spacing: 2 }, /* @__PURE__ */ React57__default.default.createElement(material.ToggleButtonGroup, { value: selectedScreenSize, exclusive: true, size: "small", onChange: handleScreenSizeChange }, /* @__PURE__ */ React57__default.default.createElement(material.ToggleButton, { value: "desktop" }, /* @__PURE__ */ React57__default.default.createElement(material.Tooltip, { title: "Desktop view" }, /* @__PURE__ */ React57__default.default.createElement(iconsMaterial.MonitorOutlined, { fontSize: "small" }))), /* @__PURE__ */ React57__default.default.createElement(material.ToggleButton, { value: "mobile" }, /* @__PURE__ */ React57__default.default.createElement(material.Tooltip, { title: "Mobile view" }, /* @__PURE__ */ React57__default.default.createElement(iconsMaterial.PhoneIphoneOutlined, { fontSize: "small" })))))),
9041
+ /* @__PURE__ */ React57__default.default.createElement(material.Stack, { px: 2, direction: "row", gap: 2, width: "100%", justifyContent: "space-between", alignItems: "center" }, /* @__PURE__ */ React57__default.default.createElement(material.Stack, { direction: "row", spacing: 2 }, /* @__PURE__ */ React57__default.default.createElement(MainTabsGroup, null)), /* @__PURE__ */ React57__default.default.createElement(material.Stack, { direction: "row", spacing: 2, alignItems: "center" }, selectedMainTab === "editor" && /* @__PURE__ */ React57__default.default.createElement(UndoRedoButtons, null), /* @__PURE__ */ React57__default.default.createElement(material.ToggleButtonGroup, { value: selectedScreenSize, exclusive: true, size: "small", onChange: handleScreenSizeChange }, /* @__PURE__ */ React57__default.default.createElement(material.ToggleButton, { value: "desktop" }, /* @__PURE__ */ React57__default.default.createElement(material.Tooltip, { title: "Desktop view" }, /* @__PURE__ */ React57__default.default.createElement(iconsMaterial.MonitorOutlined, { fontSize: "small" }))), /* @__PURE__ */ React57__default.default.createElement(material.ToggleButton, { value: "mobile" }, /* @__PURE__ */ React57__default.default.createElement(material.Tooltip, { title: "Mobile view" }, /* @__PURE__ */ React57__default.default.createElement(iconsMaterial.PhoneIphoneOutlined, { fontSize: "small" })))))),
8939
9042
  /* @__PURE__ */ React57__default.default.createElement(ToggleInspectorPanelButton, null)
8940
9043
  ), selectedMainTab === "editor" && /* @__PURE__ */ React57__default.default.createElement(SubjectInput, null), selectedMainTab === "preview" && /* @__PURE__ */ React57__default.default.createElement(SubjectPreview, null), /* @__PURE__ */ React57__default.default.createElement(ImageDropPasteHandler, { enabled: selectedMainTab === "editor" }, /* @__PURE__ */ React57__default.default.createElement(
8941
9044
  material.Box,