dpk-editor 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs ADDED
@@ -0,0 +1,990 @@
1
+ "use client";
2
+ 'use strict';
3
+
4
+ Object.defineProperty(exports, '__esModule', { value: true });
5
+
6
+ var react$1 = require('react');
7
+ var react = require('@tiptap/react');
8
+ var Highlight = require('@tiptap/extension-highlight');
9
+ var Image = require('@tiptap/extension-image');
10
+ var Placeholder = require('@tiptap/extension-placeholder');
11
+ var TextAlign = require('@tiptap/extension-text-align');
12
+ var extensionTextStyle = require('@tiptap/extension-text-style');
13
+ var StarterKit = require('@tiptap/starter-kit');
14
+ var jsxRuntime = require('react/jsx-runtime');
15
+ var lucideReact = require('lucide-react');
16
+
17
+ function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
18
+
19
+ var Highlight__default = /*#__PURE__*/_interopDefault(Highlight);
20
+ var Image__default = /*#__PURE__*/_interopDefault(Image);
21
+ var Placeholder__default = /*#__PURE__*/_interopDefault(Placeholder);
22
+ var TextAlign__default = /*#__PURE__*/_interopDefault(TextAlign);
23
+ var StarterKit__default = /*#__PURE__*/_interopDefault(StarterKit);
24
+
25
+ // src/components/EmailEditor.tsx
26
+
27
+ // src/utils/shellBridge.ts
28
+ var BODY_OPEN_RE = /<body\b[^>]*>/i;
29
+ var BODY_CLOSE_RE = /<\/body\s*>/i;
30
+ function splitEmailHtml(html) {
31
+ const input = html ?? "";
32
+ const openMatch = input.match(BODY_OPEN_RE);
33
+ if (!openMatch || openMatch.index === void 0) {
34
+ return { prefix: "", body: input, suffix: "" };
35
+ }
36
+ const openStart = openMatch.index;
37
+ const openEnd = openStart + openMatch[0].length;
38
+ const prefix = input.slice(0, openEnd);
39
+ const rest = input.slice(openEnd);
40
+ const closeMatch = rest.match(BODY_CLOSE_RE);
41
+ if (!closeMatch || closeMatch.index === void 0) {
42
+ return { prefix, body: rest, suffix: "" };
43
+ }
44
+ const body = rest.slice(0, closeMatch.index);
45
+ const suffix = rest.slice(closeMatch.index);
46
+ return { prefix, body, suffix };
47
+ }
48
+ function joinEmailHtml(shell, newBody) {
49
+ const body = newBody ?? "";
50
+ if (!shell.prefix && !shell.suffix) {
51
+ return body;
52
+ }
53
+ return `${shell.prefix}${body}${shell.suffix}`;
54
+ }
55
+ function extractEmailBody(html) {
56
+ return splitEmailHtml(html).body;
57
+ }
58
+ var PreserveStyles = react.Extension.create({
59
+ name: "preserveStyles",
60
+ addGlobalAttributes() {
61
+ return [
62
+ {
63
+ // ⚠️ MUST be the string shorthand "*" (= all nodes and marks).
64
+ // NEVER ["*"] — an array means "a type literally named *", which
65
+ // matches nothing and silently preserves no styles. That bug passes
66
+ // tsc/eslint/build cleanly, so it is asserted against in the tests.
67
+ types: "*",
68
+ attributes: {
69
+ style: {
70
+ default: null,
71
+ parseHTML: (element) => element.getAttribute("style"),
72
+ renderHTML: (attributes) => attributes.style ? { style: attributes.style } : {}
73
+ }
74
+ }
75
+ }
76
+ ];
77
+ }
78
+ });
79
+
80
+ // src/extensions/editorExtensions.ts
81
+ function createEmailExtensions(placeholder) {
82
+ return [
83
+ StarterKit__default.default.configure({
84
+ link: {
85
+ openOnClick: false,
86
+ autolink: true,
87
+ HTMLAttributes: { rel: "noopener noreferrer" }
88
+ }
89
+ }),
90
+ Image__default.default.configure({ inline: false, allowBase64: false }),
91
+ TextAlign__default.default.configure({ types: ["heading", "paragraph"] }),
92
+ extensionTextStyle.TextStyle,
93
+ extensionTextStyle.Color,
94
+ Highlight__default.default.configure({ multicolor: true }),
95
+ Placeholder__default.default.configure({
96
+ placeholder: placeholder ?? "Write your email\u2026",
97
+ // Emit data-placeholder so styles.css can render it via attr(); also
98
+ // keep the default is-editor-empty class for the :first-child selector.
99
+ showOnlyWhenEditable: true
100
+ }),
101
+ PreserveStyles
102
+ ];
103
+ }
104
+
105
+ // src/utils/presets.ts
106
+ var PRESET_PARAGRAPH = '<p style="margin:0 0 12px;color:#4b5563;line-height:1.6;">Write your message here.</p>';
107
+ var PRESET_DIVIDER = '<hr style="border:none;border-top:1px solid #e5e7eb;margin:20px 0;" />';
108
+ var PRESET_FOOTER = '<p style="margin:20px 0 0;color:#9ca3af;font-size:12px;">\xA9 Your Company</p>';
109
+ var PRESET_HEADING = '<h2 style="margin:0 0 12px;color:#111827;font-size:24px;line-height:1.3;">Heading</h2>';
110
+ var presets = {
111
+ paragraph: PRESET_PARAGRAPH,
112
+ divider: PRESET_DIVIDER,
113
+ footer: PRESET_FOOTER,
114
+ heading: PRESET_HEADING
115
+ };
116
+
117
+ // src/utils/resolveToolbarConfig.ts
118
+ function resolveGroup(group, defaults) {
119
+ if (group === void 0 || group === true) {
120
+ return { enabled: true, buttons: { ...defaults } };
121
+ }
122
+ if (group === false) {
123
+ const off = {};
124
+ for (const key of Object.keys(defaults)) {
125
+ off[key] = false;
126
+ }
127
+ return { enabled: false, buttons: off };
128
+ }
129
+ return { enabled: true, buttons: { ...defaults, ...group } };
130
+ }
131
+ var INLINE_DEFAULTS = {
132
+ bold: true,
133
+ italic: true,
134
+ underline: true,
135
+ strike: true,
136
+ code: true
137
+ };
138
+ var HEADING_DEFAULTS = {
139
+ h2: true,
140
+ h3: true,
141
+ h4: true,
142
+ h5: true,
143
+ h6: true
144
+ };
145
+ var LIST_DEFAULTS = {
146
+ bullet: true,
147
+ ordered: true,
148
+ blockquote: true
149
+ };
150
+ var ALIGN_DEFAULTS = {
151
+ left: true,
152
+ center: true,
153
+ right: true
154
+ };
155
+ var BLOCK_DEFAULTS = {
156
+ paragraph: true,
157
+ divider: true,
158
+ footer: true
159
+ };
160
+ function resolveToggle(value) {
161
+ return value !== false;
162
+ }
163
+ function resolveToolbarConfig(config) {
164
+ const c = config ?? {};
165
+ return {
166
+ inline: resolveGroup(c.inline, INLINE_DEFAULTS),
167
+ headings: resolveGroup(c.headings, HEADING_DEFAULTS),
168
+ lists: resolveGroup(c.lists, LIST_DEFAULTS),
169
+ align: resolveGroup(c.align, ALIGN_DEFAULTS),
170
+ link: resolveToggle(c.link),
171
+ image: resolveToggle(c.image),
172
+ button: resolveToggle(c.button),
173
+ blocks: resolveGroup(c.blocks, BLOCK_DEFAULTS),
174
+ html: resolveToggle(c.html)
175
+ };
176
+ }
177
+
178
+ // src/utils/escapeHtml.ts
179
+ function escapeHtml(value) {
180
+ return String(value).replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
181
+ }
182
+
183
+ // src/utils/buildButtonHtml.ts
184
+ function buildButtonHtml(config) {
185
+ const {
186
+ text,
187
+ href,
188
+ bgColor,
189
+ textColor,
190
+ align,
191
+ radius,
192
+ fullWidth
193
+ } = config;
194
+ const safeText = escapeHtml(text || "Button");
195
+ const safeHref = escapeHtml(href || "#");
196
+ const safeRadius = Number.isFinite(radius) ? Math.max(0, radius) : 0;
197
+ const display = fullWidth ? "block" : "inline-block";
198
+ const anchorTextAlign = fullWidth ? "text-align:center;" : "";
199
+ const anchorStyle = [
200
+ `display:${display}`,
201
+ "padding:12px 24px",
202
+ `background-color:${bgColor}`,
203
+ `color:${textColor}`,
204
+ "text-decoration:none",
205
+ "font-weight:600",
206
+ "font-family:Arial,Helvetica,sans-serif",
207
+ "font-size:14px",
208
+ "line-height:1.4",
209
+ `border-radius:${safeRadius}px`,
210
+ anchorTextAlign
211
+ ].filter(Boolean).join(";");
212
+ const paragraphStyle = `margin:16px 0;text-align:${align}`;
213
+ return `<p style="${paragraphStyle}"><a href="${safeHref}" target="_blank" rel="noopener noreferrer" style="${anchorStyle}">${safeText}</a></p>`;
214
+ }
215
+ var DEFAULT_CONFIG = {
216
+ text: "Click here",
217
+ href: "https://",
218
+ bgColor: "#2563eb",
219
+ textColor: "#ffffff",
220
+ align: "left",
221
+ radius: 6,
222
+ fullWidth: false
223
+ };
224
+ var BG_SWATCHES = [
225
+ "#2563eb",
226
+ "#16a34a",
227
+ "#dc2626",
228
+ "#7c3aed",
229
+ "#ea580c",
230
+ "#0891b2",
231
+ "#111827"
232
+ ];
233
+ var TEXT_SWATCHES = ["#ffffff", "#111827", "#f9fafb", "#1f2937"];
234
+ var HEX6_RE = /^#[0-9a-fA-F]{6}$/;
235
+ function toColorInputValue(value) {
236
+ return HEX6_RE.test(value) ? value : "#000000";
237
+ }
238
+ function ColorField({ label, value, swatches, onChange }) {
239
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "rte-dialog-field", children: [
240
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "rte-dialog-label", children: label }),
241
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "rte-color-row", children: [
242
+ swatches.map((swatch) => /* @__PURE__ */ jsxRuntime.jsx(
243
+ "button",
244
+ {
245
+ type: "button",
246
+ className: "rte-swatch" + (value === swatch ? " rte-swatch--active" : ""),
247
+ style: { backgroundColor: swatch },
248
+ "aria-label": `Use ${swatch}`,
249
+ "aria-pressed": value === swatch,
250
+ onMouseDown: (e) => e.preventDefault(),
251
+ onClick: () => onChange(swatch)
252
+ },
253
+ swatch
254
+ )),
255
+ /* @__PURE__ */ jsxRuntime.jsx(
256
+ "input",
257
+ {
258
+ type: "color",
259
+ className: "rte-color-input",
260
+ value: toColorInputValue(value),
261
+ "aria-label": `${label} picker`,
262
+ onChange: (e) => onChange(e.target.value)
263
+ }
264
+ ),
265
+ /* @__PURE__ */ jsxRuntime.jsx(
266
+ "input",
267
+ {
268
+ type: "text",
269
+ className: "rte-hex-input",
270
+ value,
271
+ "aria-label": `${label} hex`,
272
+ spellCheck: false,
273
+ onChange: (e) => onChange(e.target.value)
274
+ }
275
+ )
276
+ ] })
277
+ ] });
278
+ }
279
+ function EmailButtonDialog({
280
+ open,
281
+ onConfirm,
282
+ onClose,
283
+ initial
284
+ }) {
285
+ const [config, setConfig] = react$1.useState({
286
+ ...DEFAULT_CONFIG,
287
+ ...initial
288
+ });
289
+ const overlayRef = react$1.useRef(null);
290
+ react$1.useEffect(() => {
291
+ if (open) {
292
+ setConfig({ ...DEFAULT_CONFIG, ...initial });
293
+ }
294
+ }, [open, initial]);
295
+ react$1.useEffect(() => {
296
+ if (!open) return;
297
+ const onKeyDown = (e) => {
298
+ if (e.key === "Escape") {
299
+ e.stopPropagation();
300
+ onClose();
301
+ }
302
+ };
303
+ document.addEventListener("keydown", onKeyDown);
304
+ const previousOverflow = document.body.style.overflow;
305
+ document.body.style.overflow = "hidden";
306
+ return () => {
307
+ document.removeEventListener("keydown", onKeyDown);
308
+ document.body.style.overflow = previousOverflow;
309
+ };
310
+ }, [open, onClose]);
311
+ const previewHtml = react$1.useMemo(() => buildButtonHtml(config), [config]);
312
+ if (!open) return null;
313
+ const update = (key, value) => setConfig((prev) => ({ ...prev, [key]: value }));
314
+ const handleSubmit = () => {
315
+ onConfirm(buildButtonHtml(config), config);
316
+ };
317
+ return /* @__PURE__ */ jsxRuntime.jsx(
318
+ "div",
319
+ {
320
+ ref: overlayRef,
321
+ className: "rte-dialog-overlay",
322
+ role: "presentation",
323
+ onMouseDown: (e) => {
324
+ if (e.target === overlayRef.current) onClose();
325
+ },
326
+ children: /* @__PURE__ */ jsxRuntime.jsxs(
327
+ "div",
328
+ {
329
+ className: "rte-dialog",
330
+ role: "dialog",
331
+ "aria-modal": "true",
332
+ "aria-label": "Insert button",
333
+ children: [
334
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "rte-dialog-header", children: [
335
+ /* @__PURE__ */ jsxRuntime.jsx("h2", { className: "rte-dialog-title", children: "Insert button" }),
336
+ /* @__PURE__ */ jsxRuntime.jsx(
337
+ "button",
338
+ {
339
+ type: "button",
340
+ className: "rte-dialog-close",
341
+ "aria-label": "Close",
342
+ onClick: onClose,
343
+ children: "\xD7"
344
+ }
345
+ )
346
+ ] }),
347
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "rte-dialog-body", children: [
348
+ /* @__PURE__ */ jsxRuntime.jsxs("label", { className: "rte-dialog-field", children: [
349
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "rte-dialog-label", children: "Text" }),
350
+ /* @__PURE__ */ jsxRuntime.jsx(
351
+ "input",
352
+ {
353
+ type: "text",
354
+ className: "rte-text-input",
355
+ value: config.text,
356
+ onChange: (e) => update("text", e.target.value)
357
+ }
358
+ )
359
+ ] }),
360
+ /* @__PURE__ */ jsxRuntime.jsxs("label", { className: "rte-dialog-field", children: [
361
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "rte-dialog-label", children: "Link URL" }),
362
+ /* @__PURE__ */ jsxRuntime.jsx(
363
+ "input",
364
+ {
365
+ type: "text",
366
+ className: "rte-text-input",
367
+ value: config.href,
368
+ onChange: (e) => update("href", e.target.value)
369
+ }
370
+ )
371
+ ] }),
372
+ /* @__PURE__ */ jsxRuntime.jsx(
373
+ ColorField,
374
+ {
375
+ label: "Background color",
376
+ value: config.bgColor,
377
+ swatches: BG_SWATCHES,
378
+ onChange: (v) => update("bgColor", v)
379
+ }
380
+ ),
381
+ /* @__PURE__ */ jsxRuntime.jsx(
382
+ ColorField,
383
+ {
384
+ label: "Text color",
385
+ value: config.textColor,
386
+ swatches: TEXT_SWATCHES,
387
+ onChange: (v) => update("textColor", v)
388
+ }
389
+ ),
390
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "rte-dialog-field", children: [
391
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "rte-dialog-label", children: "Alignment" }),
392
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "rte-align-row", children: ["left", "center", "right"].map((a) => /* @__PURE__ */ jsxRuntime.jsx(
393
+ "button",
394
+ {
395
+ type: "button",
396
+ className: "rte-align-btn" + (config.align === a ? " rte-align-btn--active" : ""),
397
+ "aria-pressed": config.align === a,
398
+ onMouseDown: (e) => e.preventDefault(),
399
+ onClick: () => update("align", a),
400
+ children: a
401
+ },
402
+ a
403
+ )) })
404
+ ] }),
405
+ /* @__PURE__ */ jsxRuntime.jsxs("label", { className: "rte-dialog-field", children: [
406
+ /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "rte-dialog-label", children: [
407
+ "Corner radius: ",
408
+ config.radius,
409
+ "px"
410
+ ] }),
411
+ /* @__PURE__ */ jsxRuntime.jsx(
412
+ "input",
413
+ {
414
+ type: "range",
415
+ min: 0,
416
+ max: 32,
417
+ value: config.radius,
418
+ onChange: (e) => update("radius", Number(e.target.value))
419
+ }
420
+ )
421
+ ] }),
422
+ /* @__PURE__ */ jsxRuntime.jsxs("label", { className: "rte-checkbox-field", children: [
423
+ /* @__PURE__ */ jsxRuntime.jsx(
424
+ "input",
425
+ {
426
+ type: "checkbox",
427
+ checked: config.fullWidth,
428
+ onChange: (e) => update("fullWidth", e.target.checked)
429
+ }
430
+ ),
431
+ /* @__PURE__ */ jsxRuntime.jsx("span", { children: "Full width" })
432
+ ] }),
433
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "rte-dialog-field", children: [
434
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "rte-dialog-label", children: "Preview" }),
435
+ /* @__PURE__ */ jsxRuntime.jsx(
436
+ "div",
437
+ {
438
+ className: "rte-button-preview",
439
+ dangerouslySetInnerHTML: { __html: previewHtml }
440
+ }
441
+ )
442
+ ] })
443
+ ] }),
444
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "rte-dialog-footer", children: [
445
+ /* @__PURE__ */ jsxRuntime.jsx(
446
+ "button",
447
+ {
448
+ type: "button",
449
+ className: "rte-btn rte-btn--ghost",
450
+ onClick: onClose,
451
+ children: "Cancel"
452
+ }
453
+ ),
454
+ /* @__PURE__ */ jsxRuntime.jsx(
455
+ "button",
456
+ {
457
+ type: "button",
458
+ className: "rte-btn rte-btn--primary",
459
+ onClick: handleSubmit,
460
+ children: "Insert"
461
+ }
462
+ )
463
+ ] })
464
+ ]
465
+ }
466
+ )
467
+ }
468
+ );
469
+ }
470
+ function TbButton({ onClick, active, label, disabled, children }) {
471
+ return /* @__PURE__ */ jsxRuntime.jsx(
472
+ "button",
473
+ {
474
+ type: "button",
475
+ className: "rte-tb-btn" + (active ? " rte-tb-btn--active" : ""),
476
+ onMouseDown: (e) => e.preventDefault(),
477
+ onClick,
478
+ "aria-pressed": !!active,
479
+ "aria-label": label,
480
+ title: label,
481
+ disabled,
482
+ children
483
+ }
484
+ );
485
+ }
486
+ function Divider() {
487
+ return /* @__PURE__ */ jsxRuntime.jsx("span", { className: "rte-tb-divider", "aria-hidden": "true" });
488
+ }
489
+ var HEADING_ICONS = {
490
+ 2: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Heading2, { size: 16 }),
491
+ 3: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Heading3, { size: 16 }),
492
+ 4: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Heading4, { size: 16 }),
493
+ 5: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Heading5, { size: 16 }),
494
+ 6: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Heading6, { size: 16 })
495
+ };
496
+ function Toolbar({
497
+ editor,
498
+ config,
499
+ htmlMode,
500
+ onToggleHtml,
501
+ onLink,
502
+ onImage,
503
+ onButton,
504
+ onInsertParagraph,
505
+ onInsertDivider,
506
+ onInsertFooter
507
+ }) {
508
+ const state = react.useEditorState({
509
+ editor,
510
+ selector: ({ editor: e }) => ({
511
+ bold: e.isActive("bold"),
512
+ italic: e.isActive("italic"),
513
+ underline: e.isActive("underline"),
514
+ strike: e.isActive("strike"),
515
+ code: e.isActive("code"),
516
+ h2: e.isActive("heading", { level: 2 }),
517
+ h3: e.isActive("heading", { level: 3 }),
518
+ h4: e.isActive("heading", { level: 4 }),
519
+ h5: e.isActive("heading", { level: 5 }),
520
+ h6: e.isActive("heading", { level: 6 }),
521
+ bulletList: e.isActive("bulletList"),
522
+ orderedList: e.isActive("orderedList"),
523
+ blockquote: e.isActive("blockquote"),
524
+ alignLeft: e.isActive({ textAlign: "left" }),
525
+ alignCenter: e.isActive({ textAlign: "center" }),
526
+ alignRight: e.isActive({ textAlign: "right" }),
527
+ link: e.isActive("link"),
528
+ image: e.isActive("image")
529
+ })
530
+ });
531
+ const headingActive = {
532
+ 2: state.h2,
533
+ 3: state.h3,
534
+ 4: state.h4,
535
+ 5: state.h5,
536
+ 6: state.h6
537
+ };
538
+ const disabled = htmlMode;
539
+ const inline = config.inline.enabled ? [
540
+ config.inline.buttons.bold && /* @__PURE__ */ jsxRuntime.jsx(
541
+ TbButton,
542
+ {
543
+ label: "Bold",
544
+ active: state.bold,
545
+ disabled,
546
+ onClick: () => editor.chain().focus().toggleBold().run(),
547
+ children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Bold, { size: 16 })
548
+ },
549
+ "bold"
550
+ ),
551
+ config.inline.buttons.italic && /* @__PURE__ */ jsxRuntime.jsx(
552
+ TbButton,
553
+ {
554
+ label: "Italic",
555
+ active: state.italic,
556
+ disabled,
557
+ onClick: () => editor.chain().focus().toggleItalic().run(),
558
+ children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Italic, { size: 16 })
559
+ },
560
+ "italic"
561
+ ),
562
+ config.inline.buttons.underline && /* @__PURE__ */ jsxRuntime.jsx(
563
+ TbButton,
564
+ {
565
+ label: "Underline",
566
+ active: state.underline,
567
+ disabled,
568
+ onClick: () => editor.chain().focus().toggleUnderline().run(),
569
+ children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Underline, { size: 16 })
570
+ },
571
+ "underline"
572
+ ),
573
+ config.inline.buttons.strike && /* @__PURE__ */ jsxRuntime.jsx(
574
+ TbButton,
575
+ {
576
+ label: "Strikethrough",
577
+ active: state.strike,
578
+ disabled,
579
+ onClick: () => editor.chain().focus().toggleStrike().run(),
580
+ children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Strikethrough, { size: 16 })
581
+ },
582
+ "strike"
583
+ ),
584
+ config.inline.buttons.code && /* @__PURE__ */ jsxRuntime.jsx(
585
+ TbButton,
586
+ {
587
+ label: "Inline code",
588
+ active: state.code,
589
+ disabled,
590
+ onClick: () => editor.chain().focus().toggleCode().run(),
591
+ children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Code, { size: 16 })
592
+ },
593
+ "code"
594
+ )
595
+ ] : [];
596
+ const headings = config.headings.enabled ? [2, 3, 4, 5, 6].map(
597
+ (level) => config.headings.buttons[`h${level}`] ? /* @__PURE__ */ jsxRuntime.jsx(
598
+ TbButton,
599
+ {
600
+ label: `Heading ${level}`,
601
+ active: headingActive[level],
602
+ disabled,
603
+ onClick: () => editor.chain().focus().toggleHeading({ level }).run(),
604
+ children: HEADING_ICONS[level]
605
+ },
606
+ `h${level}`
607
+ ) : false
608
+ ) : [];
609
+ const lists = config.lists.enabled ? [
610
+ config.lists.buttons.bullet && /* @__PURE__ */ jsxRuntime.jsx(
611
+ TbButton,
612
+ {
613
+ label: "Bullet list",
614
+ active: state.bulletList,
615
+ disabled,
616
+ onClick: () => editor.chain().focus().toggleBulletList().run(),
617
+ children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.List, { size: 16 })
618
+ },
619
+ "bullet"
620
+ ),
621
+ config.lists.buttons.ordered && /* @__PURE__ */ jsxRuntime.jsx(
622
+ TbButton,
623
+ {
624
+ label: "Ordered list",
625
+ active: state.orderedList,
626
+ disabled,
627
+ onClick: () => editor.chain().focus().toggleOrderedList().run(),
628
+ children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.ListOrdered, { size: 16 })
629
+ },
630
+ "ordered"
631
+ ),
632
+ config.lists.buttons.blockquote && /* @__PURE__ */ jsxRuntime.jsx(
633
+ TbButton,
634
+ {
635
+ label: "Quote",
636
+ active: state.blockquote,
637
+ disabled,
638
+ onClick: () => editor.chain().focus().toggleBlockquote().run(),
639
+ children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Quote, { size: 16 })
640
+ },
641
+ "quote"
642
+ )
643
+ ] : [];
644
+ const align = config.align.enabled ? [
645
+ config.align.buttons.left && /* @__PURE__ */ jsxRuntime.jsx(
646
+ TbButton,
647
+ {
648
+ label: "Align left",
649
+ active: state.alignLeft,
650
+ disabled,
651
+ onClick: () => editor.chain().focus().setTextAlign("left").run(),
652
+ children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.AlignLeft, { size: 16 })
653
+ },
654
+ "left"
655
+ ),
656
+ config.align.buttons.center && /* @__PURE__ */ jsxRuntime.jsx(
657
+ TbButton,
658
+ {
659
+ label: "Align center",
660
+ active: state.alignCenter,
661
+ disabled,
662
+ onClick: () => editor.chain().focus().setTextAlign("center").run(),
663
+ children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.AlignCenter, { size: 16 })
664
+ },
665
+ "center"
666
+ ),
667
+ config.align.buttons.right && /* @__PURE__ */ jsxRuntime.jsx(
668
+ TbButton,
669
+ {
670
+ label: "Align right",
671
+ active: state.alignRight,
672
+ disabled,
673
+ onClick: () => editor.chain().focus().setTextAlign("right").run(),
674
+ children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.AlignRight, { size: 16 })
675
+ },
676
+ "right"
677
+ )
678
+ ] : [];
679
+ const inserts = [
680
+ config.link && /* @__PURE__ */ jsxRuntime.jsx(
681
+ TbButton,
682
+ {
683
+ label: "Link",
684
+ active: state.link,
685
+ disabled,
686
+ onClick: onLink,
687
+ children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Link, { size: 16 })
688
+ },
689
+ "link"
690
+ ),
691
+ config.image && /* @__PURE__ */ jsxRuntime.jsx(
692
+ TbButton,
693
+ {
694
+ label: "Image",
695
+ active: state.image,
696
+ disabled,
697
+ onClick: onImage,
698
+ children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Image, { size: 16 })
699
+ },
700
+ "image"
701
+ ),
702
+ config.button && /* @__PURE__ */ jsxRuntime.jsx(
703
+ TbButton,
704
+ {
705
+ label: "Button",
706
+ disabled,
707
+ onClick: onButton,
708
+ children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.MousePointerClick, { size: 16 })
709
+ },
710
+ "button"
711
+ )
712
+ ];
713
+ const blocks = config.blocks.enabled ? [
714
+ config.blocks.buttons.paragraph && /* @__PURE__ */ jsxRuntime.jsx(
715
+ TbButton,
716
+ {
717
+ label: "Insert paragraph",
718
+ disabled,
719
+ onClick: onInsertParagraph,
720
+ children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Pilcrow, { size: 16 })
721
+ },
722
+ "paragraph"
723
+ ),
724
+ config.blocks.buttons.divider && /* @__PURE__ */ jsxRuntime.jsx(
725
+ TbButton,
726
+ {
727
+ label: "Insert divider",
728
+ disabled,
729
+ onClick: onInsertDivider,
730
+ children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Minus, { size: 16 })
731
+ },
732
+ "divider"
733
+ ),
734
+ config.blocks.buttons.footer && /* @__PURE__ */ jsxRuntime.jsx(
735
+ TbButton,
736
+ {
737
+ label: "Insert footer",
738
+ disabled,
739
+ onClick: onInsertFooter,
740
+ children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.PanelBottom, { size: 16 })
741
+ },
742
+ "footer"
743
+ )
744
+ ] : [];
745
+ const html = config.html ? [
746
+ /* @__PURE__ */ jsxRuntime.jsx(
747
+ TbButton,
748
+ {
749
+ label: "HTML source",
750
+ active: htmlMode,
751
+ onClick: onToggleHtml,
752
+ children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.FileCode, { size: 16 })
753
+ },
754
+ "html"
755
+ )
756
+ ] : [];
757
+ const groups = [inline, headings, lists, align, inserts, blocks, html].map((g) => g.filter(Boolean)).filter((g) => g.length > 0);
758
+ return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "rte-toolbar", role: "toolbar", "aria-label": "Formatting", children: groups.map((buttons, i) => /* @__PURE__ */ jsxRuntime.jsxs(react$1.Fragment, { children: [
759
+ i > 0 && /* @__PURE__ */ jsxRuntime.jsx(Divider, {}),
760
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "rte-tb-group", children: buttons })
761
+ ] }, i)) });
762
+ }
763
+ var RichTextEditor = react$1.forwardRef(function RichTextEditor2({
764
+ value,
765
+ onChange,
766
+ placeholder = "Write your email\u2026",
767
+ onUploadImage,
768
+ className,
769
+ editorStyle,
770
+ toolbar
771
+ }, ref) {
772
+ const toolbarConfig = react$1.useMemo(
773
+ () => resolveToolbarConfig(toolbar),
774
+ [toolbar]
775
+ );
776
+ const extensions = react$1.useMemo(
777
+ () => createEmailExtensions(placeholder),
778
+ [placeholder]
779
+ );
780
+ const fileInputRef = react$1.useRef(null);
781
+ const [htmlMode, setHtmlMode] = react$1.useState(false);
782
+ const [buttonDialogOpen, setButtonDialogOpen] = react$1.useState(false);
783
+ const editor = react.useEditor({
784
+ extensions,
785
+ content: value,
786
+ // REQUIRED for SSR/Next.js — rendering immediately causes a hydration
787
+ // mismatch because the server has no DOM.
788
+ immediatelyRender: false,
789
+ editorProps: {
790
+ attributes: {
791
+ class: "rte-content"
792
+ }
793
+ },
794
+ onUpdate: ({ editor: e }) => {
795
+ onChange(e.getHTML());
796
+ }
797
+ });
798
+ if (editor && !htmlMode && value !== editor.getHTML()) {
799
+ editor.commands.setContent(value, { emitUpdate: false });
800
+ }
801
+ react$1.useImperativeHandle(
802
+ ref,
803
+ () => ({
804
+ insertAtCaret: (htmlOrText) => {
805
+ editor?.chain().focus().insertContent(htmlOrText).run();
806
+ }
807
+ }),
808
+ [editor]
809
+ );
810
+ const insertSnippet = react$1.useCallback(
811
+ (snippet) => {
812
+ editor?.chain().focus().insertContent(snippet).run();
813
+ },
814
+ [editor]
815
+ );
816
+ const handleLink = react$1.useCallback(() => {
817
+ if (!editor) return;
818
+ const previous = editor.getAttributes("link").href;
819
+ const url = window.prompt("Link URL", previous ?? "https://");
820
+ if (url === null) return;
821
+ if (url.trim() === "") {
822
+ editor.chain().focus().extendMarkRange("link").unsetLink().run();
823
+ return;
824
+ }
825
+ editor.chain().focus().extendMarkRange("link").setLink({ href: url.trim() }).run();
826
+ }, [editor]);
827
+ const handleImageClick = react$1.useCallback(() => {
828
+ if (!editor) return;
829
+ if (onUploadImage) {
830
+ fileInputRef.current?.click();
831
+ } else {
832
+ const url = window.prompt("Image URL", "https://");
833
+ if (url && url.trim()) {
834
+ editor.chain().focus().setImage({ src: url.trim() }).run();
835
+ }
836
+ }
837
+ }, [editor, onUploadImage]);
838
+ const handleFileSelected = react$1.useCallback(
839
+ async (e) => {
840
+ const file = e.target.files?.[0];
841
+ e.target.value = "";
842
+ if (!file || !onUploadImage || !editor) return;
843
+ try {
844
+ const src = await onUploadImage(file);
845
+ if (src) editor.chain().focus().setImage({ src }).run();
846
+ } catch (err) {
847
+ console.error("dpk-editor: image upload failed", err);
848
+ }
849
+ },
850
+ [editor, onUploadImage]
851
+ );
852
+ const handleButtonConfirm = react$1.useCallback(
853
+ (html) => {
854
+ setButtonDialogOpen(false);
855
+ editor?.chain().focus().insertContent(html).run();
856
+ },
857
+ [editor]
858
+ );
859
+ if (!editor) return null;
860
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "rte-root" + (className ? ` ${className}` : ""), children: [
861
+ /* @__PURE__ */ jsxRuntime.jsx(
862
+ Toolbar,
863
+ {
864
+ editor,
865
+ config: toolbarConfig,
866
+ htmlMode,
867
+ onToggleHtml: () => setHtmlMode((m) => !m),
868
+ onLink: handleLink,
869
+ onImage: handleImageClick,
870
+ onButton: () => setButtonDialogOpen(true),
871
+ onInsertParagraph: () => insertSnippet(PRESET_PARAGRAPH),
872
+ onInsertDivider: () => insertSnippet(PRESET_DIVIDER),
873
+ onInsertFooter: () => insertSnippet(PRESET_FOOTER)
874
+ }
875
+ ),
876
+ htmlMode ? /* @__PURE__ */ jsxRuntime.jsx(
877
+ "textarea",
878
+ {
879
+ className: "rte-source",
880
+ style: editorStyle,
881
+ value,
882
+ spellCheck: false,
883
+ onChange: (e) => onChange(e.target.value),
884
+ "aria-label": "HTML source"
885
+ }
886
+ ) : /* @__PURE__ */ jsxRuntime.jsx(
887
+ react.EditorContent,
888
+ {
889
+ editor,
890
+ className: "rte-editor",
891
+ style: editorStyle
892
+ }
893
+ ),
894
+ onUploadImage && /* @__PURE__ */ jsxRuntime.jsx(
895
+ "input",
896
+ {
897
+ ref: fileInputRef,
898
+ type: "file",
899
+ accept: "image/*",
900
+ hidden: true,
901
+ onChange: handleFileSelected
902
+ }
903
+ ),
904
+ /* @__PURE__ */ jsxRuntime.jsx(
905
+ EmailButtonDialog,
906
+ {
907
+ open: buttonDialogOpen,
908
+ onConfirm: handleButtonConfirm,
909
+ onClose: () => setButtonDialogOpen(false)
910
+ }
911
+ )
912
+ ] });
913
+ });
914
+ function EmailEditor({
915
+ value,
916
+ onChange,
917
+ placeholders,
918
+ onUploadImage,
919
+ minHeight = 288,
920
+ placeholder,
921
+ toolbar,
922
+ className
923
+ }) {
924
+ const shell = react$1.useMemo(() => splitEmailHtml(value), [value]);
925
+ const shellRef = react$1.useRef(shell);
926
+ react$1.useEffect(() => {
927
+ shellRef.current = shell;
928
+ }, [shell]);
929
+ const editorRef = react$1.useRef(null);
930
+ const handleBodyChange = react$1.useCallback(
931
+ (newBody) => {
932
+ onChange(joinEmailHtml(shellRef.current, newBody));
933
+ },
934
+ [onChange]
935
+ );
936
+ const editorStyle = react$1.useMemo(
937
+ () => ({ minHeight }),
938
+ [minHeight]
939
+ );
940
+ const insertToken = react$1.useCallback((token) => {
941
+ editorRef.current?.insertAtCaret(token);
942
+ }, []);
943
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "rte-email" + (className ? ` ${className}` : ""), children: [
944
+ placeholders && placeholders.length > 0 && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "rte-chips", role: "group", "aria-label": "Merge tokens", children: placeholders.map((p) => /* @__PURE__ */ jsxRuntime.jsx(
945
+ "button",
946
+ {
947
+ type: "button",
948
+ className: "rte-chip",
949
+ title: `Insert ${p.token}`,
950
+ onMouseDown: (e) => e.preventDefault(),
951
+ onClick: () => insertToken(p.token),
952
+ children: p.label
953
+ },
954
+ p.token
955
+ )) }),
956
+ /* @__PURE__ */ jsxRuntime.jsx(
957
+ RichTextEditor,
958
+ {
959
+ ref: editorRef,
960
+ value: shell.body,
961
+ onChange: handleBodyChange,
962
+ onUploadImage,
963
+ placeholder,
964
+ editorStyle,
965
+ toolbar
966
+ }
967
+ )
968
+ ] });
969
+ }
970
+ var EmailEditor_default = EmailEditor;
971
+
972
+ exports.EmailButtonDialog = EmailButtonDialog;
973
+ exports.EmailEditor = EmailEditor;
974
+ exports.PRESET_DIVIDER = PRESET_DIVIDER;
975
+ exports.PRESET_FOOTER = PRESET_FOOTER;
976
+ exports.PRESET_HEADING = PRESET_HEADING;
977
+ exports.PRESET_PARAGRAPH = PRESET_PARAGRAPH;
978
+ exports.PreserveStyles = PreserveStyles;
979
+ exports.RichTextEditor = RichTextEditor;
980
+ exports.buildButtonHtml = buildButtonHtml;
981
+ exports.createEmailExtensions = createEmailExtensions;
982
+ exports.default = EmailEditor_default;
983
+ exports.escapeHtml = escapeHtml;
984
+ exports.extractEmailBody = extractEmailBody;
985
+ exports.joinEmailHtml = joinEmailHtml;
986
+ exports.presets = presets;
987
+ exports.resolveToolbarConfig = resolveToolbarConfig;
988
+ exports.splitEmailHtml = splitEmailHtml;
989
+ //# sourceMappingURL=index.cjs.map
990
+ //# sourceMappingURL=index.cjs.map