@react-email/editor 0.0.0-experimental.1 → 0.0.0-experimental.10

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.mjs CHANGED
@@ -1,12 +1,19 @@
1
- import { Node, findChildren, mergeAttributes } from "@tiptap/core";
2
- import { jsx } from "react/jsx-runtime";
1
+ import { Extension, Mark, Node, findChildren, markInputRule, markPasteRule, mergeAttributes } from "@tiptap/core";
2
+ import { StarterKit } from "@tiptap/starter-kit";
3
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
3
4
  import * as ReactEmailComponents from "@react-email/components";
4
5
  import { Button as Button$1, CodeBlock, Column, Row, Section as Section$1 } from "@react-email/components";
5
6
  import CodeBlock$1 from "@tiptap/extension-code-block";
6
- import { Plugin, PluginKey } from "@tiptap/pm/state";
7
+ import { Plugin, PluginKey, TextSelection } from "@tiptap/pm/state";
7
8
  import { Decoration, DecorationSet } from "@tiptap/pm/view";
8
9
  import { fromHtml } from "hast-util-from-html";
9
10
  import Prism from "prismjs";
11
+ import TipTapPlaceholder from "@tiptap/extension-placeholder";
12
+ import { useCurrentEditor, useEditorState } from "@tiptap/react";
13
+ import { AlignCenterIcon, AlignLeftIcon, AlignRightIcon, BoldIcon, CaseUpperIcon, Check, ChevronDown, Code, CodeIcon, ExternalLinkIcon, Heading1, Heading2, Heading3, ItalicIcon, LinkIcon, List, ListOrdered, PencilIcon, StrikethroughIcon, TextIcon, TextQuote, UnderlineIcon, UnlinkIcon } from "lucide-react";
14
+ import * as React from "react";
15
+ import * as Popover from "@radix-ui/react-popover";
16
+ import { BubbleMenu as BubbleMenu$1 } from "@tiptap/react/menus";
10
17
 
11
18
  //#region src/core/email-node.ts
12
19
  var EmailNode = class EmailNode extends Node {
@@ -38,6 +45,103 @@ var EmailNode = class EmailNode extends Node {
38
45
  }
39
46
  };
40
47
 
48
+ //#endregion
49
+ //#region src/core/event-bus.ts
50
+ const EVENT_PREFIX = "@react-email/editor:";
51
+ var EditorEventBus = class {
52
+ prefixEventName(eventName) {
53
+ return `${EVENT_PREFIX}${String(eventName)}`;
54
+ }
55
+ dispatch(eventName, payload, options) {
56
+ const target = options?.target ?? window;
57
+ const prefixedEventName = this.prefixEventName(eventName);
58
+ const event = new CustomEvent(prefixedEventName, {
59
+ detail: payload,
60
+ bubbles: false,
61
+ cancelable: false
62
+ });
63
+ target.dispatchEvent(event);
64
+ }
65
+ on(eventName, handler, options) {
66
+ const target = options?.target ?? window;
67
+ const prefixedEventName = this.prefixEventName(eventName);
68
+ const abortController = new AbortController();
69
+ const wrappedHandler = (event) => {
70
+ const customEvent = event;
71
+ const result = handler(customEvent.detail);
72
+ if (result instanceof Promise) result.catch((error) => {
73
+ console.error(`Error in async event handler for ${prefixedEventName}:`, {
74
+ event: customEvent.detail,
75
+ error
76
+ });
77
+ });
78
+ };
79
+ target.addEventListener(prefixedEventName, wrappedHandler, {
80
+ ...options,
81
+ signal: abortController.signal
82
+ });
83
+ return { unsubscribe: () => {
84
+ abortController.abort();
85
+ } };
86
+ }
87
+ };
88
+ const editorEventBus = new EditorEventBus();
89
+
90
+ //#endregion
91
+ //#region src/extensions/alignment-attribute.tsx
92
+ const AlignmentAttribute = Extension.create({
93
+ name: "alignmentAttribute",
94
+ addOptions() {
95
+ return {
96
+ types: [],
97
+ alignments: [
98
+ "left",
99
+ "center",
100
+ "right",
101
+ "justify"
102
+ ]
103
+ };
104
+ },
105
+ addGlobalAttributes() {
106
+ return [{
107
+ types: this.options.types,
108
+ attributes: { alignment: {
109
+ parseHTML: (element) => {
110
+ const explicitAlign = element.getAttribute("align") || element.getAttribute("alignment") || element.style.textAlign;
111
+ if (explicitAlign && this.options.alignments.includes(explicitAlign)) return explicitAlign;
112
+ return null;
113
+ },
114
+ renderHTML: (attributes) => {
115
+ if (attributes.alignment === "left") return {};
116
+ return { alignment: attributes.alignment };
117
+ }
118
+ } }
119
+ }];
120
+ },
121
+ addCommands() {
122
+ return { setAlignment: (alignment) => ({ commands }) => {
123
+ if (!this.options.alignments.includes(alignment)) return false;
124
+ return this.options.types.every((type) => commands.updateAttributes(type, { alignment }));
125
+ } };
126
+ },
127
+ addKeyboardShortcuts() {
128
+ return {
129
+ Enter: () => {
130
+ const { from } = this.editor.state.selection;
131
+ const currentAlignment = this.editor.state.doc.nodeAt(from)?.attrs?.alignment;
132
+ if (currentAlignment) requestAnimationFrame(() => {
133
+ this.editor.commands.setAlignment(currentAlignment);
134
+ });
135
+ return false;
136
+ },
137
+ "Mod-Shift-l": () => this.editor.commands.setAlignment("left"),
138
+ "Mod-Shift-e": () => this.editor.commands.setAlignment("center"),
139
+ "Mod-Shift-r": () => this.editor.commands.setAlignment("right"),
140
+ "Mod-Shift-j": () => this.editor.commands.setAlignment("justify")
141
+ };
142
+ }
143
+ });
144
+
41
145
  //#endregion
42
146
  //#region src/utils/attribute-helpers.ts
43
147
  /**
@@ -94,6 +198,14 @@ const LAYOUT_ATTRIBUTES = [
94
198
  "height"
95
199
  ];
96
200
  /**
201
+ * Table-specific HTML attributes used for table layout and styling.
202
+ */
203
+ const TABLE_ATTRIBUTES = [
204
+ "border",
205
+ "cellpadding",
206
+ "cellspacing"
207
+ ];
208
+ /**
97
209
  * Table cell-specific HTML attributes.
98
210
  */
99
211
  const TABLE_CELL_ATTRIBUTES = [
@@ -110,6 +222,7 @@ const TABLE_HEADER_ATTRIBUTES = [...TABLE_CELL_ATTRIBUTES, "scope"];
110
222
 
111
223
  //#endregion
112
224
  //#region src/utils/styles.ts
225
+ const WHITE_SPACE_REGEX = /\s+/;
113
226
  const inlineCssToJs = (inlineStyle, options = {}) => {
114
227
  const styleObject = {};
115
228
  if (!inlineStyle || inlineStyle === "" || typeof inlineStyle === "object") return styleObject;
@@ -124,6 +237,157 @@ const inlineCssToJs = (inlineStyle, options = {}) => {
124
237
  });
125
238
  return styleObject;
126
239
  };
240
+ /**
241
+ * Expands CSS shorthand properties (margin, padding) into their longhand equivalents.
242
+ * This prevents shorthand properties from overriding specific longhand properties in email clients.
243
+ *
244
+ * @param styles - Style object that may contain shorthand properties
245
+ * @returns New style object with shorthand properties expanded to longhand
246
+ *
247
+ * @example
248
+ * expandShorthandProperties({ margin: '0', paddingTop: '10px' })
249
+ * // Returns: { marginTop: '0', marginRight: '0', marginBottom: '0', marginLeft: '0', paddingTop: '10px' }
250
+ */
251
+ function expandShorthandProperties(styles) {
252
+ if (!styles || typeof styles !== "object") return {};
253
+ const expanded = {};
254
+ for (const key in styles) {
255
+ const value = styles[key];
256
+ if (value === void 0 || value === null || value === "") continue;
257
+ switch (key) {
258
+ case "margin": {
259
+ const values = parseShorthandValue(value);
260
+ expanded.marginTop = values.top;
261
+ expanded.marginRight = values.right;
262
+ expanded.marginBottom = values.bottom;
263
+ expanded.marginLeft = values.left;
264
+ break;
265
+ }
266
+ case "padding": {
267
+ const values = parseShorthandValue(value);
268
+ expanded.paddingTop = values.top;
269
+ expanded.paddingRight = values.right;
270
+ expanded.paddingBottom = values.bottom;
271
+ expanded.paddingLeft = values.left;
272
+ break;
273
+ }
274
+ case "border": {
275
+ const values = convertBorderValue(value);
276
+ expanded.borderStyle = values.style;
277
+ expanded.borderWidth = values.width;
278
+ expanded.borderColor = values.color;
279
+ break;
280
+ }
281
+ case "borderTopLeftRadius":
282
+ case "borderTopRightRadius":
283
+ case "borderBottomLeftRadius":
284
+ case "borderBottomRightRadius":
285
+ expanded[key] = value;
286
+ if (styles.borderTopLeftRadius && styles.borderTopRightRadius && styles.borderBottomLeftRadius && styles.borderBottomRightRadius) {
287
+ const values = [
288
+ styles.borderTopLeftRadius,
289
+ styles.borderTopRightRadius,
290
+ styles.borderBottomLeftRadius,
291
+ styles.borderBottomRightRadius
292
+ ];
293
+ if (new Set(values).size === 1) expanded.borderRadius = values[0];
294
+ }
295
+ break;
296
+ default: expanded[key] = value;
297
+ }
298
+ }
299
+ return expanded;
300
+ }
301
+ /**
302
+ * Parses CSS shorthand value (1-4 values) into individual side values.
303
+ * Follows CSS specification for shorthand property value parsing.
304
+ *
305
+ * @param value - Shorthand value string (e.g., '0', '10px 20px', '5px 10px 15px 20px')
306
+ * @returns Object with top, right, bottom, left values
307
+ */
308
+ function parseShorthandValue(value) {
309
+ const stringValue = String(value).trim();
310
+ const parts = stringValue.split(WHITE_SPACE_REGEX);
311
+ const len = parts.length;
312
+ if (len === 1) return {
313
+ top: parts[0],
314
+ right: parts[0],
315
+ bottom: parts[0],
316
+ left: parts[0]
317
+ };
318
+ if (len === 2) return {
319
+ top: parts[0],
320
+ right: parts[1],
321
+ bottom: parts[0],
322
+ left: parts[1]
323
+ };
324
+ if (len === 3) return {
325
+ top: parts[0],
326
+ right: parts[1],
327
+ bottom: parts[2],
328
+ left: parts[1]
329
+ };
330
+ if (len === 4) return {
331
+ top: parts[0],
332
+ right: parts[1],
333
+ bottom: parts[2],
334
+ left: parts[3]
335
+ };
336
+ return {
337
+ top: stringValue,
338
+ right: stringValue,
339
+ bottom: stringValue,
340
+ left: stringValue
341
+ };
342
+ }
343
+ function convertBorderValue(value) {
344
+ const stringValue = String(value).trim();
345
+ const parts = stringValue.split(WHITE_SPACE_REGEX);
346
+ switch (parts.length) {
347
+ case 1: return {
348
+ style: "solid",
349
+ width: parts[0],
350
+ color: "black"
351
+ };
352
+ case 2: return {
353
+ style: parts[1],
354
+ width: parts[0],
355
+ color: "black"
356
+ };
357
+ case 3: return {
358
+ style: parts[1],
359
+ width: parts[0],
360
+ color: parts[2]
361
+ };
362
+ case 4: return {
363
+ style: parts[1],
364
+ width: parts[0],
365
+ color: parts[2]
366
+ };
367
+ default: return {
368
+ style: "solid",
369
+ width: stringValue,
370
+ color: "black"
371
+ };
372
+ }
373
+ }
374
+ /**
375
+ * Resolves conflicts between reset styles and inline styles by expanding
376
+ * shorthand properties (margin, padding) to longhand before merging.
377
+ * This prevents shorthand properties from overriding specific longhand properties.
378
+ *
379
+ * @param resetStyles - Base reset styles that may contain shorthand properties
380
+ * @param inlineStyles - Inline styles that should override reset styles
381
+ * @returns Merged styles with inline styles taking precedence
382
+ */
383
+ function resolveConflictingStyles(resetStyles, inlineStyles) {
384
+ const expandedResetStyles = expandShorthandProperties(resetStyles);
385
+ const expandedInlineStyles = expandShorthandProperties(inlineStyles);
386
+ return {
387
+ ...expandedResetStyles,
388
+ ...expandedInlineStyles
389
+ };
390
+ }
127
391
 
