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