@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 +14 -0
- package/dist/index.cjs +125 -22
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +127 -24
- package/dist/index.js.map +1 -1
- package/package.json +2 -1
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
|
|
2060
|
-
|
|
2061
|
-
|
|
2062
|
-
|
|
2063
|
-
|
|
2064
|
-
|
|
2065
|
-
|
|
2066
|
-
|
|
2067
|
-
|
|
2068
|
-
|
|
2069
|
-
|
|
2070
|
-
|
|
2071
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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% /
|
|
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,
|