128
392
  //#endregion
129
393
  //#region src/extensions/body.tsx
@@ -157,12 +421,12 @@ const Body = EmailNode.create({
157
421
  0
158
422
  ];
159
423
  },
160
- renderToReactEmail({ children, node, styles }) {
424
+ renderToReactEmail({ children, node, style }) {
161
425
  const inlineStyles = inlineCssToJs(node.attrs?.style);
162
426
  return /* @__PURE__ */ jsx("div", {
163
427
  className: node.attrs?.class || void 0,
164
428
  style: {
165
- ...styles.reset,
429
+ ...style,
166
430
  ...inlineStyles
167
431
  },
168
432
  children
@@ -170,6 +434,92 @@ const Body = EmailNode.create({
170
434
  }
171
435
  });
172
436
 
437
+ //#endregion
438
+ //#region src/extensions/bold.ts
439
+ /**
440
+ * Matches bold text via `**` as input.
441
+ */
442
+ const starInputRegex = /(?:^|\s)(\*\*(?!\s+\*\*)((?:[^*]+))\*\*(?!\s+\*\*))$/;
443
+ /**
444
+ * Matches bold text via `**` while pasting.
445
+ */
446
+ const starPasteRegex = /(?:^|\s)(\*\*(?!\s+\*\*)((?:[^*]+))\*\*(?!\s+\*\*))/g;
447
+ /**
448
+ * Matches bold text via `__` as input.
449
+ */
450
+ const underscoreInputRegex = /(?:^|\s)(__(?!\s+__)((?:[^_]+))__(?!\s+__))$/;
451
+ /**
452
+ * Matches bold text via `__` while pasting.
453
+ */
454
+ const underscorePasteRegex = /(?:^|\s)(__(?!\s+__)((?:[^_]+))__(?!\s+__))/g;
455
+ /**
456
+ * This extension allows you to mark text as bold.
457
+ * @see https://tiptap.dev/api/marks/bold
458
+ */
459
+ const Bold = Mark.create({
460
+ name: "bold",
461
+ addOptions() {
462
+ return { HTMLAttributes: {} };
463
+ },
464
+ parseHTML() {
465
+ return [
466
+ { tag: "strong" },
467
+ {
468
+ tag: "b",
469
+ getAttrs: (node) => node.style.fontWeight !== "normal" && null
470
+ },
471
+ {
472
+ style: "font-weight=400",
473
+ clearMark: (mark) => mark.type.name === this.name
474
+ }
475
+ ];
476
+ },
477
+ renderHTML({ HTMLAttributes }) {
478
+ return [
479
+ "strong",
480
+ mergeAttributes(this.options.HTMLAttributes, HTMLAttributes),
481
+ 0
482
+ ];
483
+ },
484
+ addCommands() {
485
+ return {
486
+ setBold: () => ({ commands }) => {
487
+ return commands.setMark(this.name);
488
+ },
489
+ toggleBold: () => ({ commands }) => {
490
+ return commands.toggleMark(this.name);
491
+ },
492
+ unsetBold: () => ({ commands }) => {
493
+ return commands.unsetMark(this.name);
494
+ }
495
+ };
496
+ },
497
+ addKeyboardShortcuts() {
498
+ return {
499
+ "Mod-b": () => this.editor.commands.toggleBold(),
500
+ "Mod-B": () => this.editor.commands.toggleBold()
501
+ };
502
+ },
503
+ addInputRules() {
504
+ return [markInputRule({
505
+ find: starInputRegex,
506
+ type: this.type
507
+ }), markInputRule({
508
+ find: underscoreInputRegex,
509
+ type: this.type
510
+ })];
511
+ },
512
+ addPasteRules() {
513
+ return [markPasteRule({
514
+ find: starPasteRegex,
515
+ type: this.type
516
+ }), markPasteRule({
517
+ find: underscorePasteRegex,
518
+ type: this.type
519
+ })];
520
+ }
521
+ });
522
+
173
523
  //#endregion
174
524
  //#region src/extensions/button.tsx
175
525
  const Button = EmailNode.create({
@@ -232,7 +582,7 @@ const Button = EmailNode.create({
232
582
  }
233
583
  };
234
584
  },
235
- renderToReactEmail({ children, node, styles }) {
585
+ renderToReactEmail({ children, node, style }) {
236
586
  const inlineStyles = inlineCssToJs(node.attrs?.style);
237
587
  return /* @__PURE__ */ jsx(Row, { children: /* @__PURE__ */ jsx(Column, {
238
588
  align: node.attrs?.align || node.attrs?.alignment,
@@ -240,8 +590,7 @@ const Button = EmailNode.create({
240
590
  className: node.attrs?.class || void 0,
241
591
  href: node.attrs?.href,
242
592
  style: {
243
- ...styles.reset,
244
- ...styles.button,
593
+ ...style,
245
594
  ...inlineStyles
246
595
  },
247
596
  children
@@ -250,6 +599,48 @@ const Button = EmailNode.create({
250
599
  }
251
600
  });
252
601
 
602
+ //#endregion
603
+ //#region src/extensions/class-attribute.tsx
604
+ const ClassAttribute = Extension.create({
605
+ name: "classAttribute",
606
+ addOptions() {
607
+ return {
608
+ types: [],
609
+ class: []
610
+ };
611
+ },
612
+ addGlobalAttributes() {
613
+ return [{
614
+ types: this.options.types,
615
+ attributes: { class: {
616
+ default: "",
617
+ parseHTML: (element) => element.className || "",
618
+ renderHTML: (attributes) => {
619
+ return attributes.class ? { class: attributes.class } : {};
620
+ }
621
+ } }
622
+ }];
623
+ },
624
+ addCommands() {
625
+ return {
626
+ unsetClass: () => ({ commands }) => {
627
+ return this.options.types.every((type) => commands.resetAttributes(type, "class"));
628
+ },
629
+ setClass: (classList) => ({ commands }) => {
630
+ return this.options.types.every((type) => commands.updateAttributes(type, { class: classList }));
631
+ }
632
+ };
633
+ },
634
+ addKeyboardShortcuts() {
635
+ return { Enter: ({ editor }) => {
636
+ requestAnimationFrame(() => {
637
+ editor.commands.resetAttributes("paragraph", "class");
638
+ });
639
+ return false;
640
+ } };
641
+ }
642
+ });
643
+
253
644
  //#endregion
254
645
  //#region src/utils/prism-utils.ts
255
646
  const publicURL = "/styles/prism";
@@ -428,6 +819,25 @@ const CodeBlockPrism = EmailNode.from(CodeBlock$1.extend({
428
819
  ]
429
820
  ];
430
821
  },
822
+ addKeyboardShortcuts() {
823
+ return {
824
+ ...this.parent?.(),
825
+ "Mod-a": ({ editor }) => {
826
+ const { state } = editor;
827
+ const { selection } = state;
828
+ const { $from } = selection;
829
+ for (let depth = $from.depth; depth >= 1; depth--) if ($from.node(depth).type.name === this.name) {
830
+ const blockStart = $from.start(depth);
831
+ const blockEnd = $from.end(depth);
832
+ if (selection.from === blockStart && selection.to === blockEnd) return false;
833
+ const tr = state.tr.setSelection(TextSelection.create(state.doc, blockStart, blockEnd));
834
+ editor.view.dispatch(tr);
835
+ return true;
836
+ }
837
+ return false;
838
+ }
839
+ };
840
+ },
431
841
  addProseMirrorPlugins() {
432
842
  return [...this.parent?.() || [], PrismPlugin({
433
843
  name: this.name,
@@ -435,7 +845,7 @@ const CodeBlockPrism = EmailNode.from(CodeBlock$1.extend({
435
845
  defaultTheme: this.options.defaultTheme
436
846
  })];
437
847
  }
438
- }), ({ node, styles }) => {
848
+ }), ({ node, style }) => {
439
849
  const language = node.attrs?.language ? `${node.attrs.language}` : "javascript";
440
850
  const userTheme = ReactEmailComponents[node.attrs?.theme];
441
851
  const theme = userTheme ? {
@@ -459,7 +869,7 @@ const CodeBlockPrism = EmailNode.from(CodeBlock$1.extend({
459
869
  theme,
460
870
  style: {
461
871
  width: "auto",
462
- ...styles.codeBlock
872
+ ...style
463
873
  }
464
874
  });
465
875
  });
@@ -496,12 +906,12 @@ const Div = EmailNode.create({
496
906
  addAttributes() {
497
907
  return { ...createStandardAttributes([...COMMON_HTML_ATTRIBUTES, ...LAYOUT_ATTRIBUTES]) };
498
908
  },
499
- renderToReactEmail({ children, node, styles }) {
909
+ renderToReactEmail({ children, node, style }) {
500
910
  const inlineStyles = inlineCssToJs(node.attrs?.style);
501
911
  return /* @__PURE__ */ jsx("div", {
502
912
  className: node.attrs?.class || void 0,
503
913
  style: {
504
- ...styles.reset,
914
+ ...style,
505
915
  ...inlineStyles
506
916
  },
507
917
  children
@@ -509,6 +919,206 @@ const Div = EmailNode.create({
509
919
  }
510
920
  });
511
921
 
922
+ //#endregion
923
+ //#region src/extensions/max-nesting.ts
924
+ const MaxNesting = Extension.create({
925
+ name: "maxNesting",
926
+ addOptions() {
927
+ return {
928
+ maxDepth: 3,
929
+ nodeTypes: void 0
930
+ };
931
+ },
932
+ addProseMirrorPlugins() {
933
+ const { maxDepth, nodeTypes } = this.options;
934
+ if (typeof maxDepth !== "number" || maxDepth < 1) throw new Error("maxDepth must be a positive number");
935
+ return [new Plugin({
936
+ key: new PluginKey("maxNesting"),
937
+ appendTransaction(transactions, _oldState, newState) {
938
+ if (!transactions.some((tr$1) => tr$1.docChanged)) return null;
939
+ const rangesToLift = [];
940
+ newState.doc.descendants((node, pos) => {
941
+ let depth = 0;
942
+ let currentPos = pos;
943
+ let currentNode = node;
944
+ while (currentNode && depth <= maxDepth) {
945
+ if (!nodeTypes || nodeTypes.includes(currentNode.type.name)) depth++;
946
+ const $pos = newState.doc.resolve(currentPos);
947
+ if ($pos.depth === 0) break;
948
+ currentPos = $pos.before($pos.depth);
949
+ currentNode = newState.doc.nodeAt(currentPos);
950
+ }
951
+ if (depth > maxDepth) {
952
+ const $pos = newState.doc.resolve(pos);
953
+ if ($pos.depth > 0) {
954
+ const range = $pos.blockRange();
955
+ if (range && "canReplace" in newState.schema.nodes.doc && typeof newState.schema.nodes.doc.canReplace === "function" && newState.schema.nodes.doc.canReplace(range.start - 1, range.end + 1, newState.doc.slice(range.start, range.end).content)) rangesToLift.push({
956
+ range,
957
+ target: range.start - 1
958
+ });
959
+ }
960
+ }
961
+ });
962
+ if (rangesToLift.length === 0) return null;
963
+ const tr = newState.tr;
964
+ for (let i = rangesToLift.length - 1; i >= 0; i--) {
965
+ const { range, target } = rangesToLift[i];
966
+ tr.lift(range, target);
967
+ }
968
+ return tr;
969
+ },
970
+ filterTransaction(tr) {
971
+ if (!tr.docChanged) return true;
972
+ let wouldCreateDeepNesting = false;
973
+ const newDoc = tr.doc;
974
+ newDoc.descendants((node, pos) => {
975
+ if (wouldCreateDeepNesting) return false;
976
+ let depth = 0;
977
+ let currentPos = pos;
978
+ let currentNode = node;
979
+ while (currentNode && depth <= maxDepth) {
980
+ if (!nodeTypes || nodeTypes.includes(currentNode.type.name)) depth++;
981
+ const $pos = newDoc.resolve(currentPos);
982
+ if ($pos.depth === 0) break;
983
+ currentPos = $pos.before($pos.depth);
984
+ currentNode = newDoc.nodeAt(currentPos);
985
+ }
986
+ if (depth > maxDepth) {
987
+ wouldCreateDeepNesting = true;
988
+ return false;
989
+ }
990
+ });
991
+ return !wouldCreateDeepNesting;
992
+ }
993
+ })];
994
+ }
995
+ });
996
+
997
+ //#endregion
998
+ //#region src/extensions/placeholder.ts
999
+ const Placeholder = TipTapPlaceholder.configure({
1000
+ placeholder: ({ node }) => {
1001
+ if (node.type.name === "heading") return `Heading ${node.attrs.level}`;
1002
+ return "Press '/' for commands";
1003
+ },
1004
+ includeChildren: true
1005
+ });
1006
+
1007
+ //#endregion
1008
+ //#region src/extensions/preserved-style.ts
1009
+ const PreservedStyle = Mark.create({
1010
+ name: "preservedStyle",
1011
+ addAttributes() {
1012
+ return { style: {
1013
+ default: null,
1014
+ parseHTML: (element) => element.getAttribute("style"),
1015
+ renderHTML: (attributes) => {
1016
+ if (!attributes.style) return {};
1017
+ return { style: attributes.style };
1018
+ }
1019
+ } };
1020
+ },
1021
+ parseHTML() {
1022
+ return [{
1023
+ tag: "span[style]",
1024
+ getAttrs: (element) => {
1025
+ if (typeof element === "string") return false;
1026
+ const style = element.getAttribute("style");
1027
+ if (style && hasPreservableStyles(style)) return { style };
1028
+ return false;
1029
+ }
1030
+ }];
1031
+ },
1032
+ renderHTML({ HTMLAttributes }) {
1033
+ return [
1034
+ "span",
1035
+ mergeAttributes(HTMLAttributes),
1036
+ 0
1037
+ ];
1038
+ }
1039
+ });
1040
+ const LINK_INDICATOR_STYLES = [
1041
+ "color",
1042
+ "text-decoration",
1043
+ "text-decoration-line",
1044
+ "text-decoration-color",
1045
+ "text-decoration-style"
1046
+ ];
1047
+ function parseStyleString(styleString) {
1048
+ const temp = document.createElement("div");
1049
+ temp.style.cssText = styleString;
1050
+ return temp.style;
1051
+ }
1052
+ function hasBackground(style) {
1053
+ const bgColor = style.backgroundColor;
1054
+ const bg = style.background;
1055
+ if (bgColor && bgColor !== "transparent" && bgColor !== "rgba(0, 0, 0, 0)") return true;
1056
+ if (bg && bg !== "transparent" && bg !== "none" && bg !== "rgba(0, 0, 0, 0)") return true;
1057
+ return false;
1058
+ }
1059
+ function hasPreservableStyles(styleString) {
1060
+ return processStylesForUnlink(styleString) !== null;
1061
+ }
1062
+ /**
1063
+ * Processes styles when unlinking:
1064
+ * - Has background (button-like): preserve all styles
1065
+ * - No background: strip link-indicator styles (color, text-decoration), keep the rest
1066
+ */
1067
+ function processStylesForUnlink(styleString) {
1068
+ if (!styleString) return null;
1069
+ const style = parseStyleString(styleString);
1070
+ if (hasBackground(style)) return styleString;
1071
+ const filtered = [];
1072
+ for (let i = 0; i < style.length; i++) {
1073
+ const prop = style[i];
1074
+ if (LINK_INDICATOR_STYLES.includes(prop)) continue;
1075
+ const value = style.getPropertyValue(prop);
1076
+ if (value) filtered.push(`${prop}: ${value}`);
1077
+ }
1078
+ return filtered.length > 0 ? filtered.join("; ") : null;
1079
+ }
1080
+
1081
+ //#endregion
1082
+ //#region src/extensions/preview-text.ts
1083
+ const PreviewText = Node.create({
1084
+ name: "previewText",
1085
+ group: "block",
1086
+ selectable: false,
1087
+ draggable: false,
1088
+ atom: true,
1089
+ addOptions() {
1090
+ return { HTMLAttributes: {} };
1091
+ },
1092
+ addStorage() {
1093
+ return { previewText: null };
1094
+ },
1095
+ renderHTML() {
1096
+ return ["div", { style: "display: none" }];
1097
+ },
1098
+ parseHTML() {
1099
+ return [{
1100
+ tag: "div[data-skip-in-text=\"true\"]",
1101
+ getAttrs: (node) => {
1102
+ if (typeof node === "string") return false;
1103
+ const element = node;
1104
+ let directText = "";
1105
+ for (const child of element.childNodes) if (child.nodeType === 3) directText += child.textContent || "";
1106
+ const cleanText = directText.trim();
1107
+ if (cleanText) this.storage.previewText = cleanText;
1108
+ return false;
1109
+ }
1110
+ }, {
1111
+ tag: "span.preheader",
1112
+ getAttrs: (node) => {
1113
+ if (typeof node === "string") return false;
1114
+ const preheaderText = node.textContent?.trim();
1115
+ if (preheaderText) this.storage.previewText = preheaderText;
1116
+ return false;
1117
+ }
1118
+ }];
1119
+ }
1120
+ });
1121
+
512
1122
  //#endregion
513
1123
  //#region src/utils/get-text-alignment.ts
514
1124
  function getTextAlignment(alignment) {
@@ -552,14 +1162,14 @@ const Section = EmailNode.create({
552
1162
  });
553
1163
  } };
554
1164
  },
555
- renderToReactEmail({ children, node, styles }) {
1165
+ renderToReactEmail({ children, node, style }) {
556
1166
  const inlineStyles = inlineCssToJs(node.attrs?.style);
557
1167
  const textAlign = node.attrs?.align || node.attrs?.alignment;
558
1168
  return /* @__PURE__ */ jsx(Section$1, {
559
1169
  className: node.attrs?.class || void 0,
560
1170
  align: textAlign,
561
1171
  style: {
562
- ...styles.section,
1172
+ ...style,
563
1173
  ...inlineStyles,
564
1174
  ...getTextAlignment(textAlign)
565
1175
  },
@@ -569,5 +1179,1489 @@ const Section = EmailNode.create({
569
1179
  });
570
1180
 
571
1181
  //#endregion
572
- export { Body, Button, CodeBlockPrism, Div, EmailNode, Section };
1182
+ //#region src/extensions/style-attribute.tsx
1183
+ const StyleAttribute = Extension.create({
1184
+ name: "styleAttribute",
1185
+ priority: 101,
1186
+ addOptions() {
1187
+ return {
1188
+ types: [],
1189
+ style: []
1190
+ };
1191
+ },
1192
+ addGlobalAttributes() {
1193
+ return [{
1194
+ types: this.options.types,
1195
+ attributes: { style: {
1196
+ default: "",
1197
+ parseHTML: (element) => element.getAttribute("style") || "",
1198
+ renderHTML: (attributes) => {
1199
+ return { style: attributes.style ?? "" };
1200
+ }
1201
+ } }
1202
+ }];
1203
+ },
1204
+ addCommands() {
1205
+ return {
1206
+ unsetStyle: () => ({ commands }) => {
1207
+ return this.options.types.every((type) => commands.resetAttributes(type, "style"));
1208
+ },
1209
+ setStyle: (style) => ({ commands }) => {
1210
+ return this.options.types.every((type) => commands.updateAttributes(type, { style }));
1211
+ }
1212
+ };
1213
+ },
1214
+ addKeyboardShortcuts() {
1215
+ return { Enter: ({ editor }) => {
1216
+ const { state } = editor.view;
1217
+ const { selection } = state;
1218
+ const { $from } = selection;
1219
+ const textBefore = $from.nodeBefore?.text || "";
1220
+ if (textBefore.includes("{{") || textBefore.includes("{{{")) return false;
1221
+ requestAnimationFrame(() => {
1222
+ editor.commands.resetAttributes("paragraph", "style");
1223
+ });
1224
+ return false;
1225
+ } };
1226
+ }
1227
+ });
1228
+
1229
+ //#endregion
1230
+ //#region src/extensions/sup.ts
1231
+ /**
1232
+ * This extension allows you to mark text as superscript.
1233
+ * @see https://tiptap.dev/api/marks/superscript
1234
+ */
1235
+ const Sup = Mark.create({
1236
+ name: "sup",
1237
+ addOptions() {
1238
+ return { HTMLAttributes: {} };
1239
+ },
1240
+ parseHTML() {
1241
+ return [{ tag: "sup" }];
1242
+ },
1243
+ renderHTML({ HTMLAttributes }) {
1244
+ return [
1245
+ "sup",
1246
+ mergeAttributes(this.options.HTMLAttributes, HTMLAttributes),
1247
+ 0
1248
+ ];
1249
+ },
1250
+ addCommands() {
1251
+ return {
1252
+ setSup: () => ({ commands }) => {
1253
+ return commands.setMark(this.name);
1254
+ },
1255
+ toggleSup: () => ({ commands }) => {
1256
+ return commands.toggleMark(this.name);
1257
+ },
1258
+ unsetSup: () => ({ commands }) => {
1259
+ return commands.unsetMark(this.name);
1260
+ }
1261
+ };
1262
+ }
1263
+ });
1264
+
1265
+ //#endregion
1266
+ //#region src/extensions/table.tsx
1267
+ const Table = EmailNode.create({
1268
+ name: "table",
1269
+ group: "block",
1270
+ content: "tableRow+",
1271
+ isolating: true,
1272
+ tableRole: "table",
1273
+ addAttributes() {
1274
+ return { ...createStandardAttributes([
1275
+ ...TABLE_ATTRIBUTES,
1276
+ ...LAYOUT_ATTRIBUTES,
1277
+ ...COMMON_HTML_ATTRIBUTES
1278
+ ]) };
1279
+ },
1280
+ parseHTML() {
1281
+ return [{
1282
+ tag: "table",
1283
+ getAttrs: (node) => {
1284
+ if (typeof node === "string") return false;
1285
+ const element = node;
1286
+ const attrs = {};
1287
+ Array.from(element.attributes).forEach((attr) => {
1288
+ attrs[attr.name] = attr.value;
1289
+ });
1290
+ return attrs;
1291
+ }
1292
+ }];
1293
+ },
1294
+ renderHTML({ HTMLAttributes }) {
1295
+ return [
1296
+ "table",
1297
+ mergeAttributes(this.options.HTMLAttributes, HTMLAttributes),
1298
+ [
1299
+ "tbody",
1300
+ {},
1301
+ 0
1302
+ ]
1303
+ ];
1304
+ },
1305
+ renderToReactEmail({ children, node, style }) {
1306
+ const inlineStyles = inlineCssToJs(node.attrs?.style);
1307
+ const alignment = node.attrs?.align || node.attrs?.alignment;
1308
+ const width = node.attrs?.width;
1309
+ const centeringStyles = alignment === "center" ? {
1310
+ marginLeft: "auto",
1311
+ marginRight: "auto"
1312
+ } : {};
1313
+ return /* @__PURE__ */ jsx(Section$1, {
1314
+ className: node.attrs?.class || void 0,
1315
+ align: alignment,
1316
+ style: resolveConflictingStyles(style, {
1317
+ ...inlineStyles,
1318
+ ...centeringStyles
1319
+ }),
1320
+ ...width !== void 0 ? { width } : {},
1321
+ children
1322
+ });
1323
+ }
1324
+ });
1325
+ const TableRow = EmailNode.create({
1326
+ name: "tableRow",
1327
+ group: "tableRow",
1328
+ content: "(tableCell | tableHeader)+",
1329
+ addAttributes() {
1330
+ return { ...createStandardAttributes([
1331
+ ...TABLE_CELL_ATTRIBUTES,
1332
+ ...LAYOUT_ATTRIBUTES,
1333
+ ...COMMON_HTML_ATTRIBUTES
1334
+ ]) };
1335
+ },
1336
+ parseHTML() {
1337
+ return [{
1338
+ tag: "tr",
1339
+ getAttrs: (node) => {
1340
+ if (typeof node === "string") return false;
1341
+ const element = node;
1342
+ const attrs = {};
1343
+ Array.from(element.attributes).forEach((attr) => {
1344
+ attrs[attr.name] = attr.value;
1345
+ });
1346
+ return attrs;
1347
+ }
1348
+ }];
1349
+ },
1350
+ renderHTML({ HTMLAttributes }) {
1351
+ return [
1352
+ "tr",
1353
+ HTMLAttributes,
1354
+ 0
1355
+ ];
1356
+ },
1357
+ renderToReactEmail({ children, node, style }) {
1358
+ const inlineStyles = inlineCssToJs(node.attrs?.style);
1359
+ return /* @__PURE__ */ jsx("tr", {
1360
+ className: node.attrs?.class || void 0,
1361
+ style: {
1362
+ ...style,
1363
+ ...inlineStyles
1364
+ },
1365
+ children
1366
+ });
1367
+ }
1368
+ });
1369
+ const TableCell = EmailNode.create({
1370
+ name: "tableCell",
1371
+ group: "tableCell",
1372
+ content: "block+",
1373
+ isolating: true,
1374
+ addAttributes() {
1375
+ return { ...createStandardAttributes([
1376
+ ...TABLE_CELL_ATTRIBUTES,
1377
+ ...LAYOUT_ATTRIBUTES,
1378
+ ...COMMON_HTML_ATTRIBUTES
1379
+ ]) };
1380
+ },
1381
+ parseHTML() {
1382
+ return [{
1383
+ tag: "td",
1384
+ getAttrs: (node) => {
1385
+ if (typeof node === "string") return false;
1386
+ const element = node;
1387
+ const attrs = {};
1388
+ Array.from(element.attributes).forEach((attr) => {
1389
+ attrs[attr.name] = attr.value;
1390
+ });
1391
+ return attrs;
1392
+ }
1393
+ }];
1394
+ },
1395
+ renderHTML({ HTMLAttributes }) {
1396
+ return [
1397
+ "td",
1398
+ HTMLAttributes,
1399
+ 0
1400
+ ];
1401
+ },
1402
+ renderToReactEmail({ children, node, style }) {
1403
+ const inlineStyles = inlineCssToJs(node.attrs?.style);
1404
+ return /* @__PURE__ */ jsx(Column, {
1405
+ className: node.attrs?.class || void 0,
1406
+ align: node.attrs?.align || node.attrs?.alignment,
1407
+ style: {
1408
+ ...style,
1409
+ ...inlineStyles
1410
+ },
1411
+ children
1412
+ });
1413
+ }
1414
+ });
1415
+ const TableHeader = Node.create({
1416
+ name: "tableHeader",
1417
+ group: "tableCell",
1418
+ content: "block+",
1419
+ isolating: true,
1420
+ addAttributes() {
1421
+ return { ...createStandardAttributes([
1422
+ ...TABLE_HEADER_ATTRIBUTES,
1423
+ ...TABLE_CELL_ATTRIBUTES,
1424
+ ...LAYOUT_ATTRIBUTES,
1425
+ ...COMMON_HTML_ATTRIBUTES
1426
+ ]) };
1427
+ },
1428
+ parseHTML() {
1429
+ return [{
1430
+ tag: "th",
1431
+ getAttrs: (node) => {
1432
+ if (typeof node === "string") return false;
1433
+ const element = node;
1434
+ const attrs = {};
1435
+ Array.from(element.attributes).forEach((attr) => {
1436
+ attrs[attr.name] = attr.value;
1437
+ });
1438
+ return attrs;
1439
+ }
1440
+ }];
1441
+ },
1442
+ renderHTML({ HTMLAttributes }) {
1443
+ return [
1444
+ "th",
1445
+ HTMLAttributes,
1446
+ 0
1447
+ ];
1448
+ }
1449
+ });
1450
+
1451
+ //#endregion
1452
+ //#region src/extensions/uppercase.ts
1453
+ const Uppercase = Mark.create({
1454
+ name: "uppercase",
1455
+ addOptions() {
1456
+ return { HTMLAttributes: {} };
1457
+ },
1458
+ parseHTML() {
1459
+ return [{
1460
+ tag: "span",
1461
+ getAttrs: (node) => {
1462
+ if (node.style.textTransform === "uppercase") return {};
1463
+ return false;
1464
+ }
1465
+ }];
1466
+ },
1467
+ renderHTML({ HTMLAttributes }) {
1468
+ return [
1469
+ "span",
1470
+ mergeAttributes(this.options.HTMLAttributes, HTMLAttributes, { style: "text-transform: uppercase" }),
1471
+ 0
1472
+ ];
1473
+ },
1474
+ addCommands() {
1475
+ return {
1476
+ setUppercase: () => ({ commands }) => {
1477
+ return commands.setMark(this.name);
1478
+ },
1479
+ toggleUppercase: () => ({ commands }) => {
1480
+ return commands.toggleMark(this.name);
1481
+ },
1482
+ unsetUppercase: () => ({ commands }) => {
1483
+ return commands.unsetMark(this.name);
1484
+ }
1485
+ };
1486
+ }
1487
+ });
1488
+
1489
+ //#endregion
1490
+ //#region src/extensions/columns.tsx
1491
+ const COLUMN_PARENT_TYPES = [
1492
+ "twoColumns",
1493
+ "threeColumns",
1494
+ "fourColumns"
1495
+ ];
1496
+ const COLUMN_PARENT_SET = new Set(COLUMN_PARENT_TYPES);
1497
+ const MAX_COLUMNS_DEPTH = 3;
1498
+ function getColumnsDepth(doc, from) {
1499
+ const $from = doc.resolve(from);
1500
+ let depth = 0;
1501
+ for (let d = $from.depth; d > 0; d--) if (COLUMN_PARENT_SET.has($from.node(d).type.name)) depth++;
1502
+ return depth;
1503
+ }
1504
+ const VARIANTS = [
1505
+ {
1506
+ name: "twoColumns",
1507
+ columnCount: 2,
1508
+ content: "columnsColumn columnsColumn",
1509
+ dataType: "two-columns"
1510
+ },
1511
+ {
1512
+ name: "threeColumns",
1513
+ columnCount: 3,
1514
+ content: "columnsColumn columnsColumn columnsColumn",
1515
+ dataType: "three-columns"
1516
+ },
1517
+ {
1518
+ name: "fourColumns",
1519
+ columnCount: 4,
1520
+ content: "columnsColumn{4}",
1521
+ dataType: "four-columns"
1522
+ }
1523
+ ];
1524
+ const NODE_TYPE_MAP = {
1525
+ 2: "twoColumns",
1526
+ 3: "threeColumns",
1527
+ 4: "fourColumns"
1528
+ };
1529
+ function createColumnsNode(config, includeCommands) {
1530
+ return EmailNode.create({
1531
+ name: config.name,
1532
+ group: "block",
1533
+ content: config.content,
1534
+ isolating: true,
1535
+ defining: true,
1536
+ addAttributes() {
1537
+ return createStandardAttributes([...LAYOUT_ATTRIBUTES, ...COMMON_HTML_ATTRIBUTES]);
1538
+ },
1539
+ parseHTML() {
1540
+ return [{ tag: `div[data-type="${config.dataType}"]` }];
1541
+ },
1542
+ renderHTML({ HTMLAttributes }) {
1543
+ return [
1544
+ "div",
1545
+ mergeAttributes({
1546
+ "data-type": config.dataType,
1547
+ class: "node-columns"
1548
+ }, HTMLAttributes),
1549
+ 0
1550
+ ];
1551
+ },
1552
+ ...includeCommands && { addCommands() {
1553
+ return { insertColumns: (count) => ({ commands, state }) => {
1554
+ if (getColumnsDepth(state.doc, state.selection.from) >= MAX_COLUMNS_DEPTH) return false;
1555
+ const nodeType = NODE_TYPE_MAP[count];
1556
+ const children = Array.from({ length: count }, () => ({
1557
+ type: "columnsColumn",
1558
+ content: [{
1559
+ type: "paragraph",
1560
+ content: []
1561
+ }]
1562
+ }));
1563
+ return commands.insertContent({
1564
+ type: nodeType,
1565
+ content: children
1566
+ });
1567
+ } };
1568
+ } },
1569
+ renderToReactEmail({ children, node, style }) {
1570
+ const inlineStyles = inlineCssToJs(node.attrs?.style);
1571
+ return /* @__PURE__ */ jsx(Row, {
1572
+ className: node.attrs?.class || void 0,
1573
+ style: {
1574
+ ...style,
1575
+ ...inlineStyles
1576
+ },
1577
+ children
1578
+ });
1579
+ }
1580
+ });
1581
+ }
1582
+ const TwoColumns = createColumnsNode(VARIANTS[0], true);
1583
+ const ThreeColumns = createColumnsNode(VARIANTS[1], false);
1584
+ const FourColumns = createColumnsNode(VARIANTS[2], false);
1585
+ const ColumnsColumn = EmailNode.create({
1586
+ name: "columnsColumn",
1587
+ group: "columnsColumn",
1588
+ content: "block+",
1589
+ isolating: true,
1590
+ addAttributes() {
1591
+ return { ...createStandardAttributes([...LAYOUT_ATTRIBUTES, ...COMMON_HTML_ATTRIBUTES]) };
1592
+ },
1593
+ parseHTML() {
1594
+ return [{ tag: "div[data-type=\"column\"]" }];
1595
+ },
1596
+ renderHTML({ HTMLAttributes }) {
1597
+ return [
1598
+ "div",
1599
+ mergeAttributes({
1600
+ "data-type": "column",
1601
+ class: "node-column"
1602
+ }, HTMLAttributes),
1603
+ 0
1604
+ ];
1605
+ },
1606
+ addKeyboardShortcuts() {
1607
+ return {
1608
+ Backspace: ({ editor }) => {
1609
+ const { state } = editor;
1610
+ const { selection } = state;
1611
+ const { empty, $from } = selection;
1612
+ if (!empty) return false;
1613
+ for (let depth = $from.depth; depth >= 1; depth--) {
1614
+ if ($from.pos !== $from.start(depth)) break;
1615
+ const indexInParent = $from.index(depth - 1);
1616
+ if (indexInParent === 0) continue;
1617
+ const prevNode = $from.node(depth - 1).child(indexInParent - 1);
1618
+ if (COLUMN_PARENT_SET.has(prevNode.type.name)) {
1619
+ const deleteFrom = $from.before(depth) - prevNode.nodeSize;
1620
+ const deleteTo = $from.before(depth);
1621
+ editor.view.dispatch(state.tr.delete(deleteFrom, deleteTo));
1622
+ return true;
1623
+ }
1624
+ break;
1625
+ }
1626
+ return false;
1627
+ },
1628
+ "Mod-a": ({ editor }) => {
1629
+ const { state } = editor;
1630
+ const { $from } = state.selection;
1631
+ for (let d = $from.depth; d > 0; d--) {
1632
+ if ($from.node(d).type.name !== "columnsColumn") continue;
1633
+ const columnStart = $from.start(d);
1634
+ const columnEnd = $from.end(d);
1635
+ const { from, to } = state.selection;
1636
+ if (from === columnStart && to === columnEnd) return false;
1637
+ editor.view.dispatch(state.tr.setSelection(TextSelection.create(state.doc, columnStart, columnEnd)));
1638
+ return true;
1639
+ }
1640
+ return false;
1641
+ }
1642
+ };
1643
+ },
1644
+ renderToReactEmail({ children, node, style }) {
1645
+ const inlineStyles = inlineCssToJs(node.attrs?.style);
1646
+ const width = node.attrs?.width;
1647
+ return /* @__PURE__ */ jsx(Column, {
1648
+ className: node.attrs?.class || void 0,
1649
+ style: {
1650
+ ...style,
1651
+ ...inlineStyles,
1652
+ ...width ? { width } : {}
1653
+ },
1654
+ children
1655
+ });
1656
+ }
1657
+ });
1658
+
1659
+ //#endregion
1660
+ //#region src/extensions/index.ts
1661
+ const coreExtensions = [
1662
+ StarterKit.configure({
1663
+ undoRedo: false,
1664
+ heading: false,
1665
+ link: false,
1666
+ underline: false,
1667
+ trailingNode: false,
1668
+ bold: false,
1669
+ gapcursor: false,
1670
+ listItem: {},
1671
+ bulletList: { HTMLAttributes: { class: "node-bulletList" } },
1672
+ paragraph: { HTMLAttributes: { class: "node-paragraph" } },
1673
+ orderedList: { HTMLAttributes: { class: "node-orderedList" } },
1674
+ blockquote: { HTMLAttributes: { class: "node-blockquote" } },
1675
+ codeBlock: false,
1676
+ code: { HTMLAttributes: {
1677
+ class: "node-inlineCode",
1678
+ spellcheck: "false"
1679
+ } },
1680
+ horizontalRule: false,
1681
+ dropcursor: {
1682
+ color: "#61a8f8",
1683
+ class: "rounded-full animate-[fade-in_300ms_ease-in-out] !z-40",
1684
+ width: 4
1685
+ }
1686
+ }),
1687
+ CodeBlockPrism.configure({
1688
+ defaultLanguage: "javascript",
1689
+ HTMLAttributes: { class: "prism node-codeBlock" }
1690
+ }),
1691
+ Placeholder,
1692
+ PreviewText,
1693
+ Bold,
1694
+ Sup,
1695
+ Uppercase,
1696
+ PreservedStyle,
1697
+ Table,
1698
+ TableRow,
1699
+ TableCell,
1700
+ TableHeader,
1701
+ Body,
1702
+ Div,
1703
+ Button,
1704
+ Section,
1705
+ AlignmentAttribute.configure({ types: [
1706
+ "heading",
1707
+ "paragraph",
1708
+ "image",
1709
+ "blockquote",
1710
+ "codeBlock",
1711
+ "bulletList",
1712
+ "orderedList",
1713
+ "listItem",
1714
+ "button",
1715
+ "youtube",
1716
+ "twitter",
1717
+ "table",
1718
+ "tableRow",
1719
+ "tableCell",
1720
+ "tableHeader",
1721
+ "columnsColumn"
1722
+ ] }),
1723
+ StyleAttribute.configure({ types: [
1724
+ "heading",
1725
+ "paragraph",
1726
+ "image",
1727
+ "blockquote",
1728
+ "codeBlock",
1729
+ "bulletList",
1730
+ "orderedList",
1731
+ "listItem",
1732
+ "button",
1733
+ "youtube",
1734
+ "twitter",
1735
+ "horizontalRule",
1736
+ "footer",
1737
+ "section",
1738
+ "div",
1739
+ "body",
1740
+ "table",
1741
+ "tableRow",
1742
+ "tableCell",
1743
+ "tableHeader",
1744
+ "columnsColumn",
1745
+ "link"
1746
+ ] }),
1747
+ ClassAttribute.configure({ types: [
1748
+ "heading",
1749
+ "paragraph",
1750
+ "image",
1751
+ "blockquote",
1752
+ "bulletList",
1753
+ "orderedList",
1754
+ "listItem",
1755
+ "button",
1756
+ "youtube",
1757
+ "twitter",
1758
+ "horizontalRule",
1759
+ "footer",
1760
+ "section",
1761
+ "div",
1762
+ "body",
1763
+ "table",
1764
+ "tableRow",
1765
+ "tableCell",
1766
+ "tableHeader",
1767
+ "columnsColumn",
1768
+ "link"
1769
+ ] }),
1770
+ MaxNesting.configure({
1771
+ maxDepth: 50,
1772
+ nodeTypes: [
1773
+ "section",
1774
+ "bulletList",
1775
+ "orderedList"
1776
+ ]
1777
+ })
1778
+ ];
1779
+
1780
+ //#endregion
1781
+ //#region src/utils/set-text-alignment.ts
1782
+ function setTextAlignment(editor, alignment) {
1783
+ const { from, to } = editor.state.selection;
1784
+ const tr = editor.state.tr;
1785
+ editor.state.doc.nodesBetween(from, to, (node, pos) => {
1786
+ if (node.isTextblock) {
1787
+ const prop = "align" in node.attrs ? "align" : "alignment";
1788
+ tr.setNodeMarkup(pos, null, {
1789
+ ...node.attrs,
1790
+ [prop]: alignment
1791
+ });
1792
+ }
1793
+ });
1794
+ editor.view.dispatch(tr);
1795
+ }
1796
+
1797
+ //#endregion
1798
+ //#region src/ui/bubble-menu/context.tsx
1799
+ const BubbleMenuContext = React.createContext(null);
1800
+ function useBubbleMenuContext() {
1801
+ const context = React.useContext(BubbleMenuContext);
1802
+ if (!context) throw new Error("BubbleMenu compound components must be used within <BubbleMenu.Root>");
1803
+ return context;
1804
+ }
1805
+
1806
+ //#endregion
1807
+ //#region src/ui/bubble-menu/item.tsx
1808
+ function BubbleMenuItem({ name, isActive, onCommand, className, children,...rest }) {
1809
+ return /* @__PURE__ */ jsx("button", {
1810
+ type: "button",
1811
+ "aria-label": name,
1812
+ "aria-pressed": isActive,
1813
+ className,
1814
+ "data-re-bubble-menu-item": "",
1815
+ "data-item": name,
1816
+ ...isActive ? { "data-active": "" } : {},
1817
+ onMouseDown: (e) => e.preventDefault(),
1818
+ onClick: onCommand,
1819
+ ...rest,
1820
+ children
1821
+ });
1822
+ }
1823
+
1824
+ //#endregion
1825
+ //#region src/ui/bubble-menu/align-center.tsx
1826
+ function BubbleMenuAlignCenter({ className, children }) {
1827
+ const { editor } = useBubbleMenuContext();
1828
+ return /* @__PURE__ */ jsx(BubbleMenuItem, {
1829
+ name: "align-center",
1830
+ isActive: useEditorState({
1831
+ editor,
1832
+ selector: ({ editor: editor$1 }) => editor$1?.isActive({ alignment: "center" }) ?? false
1833
+ }),
1834
+ onCommand: () => setTextAlignment(editor, "center"),
1835
+ className,
1836
+ children: children ?? /* @__PURE__ */ jsx(AlignCenterIcon, {})
1837
+ });
1838
+ }
1839
+
1840
+ //#endregion
1841
+ //#region src/ui/bubble-menu/align-left.tsx
1842
+ function BubbleMenuAlignLeft({ className, children }) {
1843
+ const { editor } = useBubbleMenuContext();
1844
+ return /* @__PURE__ */ jsx(BubbleMenuItem, {
1845
+ name: "align-left",
1846
+ isActive: useEditorState({
1847
+ editor,
1848
+ selector: ({ editor: editor$1 }) => editor$1?.isActive({ alignment: "left" }) ?? false
1849
+ }),
1850
+ onCommand: () => setTextAlignment(editor, "left"),
1851
+ className,
1852
+ children: children ?? /* @__PURE__ */ jsx(AlignLeftIcon, {})
1853
+ });
1854
+ }
1855
+
1856
+ //#endregion
1857
+ //#region src/ui/bubble-menu/align-right.tsx
1858
+ function BubbleMenuAlignRight({ className, children }) {
1859
+ const { editor } = useBubbleMenuContext();
1860
+ return /* @__PURE__ */ jsx(BubbleMenuItem, {
1861
+ name: "align-right",
1862
+ isActive: useEditorState({
1863
+ editor,
1864
+ selector: ({ editor: editor$1 }) => editor$1?.isActive({ alignment: "right" }) ?? false
1865
+ }),
1866
+ onCommand: () => setTextAlignment(editor, "right"),
1867
+ className,
1868
+ children: children ?? /* @__PURE__ */ jsx(AlignRightIcon, {})
1869
+ });
1870
+ }
1871
+
1872
+ //#endregion
1873
+ //#region src/ui/bubble-menu/create-mark-bubble-item.tsx
1874
+ function createMarkBubbleItem(config) {
1875
+ function MarkBubbleItem({ className, children }) {
1876
+ const { editor } = useBubbleMenuContext();
1877
+ const isActive = useEditorState({
1878
+ editor,
1879
+ selector: ({ editor: editor$1 }) => {
1880
+ if (config.activeParams) return editor$1?.isActive(config.activeName, config.activeParams) ?? false;
1881
+ return editor$1?.isActive(config.activeName) ?? false;
1882
+ }
1883
+ });
1884
+ const handleCommand = () => {
1885
+ const chain = editor.chain().focus();
1886
+ const method = chain[config.command];
1887
+ if (method) method.call(chain).run();
1888
+ };
1889
+ return /* @__PURE__ */ jsx(BubbleMenuItem, {
1890
+ name: config.name,
1891
+ isActive,
1892
+ onCommand: handleCommand,
1893
+ className,
1894
+ children: children ?? config.icon
1895
+ });
1896
+ }
1897
+ MarkBubbleItem.displayName = `BubbleMenu${config.name.charAt(0).toUpperCase() + config.name.slice(1)}`;
1898
+ return MarkBubbleItem;
1899
+ }
1900
+
1901
+ //#endregion
1902
+ //#region src/ui/bubble-menu/bold.tsx
1903
+ const BubbleMenuBold = createMarkBubbleItem({
1904
+ name: "bold",
1905
+ activeName: "bold",
1906
+ command: "toggleBold",
1907
+ icon: /* @__PURE__ */ jsx(BoldIcon, {})
1908
+ });
1909
+
1910
+ //#endregion
1911
+ //#region src/ui/bubble-menu/code.tsx
1912
+ const BubbleMenuCode = createMarkBubbleItem({
1913
+ name: "code",
1914
+ activeName: "code",
1915
+ command: "toggleCode",
1916
+ icon: /* @__PURE__ */ jsx(CodeIcon, {})
1917
+ });
1918
+
1919
+ //#endregion
1920
+ //#region src/ui/bubble-menu/group.tsx
1921
+ function BubbleMenuItemGroup({ className, children }) {
1922
+ return /* @__PURE__ */ jsx("fieldset", {
1923
+ className,
1924
+ "data-re-bubble-menu-group": "",
1925
+ children
1926
+ });
1927
+ }
1928
+
1929
+ //#endregion
1930
+ //#region src/ui/bubble-menu/italic.tsx
1931
+ const BubbleMenuItalic = createMarkBubbleItem({
1932
+ name: "italic",
1933
+ activeName: "italic",
1934
+ command: "toggleItalic",
1935
+ icon: /* @__PURE__ */ jsx(ItalicIcon, {})
1936
+ });
1937
+
1938
+ //#endregion
1939
+ //#region src/ui/bubble-menu/utils.ts
1940
+ const SAFE_PROTOCOLS = new Set([
1941
+ "http:",
1942
+ "https:",
1943
+ "mailto:",
1944
+ "tel:"
1945
+ ]);
1946
+ /**
1947
+ * Basic URL validation and auto-prefixing.
1948
+ * Rejects dangerous schemes (javascript:, data:, vbscript:, etc.).
1949
+ * Returns the valid URL string or null.
1950
+ */
1951
+ function getUrlFromString(str) {
1952
+ if (str === "#") return str;
1953
+ try {
1954
+ const url = new URL(str);
1955
+ if (SAFE_PROTOCOLS.has(url.protocol)) return str;
1956
+ return null;
1957
+ } catch {}
1958
+ try {
1959
+ if (str.includes(".") && !str.includes(" ")) return new URL(`https://${str}`).toString();
1960
+ } catch {}
1961
+ return null;
1962
+ }
1963
+ function setLinkHref(editor, href) {
1964
+ if (href.length === 0) {
1965
+ editor.chain().unsetLink().run();
1966
+ return;
1967
+ }
1968
+ const { from, to } = editor.state.selection;
1969
+ if (from === to) {
1970
+ editor.chain().extendMarkRange("link").setLink({ href }).setTextSelection({
1971
+ from,
1972
+ to
1973
+ }).run();
1974
+ return;
1975
+ }
1976
+ editor.chain().setLink({ href }).run();
1977
+ }
1978
+ function focusEditor(editor) {
1979
+ setTimeout(() => {
1980
+ editor.commands.focus();
1981
+ }, 0);
1982
+ }
1983
+
1984
+ //#endregion
1985
+ //#region src/ui/bubble-menu/link-selector.tsx
1986
+ function BubbleMenuLinkSelector({ className, showToggle = true, validateUrl, onLinkApply, onLinkRemove, children, open: controlledOpen, onOpenChange }) {
1987
+ const { editor } = useBubbleMenuContext();
1988
+ const [uncontrolledOpen, setUncontrolledOpen] = React.useState(false);
1989
+ const isControlled = controlledOpen !== void 0;
1990
+ const isOpen = isControlled ? controlledOpen : uncontrolledOpen;
1991
+ const setIsOpen = React.useCallback((value) => {
1992
+ if (!isControlled) setUncontrolledOpen(value);
1993
+ onOpenChange?.(value);
1994
+ }, [isControlled, onOpenChange]);
1995
+ const editorState = useEditorState({
1996
+ editor,
1997
+ selector: ({ editor: editor$1 }) => ({
1998
+ isLinkActive: editor$1?.isActive("link") ?? false,
1999
+ hasLink: Boolean(editor$1?.getAttributes("link").href),
2000
+ currentHref: editor$1?.getAttributes("link").href || ""
2001
+ })
2002
+ });
2003
+ const setIsOpenRef = React.useRef(setIsOpen);
2004
+ setIsOpenRef.current = setIsOpen;
2005
+ React.useEffect(() => {
2006
+ const subscription = editorEventBus.on("bubble-menu:add-link", () => {
2007
+ setIsOpenRef.current(true);
2008
+ });
2009
+ return () => {
2010
+ setIsOpenRef.current(false);
2011
+ subscription.unsubscribe();
2012
+ };
2013
+ }, []);
2014
+ if (!editorState) return null;
2015
+ const handleOpenLink = () => {
2016
+ setIsOpen(!isOpen);
2017
+ };
2018
+ return /* @__PURE__ */ jsxs("div", {
2019
+ "data-re-link-selector": "",
2020
+ ...isOpen ? { "data-open": "" } : {},
2021
+ ...editorState.hasLink ? { "data-has-link": "" } : {},
2022
+ className,
2023
+ children: [showToggle && /* @__PURE__ */ jsx("button", {
2024
+ type: "button",
2025
+ "aria-expanded": isOpen,
2026
+ "aria-haspopup": "true",
2027
+ "aria-label": "Add link",
2028
+ "aria-pressed": editorState.isLinkActive && editorState.hasLink,
2029
+ "data-re-link-selector-trigger": "",
2030
+ onClick: handleOpenLink,
2031
+ children: /* @__PURE__ */ jsx(LinkIcon, {})
2032
+ }), isOpen && /* @__PURE__ */ jsx(LinkForm, {
2033
+ editor,
2034
+ currentHref: editorState.currentHref,
2035
+ validateUrl,
2036
+ onLinkApply,
2037
+ onLinkRemove,
2038
+ setIsOpen,
2039
+ children
2040
+ })]
2041
+ });
2042
+ }
2043
+ function LinkForm({ editor, currentHref, validateUrl, onLinkApply, onLinkRemove, setIsOpen, children }) {
2044
+ const inputRef = React.useRef(null);
2045
+ const formRef = React.useRef(null);
2046
+ const displayHref = currentHref === "#" ? "" : currentHref;
2047
+ const [inputValue, setInputValue] = React.useState(displayHref);
2048
+ React.useEffect(() => {
2049
+ const timeoutId = setTimeout(() => {
2050
+ inputRef.current?.focus();
2051
+ }, 0);
2052
+ return () => clearTimeout(timeoutId);
2053
+ }, []);
2054
+ React.useEffect(() => {
2055
+ const handleKeyDown = (event) => {
2056
+ if (event.key === "Escape") {
2057
+ if (editor.getAttributes("link").href === "#") editor.chain().unsetLink().run();
2058
+ setIsOpen(false);
2059
+ }
2060
+ };
2061
+ const handleClickOutside = (event) => {
2062
+ if (formRef.current && !formRef.current.contains(event.target)) {
2063
+ const form = formRef.current;
2064
+ const submitEvent = new Event("submit", {
2065
+ bubbles: true,
2066
+ cancelable: true
2067
+ });
2068
+ form.dispatchEvent(submitEvent);
2069
+ setIsOpen(false);
2070
+ }
2071
+ };
2072
+ document.addEventListener("mousedown", handleClickOutside);
2073
+ window.addEventListener("keydown", handleKeyDown);
2074
+ return () => {
2075
+ window.removeEventListener("keydown", handleKeyDown);
2076
+ document.removeEventListener("mousedown", handleClickOutside);
2077
+ };
2078
+ }, [editor, setIsOpen]);
2079
+ function handleSubmit(e) {
2080
+ e.preventDefault();
2081
+ const value = inputValue.trim();
2082
+ if (value === "") {
2083
+ setLinkHref(editor, "");
2084
+ setIsOpen(false);
2085
+ focusEditor(editor);
2086
+ onLinkRemove?.();
2087
+ return;
2088
+ }
2089
+ const finalValue = (validateUrl ?? getUrlFromString)(value);
2090
+ if (!finalValue) {
2091
+ setLinkHref(editor, "");
2092
+ setIsOpen(false);
2093
+ focusEditor(editor);
2094
+ onLinkRemove?.();
2095
+ return;
2096
+ }
2097
+ setLinkHref(editor, finalValue);
2098
+ setIsOpen(false);
2099
+ focusEditor(editor);
2100
+ onLinkApply?.(finalValue);
2101
+ }
2102
+ function handleUnlink(e) {
2103
+ e.stopPropagation();
2104
+ setLinkHref(editor, "");
2105
+ setIsOpen(false);
2106
+ focusEditor(editor);
2107
+ onLinkRemove?.();
2108
+ }
2109
+ return /* @__PURE__ */ jsxs("form", {
2110
+ ref: formRef,
2111
+ "data-re-link-selector-form": "",
2112
+ onMouseDown: (e) => e.stopPropagation(),
2113
+ onClick: (e) => e.stopPropagation(),
2114
+ onKeyDown: (e) => e.stopPropagation(),
2115
+ onSubmit: handleSubmit,
2116
+ children: [
2117
+ /* @__PURE__ */ jsx("input", {
2118
+ ref: inputRef,
2119
+ "data-re-link-selector-input": "",
2120
+ value: inputValue,
2121
+ onFocus: (e) => e.stopPropagation(),
2122
+ onChange: (e) => setInputValue(e.target.value),
2123
+ placeholder: "Paste a link",
2124
+ type: "text"
2125
+ }),
2126
+ children,
2127
+ displayHref ? /* @__PURE__ */ jsx("button", {
2128
+ type: "button",
2129
+ "aria-label": "Remove link",
2130
+ "data-re-link-selector-unlink": "",
2131
+ onClick: handleUnlink,
2132
+ children: /* @__PURE__ */ jsx(UnlinkIcon, {})
2133
+ }) : /* @__PURE__ */ jsx("button", {
2134
+ type: "submit",
2135
+ "aria-label": "Apply link",
2136
+ "data-re-link-selector-apply": "",
2137
+ onMouseDown: (e) => e.stopPropagation(),
2138
+ children: /* @__PURE__ */ jsx(Check, {})
2139
+ })
2140
+ ]
2141
+ });
2142
+ }
2143
+
2144
+ //#endregion
2145
+ //#region src/ui/bubble-menu/node-selector.tsx
2146
+ const NodeSelectorContext = React.createContext(null);
2147
+ function useNodeSelectorContext() {
2148
+ const context = React.useContext(NodeSelectorContext);
2149
+ if (!context) throw new Error("NodeSelector compound components must be used within <NodeSelector.Root>");
2150
+ return context;
2151
+ }
2152
+ function NodeSelectorRoot({ omit = [], open: controlledOpen, onOpenChange, className, children }) {
2153
+ const { editor } = useBubbleMenuContext();
2154
+ const [uncontrolledOpen, setUncontrolledOpen] = React.useState(false);
2155
+ const isControlled = controlledOpen !== void 0;
2156
+ const isOpen = isControlled ? controlledOpen : uncontrolledOpen;
2157
+ const setIsOpen = React.useCallback((value) => {
2158
+ if (!isControlled) setUncontrolledOpen(value);
2159
+ onOpenChange?.(value);
2160
+ }, [isControlled, onOpenChange]);
2161
+ const editorState = useEditorState({
2162
+ editor,
2163
+ selector: ({ editor: editor$1 }) => ({
2164
+ isParagraphActive: (editor$1?.isActive("paragraph") ?? false) && !editor$1?.isActive("bulletList") && !editor$1?.isActive("orderedList"),
2165
+ isHeading1Active: editor$1?.isActive("heading", { level: 1 }) ?? false,
2166
+ isHeading2Active: editor$1?.isActive("heading", { level: 2 }) ?? false,
2167
+ isHeading3Active: editor$1?.isActive("heading", { level: 3 }) ?? false,
2168
+ isBulletListActive: editor$1?.isActive("bulletList") ?? false,
2169
+ isOrderedListActive: editor$1?.isActive("orderedList") ?? false,
2170
+ isBlockquoteActive: editor$1?.isActive("blockquote") ?? false,
2171
+ isCodeBlockActive: editor$1?.isActive("codeBlock") ?? false
2172
+ })
2173
+ });
2174
+ const allItems = React.useMemo(() => [
2175
+ {
2176
+ name: "Text",
2177
+ icon: TextIcon,
2178
+ command: () => editor.chain().focus().clearNodes().toggleNode("paragraph", "paragraph").run(),
2179
+ isActive: editorState?.isParagraphActive ?? false
2180
+ },
2181
+ {
2182
+ name: "Title",
2183
+ icon: Heading1,
2184
+ command: () => editor.chain().focus().clearNodes().toggleHeading({ level: 1 }).run(),
2185
+ isActive: editorState?.isHeading1Active ?? false
2186
+ },
2187
+ {
2188
+ name: "Subtitle",
2189
+ icon: Heading2,
2190
+ command: () => editor.chain().focus().clearNodes().toggleHeading({ level: 2 }).run(),
2191
+ isActive: editorState?.isHeading2Active ?? false
2192
+ },
2193
+ {
2194
+ name: "Heading",
2195
+ icon: Heading3,
2196
+ command: () => editor.chain().focus().clearNodes().toggleHeading({ level: 3 }).run(),
2197
+ isActive: editorState?.isHeading3Active ?? false
2198
+ },
2199
+ {
2200
+ name: "Bullet List",
2201
+ icon: List,
2202
+ command: () => editor.chain().focus().clearNodes().toggleBulletList().run(),
2203
+ isActive: editorState?.isBulletListActive ?? false
2204
+ },
2205
+ {
2206
+ name: "Numbered List",
2207
+ icon: ListOrdered,
2208
+ command: () => editor.chain().focus().clearNodes().toggleOrderedList().run(),
2209
+ isActive: editorState?.isOrderedListActive ?? false
2210
+ },
2211
+ {
2212
+ name: "Quote",
2213
+ icon: TextQuote,
2214
+ command: () => editor.chain().focus().clearNodes().toggleNode("paragraph", "paragraph").toggleBlockquote().run(),
2215
+ isActive: editorState?.isBlockquoteActive ?? false
2216
+ },
2217
+ {
2218
+ name: "Code",
2219
+ icon: Code,
2220
+ command: () => editor.chain().focus().clearNodes().toggleCodeBlock().run(),
2221
+ isActive: editorState?.isCodeBlockActive ?? false
2222
+ }
2223
+ ], [editor, editorState]);
2224
+ const items = React.useMemo(() => allItems.filter((item) => !omit.includes(item.name)), [allItems, omit]);
2225
+ const activeItem = React.useMemo(() => items.find((item) => item.isActive) ?? { name: "Multiple" }, [items]);
2226
+ const contextValue = React.useMemo(() => ({
2227
+ items,
2228
+ activeItem,
2229
+ isOpen,
2230
+ setIsOpen
2231
+ }), [
2232
+ items,
2233
+ activeItem,
2234
+ isOpen,
2235
+ setIsOpen
2236
+ ]);
2237
+ if (!editorState || items.length === 0) return null;
2238
+ return /* @__PURE__ */ jsx(NodeSelectorContext.Provider, {
2239
+ value: contextValue,
2240
+ children: /* @__PURE__ */ jsx(Popover.Root, {
2241
+ open: isOpen,
2242
+ onOpenChange: setIsOpen,
2243
+ children: /* @__PURE__ */ jsx("div", {
2244
+ "data-re-node-selector": "",
2245
+ ...isOpen ? { "data-open": "" } : {},
2246
+ className,
2247
+ children
2248
+ })
2249
+ })
2250
+ });
2251
+ }
2252
+ function NodeSelectorTrigger({ className, children }) {
2253
+ const { activeItem, isOpen, setIsOpen } = useNodeSelectorContext();
2254
+ return /* @__PURE__ */ jsx(Popover.Trigger, {
2255
+ "data-re-node-selector-trigger": "",
2256
+ className,
2257
+ onClick: () => setIsOpen(!isOpen),
2258
+ children: children ?? /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("span", { children: activeItem.name }), /* @__PURE__ */ jsx(ChevronDown, {})] })
2259
+ });
2260
+ }
2261
+ function NodeSelectorContent({ className, align = "start", children }) {
2262
+ const { items, setIsOpen } = useNodeSelectorContext();
2263
+ return /* @__PURE__ */ jsx(Popover.Content, {
2264
+ align,
2265
+ "data-re-node-selector-content": "",
2266
+ className,
2267
+ children: children ? children(items, () => setIsOpen(false)) : items.map((item) => {
2268
+ const Icon = item.icon;
2269
+ return /* @__PURE__ */ jsxs("button", {
2270
+ type: "button",
2271
+ "data-re-node-selector-item": "",
2272
+ ...item.isActive ? { "data-active": "" } : {},
2273
+ onClick: () => {
2274
+ item.command();
2275
+ setIsOpen(false);
2276
+ },
2277
+ children: [
2278
+ /* @__PURE__ */ jsx(Icon, {}),
2279
+ /* @__PURE__ */ jsx("span", { children: item.name }),
2280
+ item.isActive && /* @__PURE__ */ jsx(Check, {})
2281
+ ]
2282
+ }, item.name);
2283
+ })
2284
+ });
2285
+ }
2286
+ function BubbleMenuNodeSelector({ omit = [], className, triggerContent, open, onOpenChange }) {
2287
+ return /* @__PURE__ */ jsxs(NodeSelectorRoot, {
2288
+ omit,
2289
+ open,
2290
+ onOpenChange,
2291
+ className,
2292
+ children: [/* @__PURE__ */ jsx(NodeSelectorTrigger, { children: triggerContent }), /* @__PURE__ */ jsx(NodeSelectorContent, {})]
2293
+ });
2294
+ }
2295
+
2296
+ //#endregion
2297
+ //#region src/ui/bubble-menu/root.tsx
2298
+ function BubbleMenuRoot({ excludeNodes = [], placement = "bottom", offset = 8, onHide, className, children }) {
2299
+ const { editor } = useCurrentEditor();
2300
+ if (!editor) return null;
2301
+ return /* @__PURE__ */ jsx(BubbleMenu$1, {
2302
+ editor,
2303
+ "data-re-bubble-menu": "",
2304
+ shouldShow: ({ editor: editor$1, view }) => {
2305
+ for (const node of excludeNodes) if (editor$1.isActive(node)) return false;
2306
+ if (view.dom.classList.contains("dragging")) return false;
2307
+ return editor$1.view.state.selection.content().size > 0;
2308
+ },
2309
+ options: {
2310
+ placement,
2311
+ offset,
2312
+ onHide
2313
+ },
2314
+ className,
2315
+ children: /* @__PURE__ */ jsx(BubbleMenuContext.Provider, {
2316
+ value: { editor },
2317
+ children
2318
+ })
2319
+ });
2320
+ }
2321
+
2322
+ //#endregion
2323
+ //#region src/ui/bubble-menu/strike.tsx
2324
+ const BubbleMenuStrike = createMarkBubbleItem({
2325
+ name: "strike",
2326
+ activeName: "strike",
2327
+ command: "toggleStrike",
2328
+ icon: /* @__PURE__ */ jsx(StrikethroughIcon, {})
2329
+ });
2330
+
2331
+ //#endregion
2332
+ //#region src/ui/bubble-menu/underline.tsx
2333
+ const BubbleMenuUnderline = createMarkBubbleItem({
2334
+ name: "underline",
2335
+ activeName: "underline",
2336
+ command: "toggleUnderline",
2337
+ icon: /* @__PURE__ */ jsx(UnderlineIcon, {})
2338
+ });
2339
+
2340
+ //#endregion
2341
+ //#region src/ui/bubble-menu/uppercase.tsx
2342
+ const BubbleMenuUppercase = createMarkBubbleItem({
2343
+ name: "uppercase",
2344
+ activeName: "uppercase",
2345
+ command: "toggleUppercase",
2346
+ icon: /* @__PURE__ */ jsx(CaseUpperIcon, {})
2347
+ });
2348
+
2349
+ //#endregion
2350
+ //#region src/ui/bubble-menu/default.tsx
2351
+ function BubbleMenuDefault({ excludeItems = [], excludeNodes, placement, offset, onHide, className }) {
2352
+ const [isNodeSelectorOpen, setIsNodeSelectorOpen] = React.useState(false);
2353
+ const [isLinkSelectorOpen, setIsLinkSelectorOpen] = React.useState(false);
2354
+ const has = (item) => !excludeItems.includes(item);
2355
+ const handleNodeSelectorOpenChange = React.useCallback((open) => {
2356
+ setIsNodeSelectorOpen(open);
2357
+ if (open) setIsLinkSelectorOpen(false);
2358
+ }, []);
2359
+ const handleLinkSelectorOpenChange = React.useCallback((open) => {
2360
+ setIsLinkSelectorOpen(open);
2361
+ if (open) setIsNodeSelectorOpen(false);
2362
+ }, []);
2363
+ const handleHide = React.useCallback(() => {
2364
+ setIsNodeSelectorOpen(false);
2365
+ setIsLinkSelectorOpen(false);
2366
+ onHide?.();
2367
+ }, [onHide]);
2368
+ const hasFormattingItems = has("bold") || has("italic") || has("underline") || has("strike") || has("code") || has("uppercase");
2369
+ const hasAlignmentItems = has("align-left") || has("align-center") || has("align-right");
2370
+ return /* @__PURE__ */ jsxs(BubbleMenuRoot, {
2371
+ excludeNodes,
2372
+ placement,
2373
+ offset,
2374
+ onHide: handleHide,
2375
+ className,
2376
+ children: [
2377
+ has("node-selector") && /* @__PURE__ */ jsx(BubbleMenuNodeSelector, {
2378
+ open: isNodeSelectorOpen,
2379
+ onOpenChange: handleNodeSelectorOpenChange
2380
+ }),
2381
+ has("link-selector") && /* @__PURE__ */ jsx(BubbleMenuLinkSelector, {
2382
+ open: isLinkSelectorOpen,
2383
+ onOpenChange: handleLinkSelectorOpenChange
2384
+ }),
2385
+ hasFormattingItems && /* @__PURE__ */ jsxs(BubbleMenuItemGroup, { children: [
2386
+ has("bold") && /* @__PURE__ */ jsx(BubbleMenuBold, {}),
2387
+ has("italic") && /* @__PURE__ */ jsx(BubbleMenuItalic, {}),
2388
+ has("underline") && /* @__PURE__ */ jsx(BubbleMenuUnderline, {}),
2389
+ has("strike") && /* @__PURE__ */ jsx(BubbleMenuStrike, {}),
2390
+ has("code") && /* @__PURE__ */ jsx(BubbleMenuCode, {}),
2391
+ has("uppercase") && /* @__PURE__ */ jsx(BubbleMenuUppercase, {})
2392
+ ] }),
2393
+ hasAlignmentItems && /* @__PURE__ */ jsxs(BubbleMenuItemGroup, { children: [
2394
+ has("align-left") && /* @__PURE__ */ jsx(BubbleMenuAlignLeft, {}),
2395
+ has("align-center") && /* @__PURE__ */ jsx(BubbleMenuAlignCenter, {}),
2396
+ has("align-right") && /* @__PURE__ */ jsx(BubbleMenuAlignRight, {})
2397
+ ] })
2398
+ ]
2399
+ });
2400
+ }
2401
+
2402
+ //#endregion
2403
+ //#region src/ui/bubble-menu/separator.tsx
2404
+ function BubbleMenuSeparator({ className }) {
2405
+ return /* @__PURE__ */ jsx("hr", {
2406
+ className,
2407
+ "data-re-bubble-menu-separator": ""
2408
+ });
2409
+ }
2410
+
2411
+ //#endregion
2412
+ //#region src/ui/bubble-menu/index.ts
2413
+ const BubbleMenu = {
2414
+ Root: BubbleMenuRoot,
2415
+ ItemGroup: BubbleMenuItemGroup,
2416
+ Separator: BubbleMenuSeparator,
2417
+ Item: BubbleMenuItem,
2418
+ Bold: BubbleMenuBold,
2419
+ Italic: BubbleMenuItalic,
2420
+ Underline: BubbleMenuUnderline,
2421
+ Strike: BubbleMenuStrike,
2422
+ Code: BubbleMenuCode,
2423
+ Uppercase: BubbleMenuUppercase,
2424
+ AlignLeft: BubbleMenuAlignLeft,
2425
+ AlignCenter: BubbleMenuAlignCenter,
2426
+ AlignRight: BubbleMenuAlignRight,
2427
+ NodeSelector: Object.assign(BubbleMenuNodeSelector, {
2428
+ Root: NodeSelectorRoot,
2429
+ Trigger: NodeSelectorTrigger,
2430
+ Content: NodeSelectorContent
2431
+ }),
2432
+ LinkSelector: BubbleMenuLinkSelector,
2433
+ Default: BubbleMenuDefault
2434
+ };
2435
+
2436
+ //#endregion
2437
+ //#region src/ui/link-bubble-menu/context.tsx
2438
+ const LinkBubbleMenuContext = React.createContext(null);
2439
+ function useLinkBubbleMenuContext() {
2440
+ const context = React.useContext(LinkBubbleMenuContext);
2441
+ if (!context) throw new Error("LinkBubbleMenu compound components must be used within <LinkBubbleMenu.Root>");
2442
+ return context;
2443
+ }
2444
+
2445
+ //#endregion
2446
+ //#region src/ui/link-bubble-menu/edit-link.tsx
2447
+ function LinkBubbleMenuEditLink({ className, children, onClick,...rest }) {
2448
+ const { setIsEditing } = useLinkBubbleMenuContext();
2449
+ return /* @__PURE__ */ jsx("button", {
2450
+ type: "button",
2451
+ "aria-label": "Edit link",
2452
+ "data-re-link-bm-item": "",
2453
+ "data-item": "edit-link",
2454
+ className,
2455
+ onMouseDown: (e) => e.preventDefault(),
2456
+ onClick: (e) => {
2457
+ onClick?.(e);
2458
+ setIsEditing(true);
2459
+ },
2460
+ ...rest,
2461
+ children: children ?? /* @__PURE__ */ jsx(PencilIcon, {})
2462
+ });
2463
+ }
2464
+
2465
+ //#endregion
2466
+ //#region src/ui/link-bubble-menu/form.tsx
2467
+ function LinkBubbleMenuForm({ className, validateUrl, onLinkApply, onLinkRemove, children }) {
2468
+ const { editor, linkHref, isEditing, setIsEditing } = useLinkBubbleMenuContext();
2469
+ const inputRef = React.useRef(null);
2470
+ const formRef = React.useRef(null);
2471
+ const displayHref = linkHref === "#" ? "" : linkHref;
2472
+ const [inputValue, setInputValue] = React.useState(displayHref);
2473
+ React.useEffect(() => {
2474
+ if (!isEditing) return;
2475
+ const timeoutId = setTimeout(() => {
2476
+ inputRef.current?.focus();
2477
+ }, 0);
2478
+ return () => clearTimeout(timeoutId);
2479
+ }, [isEditing]);
2480
+ React.useEffect(() => {
2481
+ if (!isEditing) return;
2482
+ const handleKeyDown = (event) => {
2483
+ if (event.key === "Escape") setIsEditing(false);
2484
+ };
2485
+ const handleClickOutside = (event) => {
2486
+ if (formRef.current && !formRef.current.contains(event.target)) {
2487
+ const form = formRef.current;
2488
+ const submitEvent = new Event("submit", {
2489
+ bubbles: true,
2490
+ cancelable: true
2491
+ });
2492
+ form.dispatchEvent(submitEvent);
2493
+ setIsEditing(false);
2494
+ }
2495
+ };
2496
+ document.addEventListener("mousedown", handleClickOutside);
2497
+ window.addEventListener("keydown", handleKeyDown);
2498
+ return () => {
2499
+ window.removeEventListener("keydown", handleKeyDown);
2500
+ document.removeEventListener("mousedown", handleClickOutside);
2501
+ };
2502
+ }, [isEditing, setIsEditing]);
2503
+ if (!isEditing) return null;
2504
+ function handleSubmit(e) {
2505
+ e.preventDefault();
2506
+ const value = inputValue.trim();
2507
+ if (value === "") {
2508
+ setLinkHref(editor, "");
2509
+ setIsEditing(false);
2510
+ focusEditor(editor);
2511
+ onLinkRemove?.();
2512
+ return;
2513
+ }
2514
+ const finalValue = (validateUrl ?? getUrlFromString)(value);
2515
+ if (!finalValue) {
2516
+ setLinkHref(editor, "");
2517
+ setIsEditing(false);
2518
+ focusEditor(editor);
2519
+ onLinkRemove?.();
2520
+ return;
2521
+ }
2522
+ setLinkHref(editor, finalValue);
2523
+ setIsEditing(false);
2524
+ focusEditor(editor);
2525
+ onLinkApply?.(finalValue);
2526
+ }
2527
+ function handleUnlink(e) {
2528
+ e.stopPropagation();
2529
+ setLinkHref(editor, "");
2530
+ setIsEditing(false);
2531
+ focusEditor(editor);
2532
+ onLinkRemove?.();
2533
+ }
2534
+ return /* @__PURE__ */ jsxs("form", {
2535
+ ref: formRef,
2536
+ "data-re-link-bm-form": "",
2537
+ className,
2538
+ onMouseDown: (e) => e.stopPropagation(),
2539
+ onClick: (e) => e.stopPropagation(),
2540
+ onKeyDown: (e) => e.stopPropagation(),
2541
+ onSubmit: handleSubmit,
2542
+ children: [
2543
+ /* @__PURE__ */ jsx("input", {
2544
+ ref: inputRef,
2545
+ "data-re-link-bm-input": "",
2546
+ value: inputValue,
2547
+ onFocus: (e) => e.stopPropagation(),
2548
+ onChange: (e) => setInputValue(e.target.value),
2549
+ placeholder: "Paste a link",
2550
+ type: "text"
2551
+ }),
2552
+ children,
2553
+ displayHref ? /* @__PURE__ */ jsx("button", {
2554
+ type: "button",
2555
+ "aria-label": "Remove link",
2556
+ "data-re-link-bm-unlink": "",
2557
+ onClick: handleUnlink,
2558
+ children: /* @__PURE__ */ jsx(UnlinkIcon, {})
2559
+ }) : /* @__PURE__ */ jsx("button", {
2560
+ type: "submit",
2561
+ "aria-label": "Apply link",
2562
+ "data-re-link-bm-apply": "",
2563
+ onMouseDown: (e) => e.stopPropagation(),
2564
+ children: /* @__PURE__ */ jsx(Check, {})
2565
+ })
2566
+ ]
2567
+ });
2568
+ }
2569
+
2570
+ //#endregion
2571
+ //#region src/ui/link-bubble-menu/open-link.tsx
2572
+ function LinkBubbleMenuOpenLink({ className, children,...rest }) {
2573
+ const { linkHref } = useLinkBubbleMenuContext();
2574
+ return /* @__PURE__ */ jsx("a", {
2575
+ ...rest,
2576
+ href: linkHref,
2577
+ target: "_blank",
2578
+ rel: "noopener noreferrer",
2579
+ "aria-label": "Open link",
2580
+ "data-re-link-bm-item": "",
2581
+ "data-item": "open-link",
2582
+ className,
2583
+ children: children ?? /* @__PURE__ */ jsx(ExternalLinkIcon, {})
2584
+ });
2585
+ }
2586
+
2587
+ //#endregion
2588
+ //#region src/ui/link-bubble-menu/root.tsx
2589
+ function LinkBubbleMenuRoot({ onHide, placement = "bottom", offset = 8, className, children }) {
2590
+ const { editor } = useCurrentEditor();
2591
+ const [isEditing, setIsEditing] = React.useState(false);
2592
+ const linkHref = useEditorState({
2593
+ editor,
2594
+ selector: ({ editor: e }) => e?.getAttributes("link").href ?? ""
2595
+ });
2596
+ if (!editor) return null;
2597
+ return /* @__PURE__ */ jsx(BubbleMenu$1, {
2598
+ editor,
2599
+ "data-re-link-bm": "",
2600
+ shouldShow: ({ editor: e }) => e.isActive("link") && e.view.state.selection.content().size === 0,
2601
+ options: {
2602
+ placement,
2603
+ offset,
2604
+ onHide: () => {
2605
+ setIsEditing(false);
2606
+ onHide?.();
2607
+ }
2608
+ },
2609
+ className,
2610
+ children: /* @__PURE__ */ jsx(LinkBubbleMenuContext.Provider, {
2611
+ value: {
2612
+ editor,
2613
+ linkHref: linkHref ?? "",
2614
+ isEditing,
2615
+ setIsEditing
2616
+ },
2617
+ children
2618
+ })
2619
+ });
2620
+ }
2621
+
2622
+ //#endregion
2623
+ //#region src/ui/link-bubble-menu/toolbar.tsx
2624
+ function LinkBubbleMenuToolbar({ className, children }) {
2625
+ const { isEditing } = useLinkBubbleMenuContext();
2626
+ if (isEditing) return null;
2627
+ return /* @__PURE__ */ jsx("div", {
2628
+ "data-re-link-bm-toolbar": "",
2629
+ className,
2630
+ children
2631
+ });
2632
+ }
2633
+
2634
+ //#endregion
2635
+ //#region src/ui/link-bubble-menu/unlink.tsx
2636
+ function LinkBubbleMenuUnlink({ className, children, onClick,...rest }) {
2637
+ const { editor } = useLinkBubbleMenuContext();
2638
+ return /* @__PURE__ */ jsx("button", {
2639
+ type: "button",
2640
+ "aria-label": "Remove link",
2641
+ "data-re-link-bm-item": "",
2642
+ "data-item": "unlink",
2643
+ className,
2644
+ onMouseDown: (e) => e.preventDefault(),
2645
+ onClick: (e) => {
2646
+ onClick?.(e);
2647
+ editor.chain().focus().unsetLink().run();
2648
+ },
2649
+ ...rest,
2650
+ children: children ?? /* @__PURE__ */ jsx(UnlinkIcon, {})
2651
+ });
2652
+ }
2653
+
2654
+ //#endregion
2655
+ //#region src/ui/link-bubble-menu/index.ts
2656
+ const LinkBubbleMenu = {
2657
+ Root: LinkBubbleMenuRoot,
2658
+ Toolbar: LinkBubbleMenuToolbar,
2659
+ Form: LinkBubbleMenuForm,
2660
+ EditLink: LinkBubbleMenuEditLink,
2661
+ Unlink: LinkBubbleMenuUnlink,
2662
+ OpenLink: LinkBubbleMenuOpenLink
2663
+ };
2664
+
2665
+ //#endregion
2666
+ export { AlignmentAttribute, Body, Bold, BubbleMenu, BubbleMenuAlignCenter, BubbleMenuAlignLeft, BubbleMenuAlignRight, BubbleMenuBold, BubbleMenuCode, BubbleMenuDefault, BubbleMenuItalic, BubbleMenuItem, BubbleMenuItemGroup, BubbleMenuLinkSelector, BubbleMenuNodeSelector, BubbleMenuRoot, BubbleMenuSeparator, BubbleMenuStrike, BubbleMenuUnderline, BubbleMenuUppercase, Button, COLUMN_PARENT_TYPES, ClassAttribute, CodeBlockPrism, ColumnsColumn, Div, EmailNode, FourColumns, LinkBubbleMenu, LinkBubbleMenuEditLink, LinkBubbleMenuForm, LinkBubbleMenuOpenLink, LinkBubbleMenuRoot, LinkBubbleMenuToolbar, LinkBubbleMenuUnlink, MAX_COLUMNS_DEPTH, MaxNesting, NodeSelectorContent, NodeSelectorRoot, NodeSelectorTrigger, Placeholder, PreservedStyle, PreviewText, Section, StyleAttribute, Sup, Table, TableCell, TableHeader, TableRow, ThreeColumns, TwoColumns, Uppercase, coreExtensions, editorEventBus, getColumnsDepth, processStylesForUnlink, setTextAlignment, useLinkBubbleMenuContext };
573
2667
  //# sourceMappingURL=index.mjs.map