@react-email/editor 0.0.0-experimental.14 → 0.0.0-experimental.16

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,51 +1,34 @@
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";
4
1
  import * as ReactEmailComponents from "@react-email/components";
5
- import { Button as Button$1, CodeBlock, Column, Row, Section as Section$1 } from "@react-email/components";
2
+ import { Body as Body$1, Button as Button$1, CodeBlock, Column, Head, Html, Preview, Row, Section as Section$1, pretty, render, toPlainText } from "@react-email/components";
3
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
4
+ import { Extension, Mark, Node as Node$1, findChildren, markInputRule, markPasteRule, mergeAttributes } from "@tiptap/core";
5
+ import { UndoRedo } from "@tiptap/extensions";
6
+ import { ReactRenderer, useCurrentEditor, useEditor as useEditor$1, useEditorState } from "@tiptap/react";
7
+ import * as React from "react";
8
+ import { useCallback, useEffect, useImperativeHandle, useLayoutEffect, useRef, useState } from "react";
9
+ import { StarterKit } from "@tiptap/starter-kit";
10
+ import BlockquoteBase from "@tiptap/extension-blockquote";
11
+ import BulletListBase from "@tiptap/extension-bullet-list";
12
+ import CodeBase from "@tiptap/extension-code";
6
13
  import CodeBlock$1 from "@tiptap/extension-code-block";
7
14
  import { Plugin, PluginKey, TextSelection } from "@tiptap/pm/state";
8
15
  import { Decoration, DecorationSet } from "@tiptap/pm/view";
9
16
  import { fromHtml } from "hast-util-from-html";
10
17
  import Prism from "prismjs";
18
+ import HardBreakBase from "@tiptap/extension-hard-break";
19
+ import ItalicBase from "@tiptap/extension-italic";
20
+ import ListItemBase from "@tiptap/extension-list-item";
21
+ import OrderedListBase from "@tiptap/extension-ordered-list";
22
+ import ParagraphBase from "@tiptap/extension-paragraph";
11
23
  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";
24
+ import StrikeBase from "@tiptap/extension-strike";
25
+ import { generateJSON } from "@tiptap/html";
26
+ import { AlignCenterIcon, AlignLeftIcon, AlignRightIcon, BoldIcon, CaseUpperIcon, Check, ChevronDown, Code as Code$1, CodeIcon, Columns2, Columns3, Columns4, ExternalLinkIcon, Heading1, Heading2, Heading3, ItalicIcon, LinkIcon, List, ListOrdered, MousePointer, PencilIcon, Rows2, SplitSquareVertical, SquareCode, StrikethroughIcon, Text, TextIcon, TextQuote, UnderlineIcon, UnlinkIcon } from "lucide-react";
15
27
  import * as Popover from "@radix-ui/react-popover";
16
28
  import { BubbleMenu as BubbleMenu$1 } from "@tiptap/react/menus";
29
+ import Suggestion from "@tiptap/suggestion";
30
+ import tippy from "tippy.js";
17
31
 
18
- //#region src/core/email-node.ts
19
- var EmailNode = class EmailNode extends Node {
20
- constructor(config) {
21
- super(config);
22
- }
23
- /**
24
- * Create a new Node instance
25
- * @param config - Node configuration object or a function that returns a configuration object
26
- */
27
- static create(config) {
28
- return new EmailNode(typeof config === "function" ? config() : config);
29
- }
30
- static from(node, renderToReactEmail) {
31
- const customNode = EmailNode.create({});
32
- Object.assign(customNode, { ...node });
33
- customNode.config = {
34
- ...node.config,
35
- renderToReactEmail
36
- };
37
- return customNode;
38
- }
39
- configure(options) {
40
- return super.configure(options);
41
- }
42
- extend(extendedConfig) {
43
- const resolvedConfig = typeof extendedConfig === "function" ? extendedConfig() : extendedConfig;
44
- return super.extend(resolvedConfig);
45
- }
46
- };
47
-
48
- //#endregion
49
32
  //#region src/core/event-bus.ts
50
33
  const EVENT_PREFIX = "@react-email/editor:";
51
34
  var EditorEventBus = class {
@@ -87,139 +70,6 @@ var EditorEventBus = class {
87
70
  };
88
71
  const editorEventBus = new EditorEventBus();
89
72
 
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
-
145
- //#endregion
146
- //#region src/utils/attribute-helpers.ts
147
- /**
148
- * Creates TipTap attribute definitions for a list of HTML attributes.
149
- * Each attribute will have the same pattern:
150
- * - default: null
151
- * - parseHTML: extracts the attribute from the element
152
- * - renderHTML: conditionally renders the attribute if it has a value
153
- *
154
- * @param attributeNames - Array of HTML attribute names to create definitions for
155
- * @returns Object with TipTap attribute definitions
156
- *
157
- * @example
158
- * const attrs = createStandardAttributes(['class', 'id', 'title']);
159
- * // Returns:
160
- * // {
161
- * // class: {
162
- * // default: null,
163
- * // parseHTML: (element) => element.getAttribute('class'),
164
- * // renderHTML: (attributes) => attributes.class ? { class: attributes.class } : {}
165
- * // },
166
- * // ...
167
- * // }
168
- */
169
- function createStandardAttributes(attributeNames) {
170
- return Object.fromEntries(attributeNames.map((attr) => [attr, {
171
- default: null,
172
- parseHTML: (element) => element.getAttribute(attr),
173
- renderHTML: (attributes) => {
174
- if (!attributes[attr]) return {};
175
- return { [attr]: attributes[attr] };
176
- }
177
- }]));
178
- }
179
- /**
180
- * Common HTML attributes used across multiple extensions.
181
- * These preserve attributes during HTML import and editing for better
182
- * fidelity when importing existing email templates.
183
- */
184
- const COMMON_HTML_ATTRIBUTES = [
185
- "id",
186
- "class",
187
- "title",
188
- "lang",
189
- "dir",
190
- "data-id"
191
- ];
192
- /**
193
- * Layout-specific HTML attributes used for positioning and sizing.
194
- */
195
- const LAYOUT_ATTRIBUTES = [
196
- "align",
197
- "width",
198
- "height"
199
- ];
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
- /**
209
- * Table cell-specific HTML attributes.
210
- */
211
- const TABLE_CELL_ATTRIBUTES = [
212
- "valign",
213
- "bgcolor",
214
- "colspan",
215
- "rowspan"
216
- ];
217
- /**
218
- * Table header cell-specific HTML attributes.
219
- * These are additional attributes that only apply to <th> elements.
220
- */
221
- const TABLE_HEADER_ATTRIBUTES = [...TABLE_CELL_ATTRIBUTES, "scope"];
222
-
223
73
  //#endregion
224
74
  //#region src/utils/styles.ts
225
75
  const WHITE_SPACE_REGEX = /\s+/;
@@ -372,22 +222,358 @@ function convertBorderValue(value) {
372
222
  }
373
223
  }
374
224
  /**
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
225
+ * Resolves conflicts between reset styles and inline styles by expanding
226
+ * shorthand properties (margin, padding) to longhand before merging.
227
+ * This prevents shorthand properties from overriding specific longhand properties.
228
+ *
229
+ * @param resetStyles - Base reset styles that may contain shorthand properties
230
+ * @param inlineStyles - Inline styles that should override reset styles
231
+ * @returns Merged styles with inline styles taking precedence
232
+ */
233
+ function resolveConflictingStyles(resetStyles, inlineStyles) {
234
+ const expandedResetStyles = expandShorthandProperties(resetStyles);
235
+ const expandedInlineStyles = expandShorthandProperties(inlineStyles);
236
+ return {
237
+ ...expandedResetStyles,
238
+ ...expandedInlineStyles
239
+ };
240
+ }
241
+
242
+ //#endregion
243
+ //#region src/core/serializer/default-base-template.tsx
244
+ function DefaultBaseTemplate({ children, previewText }) {
245
+ return /* @__PURE__ */ jsxs(Html, { children: [
246
+ /* @__PURE__ */ jsxs(Head, { children: [
247
+ /* @__PURE__ */ jsx("meta", {
248
+ content: "width=device-width",
249
+ name: "viewport"
250
+ }),
251
+ /* @__PURE__ */ jsx("meta", {
252
+ content: "IE=edge",
253
+ httpEquiv: "X-UA-Compatible"
254
+ }),
255
+ /* @__PURE__ */ jsx("meta", { name: "x-apple-disable-message-reformatting" }),
256
+ /* @__PURE__ */ jsx("meta", {
257
+ content: "telephone=no,address=no,email=no,date=no,url=no",
258
+ name: "format-detection"
259
+ })
260
+ ] }),
261
+ previewText && previewText !== "" && /* @__PURE__ */ jsx(Preview, { children: previewText }),
262
+ /* @__PURE__ */ jsx(Body$1, { children: /* @__PURE__ */ jsx(Section$1, {
263
+ width: "100%",
264
+ align: "center",
265
+ children: /* @__PURE__ */ jsx(Section$1, {
266
+ style: { width: "100%" },
267
+ children
268
+ })
269
+ }) })
270
+ ] });
271
+ }
272
+
273
+ //#endregion
274
+ //#region src/core/serializer/email-mark.ts
275
+ var EmailMark = class EmailMark extends Mark {
276
+ constructor(config) {
277
+ super(config);
278
+ }
279
+ /**
280
+ * Create a new Mark instance
281
+ * @param config - Mark configuration object or a function that returns a configuration object
282
+ */
283
+ static create(config) {
284
+ return new EmailMark(typeof config === "function" ? config() : config);
285
+ }
286
+ static from(mark, renderToReactEmail) {
287
+ const customMark = EmailMark.create({});
288
+ Object.assign(customMark, { ...mark });
289
+ customMark.config = {
290
+ ...mark.config,
291
+ renderToReactEmail
292
+ };
293
+ return customMark;
294
+ }
295
+ configure(options) {
296
+ return super.configure(options);
297
+ }
298
+ extend(extendedConfig) {
299
+ const resolvedConfig = typeof extendedConfig === "function" ? extendedConfig() : extendedConfig;
300
+ return super.extend(resolvedConfig);
301
+ }
302
+ };
303
+
304
+ //#endregion
305
+ //#region src/core/serializer/email-node.ts
306
+ var EmailNode = class EmailNode extends Node$1 {
307
+ constructor(config) {
308
+ super(config);
309
+ }
310
+ /**
311
+ * Create a new Node instance
312
+ * @param config - Node configuration object or a function that returns a configuration object
313
+ */
314
+ static create(config) {
315
+ return new EmailNode(typeof config === "function" ? config() : config);
316
+ }
317
+ static from(node, renderToReactEmail) {
318
+ const customNode = EmailNode.create({});
319
+ Object.assign(customNode, { ...node });
320
+ customNode.config = {
321
+ ...node.config,
322
+ renderToReactEmail
323
+ };
324
+ return customNode;
325
+ }
326
+ configure(options) {
327
+ return super.configure(options);
328
+ }
329
+ extend(extendedConfig) {
330
+ const resolvedConfig = typeof extendedConfig === "function" ? extendedConfig() : extendedConfig;
331
+ return super.extend(resolvedConfig);
332
+ }
333
+ };
334
+
335
+ //#endregion
336
+ //#region src/core/serializer/compose-react-email.tsx
337
+ const MARK_ORDER = {
338
+ preservedStyle: 0,
339
+ italic: 1,
340
+ strike: 2,
341
+ underline: 3,
342
+ link: 4,
343
+ bold: 5,
344
+ code: 6
345
+ };
346
+ const NODES_WITH_INCREMENTED_CHILD_DEPTH = new Set(["bulletList", "orderedList"]);
347
+ function getOrderedMarks(marks) {
348
+ if (!marks) return [];
349
+ return [...marks].sort((a, b) => (MARK_ORDER[a.type] ?? Number.MAX_SAFE_INTEGER) - (MARK_ORDER[b.type] ?? Number.MAX_SAFE_INTEGER));
350
+ }
351
+ const composeReactEmail = async ({ editor, preview }) => {
352
+ const data = editor.getJSON();
353
+ const extensions = editor.extensionManager.extensions;
354
+ const serializerPlugin = extensions.map((ext) => ext.options?.serializerPlugin).filter((p) => Boolean(p)).at(-1);
355
+ const emailNodeComponentRegistry = Object.fromEntries(extensions.filter((ext) => ext instanceof EmailNode).map((extension) => [extension.name, extension.config.renderToReactEmail]));
356
+ const emailMarkComponentRegistry = Object.fromEntries(extensions.filter((ext) => ext instanceof EmailMark).map((extension) => [extension.name, extension.config.renderToReactEmail]));
357
+ function renderMark(mark, node, children, depth) {
358
+ const markStyle = serializerPlugin?.getNodeStyles({
359
+ type: mark.type,
360
+ attrs: mark.attrs ?? {}
361
+ }, depth, editor) ?? {};
362
+ const markRenderer = emailMarkComponentRegistry[mark.type];
363
+ if (markRenderer) return markRenderer({
364
+ mark,
365
+ node,
366
+ style: markStyle,
367
+ children
368
+ });
369
+ return children;
370
+ }
371
+ function parseContent(content, depth = 0) {
372
+ if (!content) return;
373
+ return content.map((node, index) => {
374
+ const style = serializerPlugin?.getNodeStyles(node, depth, editor) ?? {};
375
+ const inlineStyles = inlineCssToJs(node.attrs?.style);
376
+ if (node.type && emailNodeComponentRegistry[node.type]) {
377
+ const Component = emailNodeComponentRegistry[node.type];
378
+ const childDepth = NODES_WITH_INCREMENTED_CHILD_DEPTH.has(node.type) ? depth + 1 : depth;
379
+ return /* @__PURE__ */ jsx(Component, {
380
+ node: node.type === "table" && inlineStyles.width && !node.attrs?.width ? {
381
+ ...node,
382
+ attrs: {
383
+ ...node.attrs,
384
+ width: inlineStyles.width
385
+ }
386
+ } : node,
387
+ style,
388
+ children: parseContent(node.content, childDepth)
389
+ }, index);
390
+ }
391
+ switch (node.type) {
392
+ case "text": {
393
+ let wrappedText = node.text;
394
+ getOrderedMarks(node.marks).forEach((mark) => {
395
+ wrappedText = renderMark(mark, node, wrappedText, depth);
396
+ });
397
+ const textAttributes = node.marks?.find((mark) => mark.type === "textStyle")?.attrs;
398
+ return /* @__PURE__ */ jsx("span", {
399
+ style: {
400
+ ...textAttributes,
401
+ ...style
402
+ },
403
+ children: wrappedText
404
+ }, index);
405
+ }
406
+ default: return null;
407
+ }
408
+ });
409
+ }
410
+ const unformattedHtml = await render(/* @__PURE__ */ jsx(serializerPlugin?.BaseTemplate ?? DefaultBaseTemplate, {
411
+ previewText: preview,
412
+ editor,
413
+ children: parseContent(data.content)
414
+ }));
415
+ const [prettyHtml, text] = await Promise.all([pretty(unformattedHtml), toPlainText(unformattedHtml)]);
416
+ return {
417
+ html: prettyHtml,
418
+ text
419
+ };
420
+ };
421
+
422
+ //#endregion
423
+ //#region src/extensions/alignment-attribute.tsx
424
+ const AlignmentAttribute = Extension.create({
425
+ name: "alignmentAttribute",
426
+ addOptions() {
427
+ return {
428
+ types: [],
429
+ alignments: [
430
+ "left",
431
+ "center",
432
+ "right",
433
+ "justify"
434
+ ]
435
+ };
436
+ },
437
+ addGlobalAttributes() {
438
+ return [{
439
+ types: this.options.types,
440
+ attributes: { alignment: {
441
+ parseHTML: (element) => {
442
+ const explicitAlign = element.getAttribute("align") || element.getAttribute("alignment") || element.style.textAlign;
443
+ if (explicitAlign && this.options.alignments.includes(explicitAlign)) return explicitAlign;
444
+ return null;
445
+ },
446
+ renderHTML: (attributes) => {
447
+ if (attributes.alignment === "left") return {};
448
+ return { alignment: attributes.alignment };
449
+ }
450
+ } }
451
+ }];
452
+ },
453
+ addCommands() {
454
+ return { setAlignment: (alignment) => ({ commands }) => {
455
+ if (!this.options.alignments.includes(alignment)) return false;
456
+ return this.options.types.every((type) => commands.updateAttributes(type, { alignment }));
457
+ } };
458
+ },
459
+ addKeyboardShortcuts() {
460
+ return {
461
+ Enter: () => {
462
+ const { from } = this.editor.state.selection;
463
+ const currentAlignment = this.editor.state.doc.nodeAt(from)?.attrs?.alignment;
464
+ if (currentAlignment) requestAnimationFrame(() => {
465
+ this.editor.commands.setAlignment(currentAlignment);
466
+ });
467
+ return false;
468
+ },
469
+ "Mod-Shift-l": () => this.editor.commands.setAlignment("left"),
470
+ "Mod-Shift-e": () => this.editor.commands.setAlignment("center"),
471
+ "Mod-Shift-r": () => this.editor.commands.setAlignment("right"),
472
+ "Mod-Shift-j": () => this.editor.commands.setAlignment("justify")
473
+ };
474
+ }
475
+ });
476
+
477
+ //#endregion
478
+ //#region src/utils/get-text-alignment.ts
479
+ function getTextAlignment(alignment) {
480
+ switch (alignment) {
481
+ case "left": return { textAlign: "left" };
482
+ case "center": return { textAlign: "center" };
483
+ case "right": return { textAlign: "right" };
484
+ default: return {};
485
+ }
486
+ }
487
+
488
+ //#endregion
489
+ //#region src/extensions/blockquote.tsx
490
+ const Blockquote = EmailNode.from(BlockquoteBase, ({ children, node, style }) => /* @__PURE__ */ jsx("blockquote", {
491
+ className: node.attrs?.class || void 0,
492
+ style: {
493
+ ...style,
494
+ ...inlineCssToJs(node.attrs?.style),
495
+ ...getTextAlignment(node.attrs?.align || node.attrs?.alignment)
496
+ },
497
+ children
498
+ }));
499
+
500
+ //#endregion
501
+ //#region src/utils/attribute-helpers.ts
502
+ /**
503
+ * Creates TipTap attribute definitions for a list of HTML attributes.
504
+ * Each attribute will have the same pattern:
505
+ * - default: null
506
+ * - parseHTML: extracts the attribute from the element
507
+ * - renderHTML: conditionally renders the attribute if it has a value
508
+ *
509
+ * @param attributeNames - Array of HTML attribute names to create definitions for
510
+ * @returns Object with TipTap attribute definitions
511
+ *
512
+ * @example
513
+ * const attrs = createStandardAttributes(['class', 'id', 'title']);
514
+ * // Returns:
515
+ * // {
516
+ * // class: {
517
+ * // default: null,
518
+ * // parseHTML: (element) => element.getAttribute('class'),
519
+ * // renderHTML: (attributes) => attributes.class ? { class: attributes.class } : {}
520
+ * // },
521
+ * // ...
522
+ * // }
523
+ */
524
+ function createStandardAttributes(attributeNames) {
525
+ return Object.fromEntries(attributeNames.map((attr) => [attr, {
526
+ default: null,
527
+ parseHTML: (element) => element.getAttribute(attr),
528
+ renderHTML: (attributes) => {
529
+ if (!attributes[attr]) return {};
530
+ return { [attr]: attributes[attr] };
531
+ }
532
+ }]));
533
+ }
534
+ /**
535
+ * Common HTML attributes used across multiple extensions.
536
+ * These preserve attributes during HTML import and editing for better
537
+ * fidelity when importing existing email templates.
538
+ */
539
+ const COMMON_HTML_ATTRIBUTES = [
540
+ "id",
541
+ "class",
542
+ "title",
543
+ "lang",
544
+ "dir",
545
+ "data-id"
546
+ ];
547
+ /**
548
+ * Layout-specific HTML attributes used for positioning and sizing.
382
549
  */
383
- function resolveConflictingStyles(resetStyles, inlineStyles) {
384
- const expandedResetStyles = expandShorthandProperties(resetStyles);
385
- const expandedInlineStyles = expandShorthandProperties(inlineStyles);
386
- return {
387
- ...expandedResetStyles,
388
- ...expandedInlineStyles
389
- };
390
- }
550
+ const LAYOUT_ATTRIBUTES = [
551
+ "align",
552
+ "width",
553
+ "height"
554
+ ];
555
+ /**
556
+ * Table-specific HTML attributes used for table layout and styling.
557
+ */
558
+ const TABLE_ATTRIBUTES = [
559
+ "border",
560
+ "cellpadding",
561
+ "cellspacing"
562
+ ];
563
+ /**
564
+ * Table cell-specific HTML attributes.
565
+ */
566
+ const TABLE_CELL_ATTRIBUTES = [
567
+ "valign",
568
+ "bgcolor",
569
+ "colspan",
570
+ "rowspan"
571
+ ];
572
+ /**
573
+ * Table header cell-specific HTML attributes.
574
+ * These are additional attributes that only apply to <th> elements.
575
+ */
576
+ const TABLE_HEADER_ATTRIBUTES = [...TABLE_CELL_ATTRIBUTES, "scope"];
391
577
 
392
578
  //#endregion
393
579
  //#region src/extensions/body.tsx
@@ -435,7 +621,7 @@ const Body = EmailNode.create({
435
621
  });
436
622
 
437
623
  //#endregion
438
- //#region src/extensions/bold.ts
624
+ //#region src/extensions/bold.tsx
439
625
  /**
440
626
  * Matches bold text via `**` as input.
441
627
  */
@@ -456,7 +642,7 @@ const underscorePasteRegex = /(?:^|\s)(__(?!\s+__)((?:[^_]+))__(?!\s+__))/g;
456
642
  * This extension allows you to mark text as bold.
457
643
  * @see https://tiptap.dev/api/marks/bold
458
644
  */
459
- const Bold = Mark.create({
645
+ const Bold = EmailMark.create({
460
646
  name: "bold",
461
647
  addOptions() {
462
648
  return { HTMLAttributes: {} };
@@ -481,6 +667,12 @@ const Bold = Mark.create({
481
667
  0
482
668
  ];
483
669
  },
670
+ renderToReactEmail({ children, style }) {
671
+ return /* @__PURE__ */ jsx("strong", {
672
+ style,
673
+ children
674
+ });
675
+ },
484
676
  addCommands() {
485
677
  return {
486
678
  setBold: () => ({ commands }) => {
@@ -520,6 +712,17 @@ const Bold = Mark.create({
520
712
  }
521
713
  });
522
714
 
715
+ //#endregion
716
+ //#region src/extensions/bullet-list.tsx
717
+ const BulletList = EmailNode.from(BulletListBase, ({ children, node, style }) => /* @__PURE__ */ jsx("ul", {
718
+ className: node.attrs?.class || void 0,
719
+ style: {
720
+ ...style,
721
+ ...inlineCssToJs(node.attrs?.style)
722
+ },
723
+ children
724
+ }));
725
+
523
726
  //#endregion
524
727
  //#region src/extensions/button.tsx
525
728
  const Button = EmailNode.create({
@@ -641,6 +844,16 @@ const ClassAttribute = Extension.create({
641
844
  }
642
845
  });
643
846
 
847
+ //#endregion
848
+ //#region src/extensions/code.tsx
849
+ const Code = EmailMark.from(CodeBase, ({ children, node, style }) => /* @__PURE__ */ jsx("code", {
850
+ style: {
851
+ ...style,
852
+ ...inlineCssToJs(node.attrs?.style)
853
+ },
854
+ children
855
+ }));
856
+
644
857
  //#endregion
645
858
  //#region src/utils/prism-utils.ts
646
859
  const publicURL = "/styles/prism";
@@ -919,6 +1132,29 @@ const Div = EmailNode.create({
919
1132
  }
920
1133
  });
921
1134
 
1135
+ //#endregion
1136
+ //#region src/extensions/hard-break.tsx
1137
+ const HardBreak = EmailNode.from(HardBreakBase, () => /* @__PURE__ */ jsx("br", {}));
1138
+
1139
+ //#endregion
1140
+ //#region src/extensions/italic.tsx
1141
+ const Italic = EmailMark.from(ItalicBase, ({ children, style }) => /* @__PURE__ */ jsx("em", {
1142
+ style,
1143
+ children
1144
+ }));
1145
+
1146
+ //#endregion
1147
+ //#region src/extensions/list-item.tsx
1148
+ const ListItem = EmailNode.from(ListItemBase, ({ children, node, style }) => /* @__PURE__ */ jsx("li", {
1149
+ className: node.attrs?.class || void 0,
1150
+ style: {
1151
+ ...style,
1152
+ ...inlineCssToJs(node.attrs?.style),
1153
+ ...getTextAlignment(node.attrs?.align || node.attrs?.alignment)
1154
+ },
1155
+ children
1156
+ }));
1157
+
922
1158
  //#endregion
923
1159
  //#region src/extensions/max-nesting.ts
924
1160
  const MaxNesting = Extension.create({
@@ -994,6 +1230,33 @@ const MaxNesting = Extension.create({
994
1230
  }
995
1231
  });
996
1232
 
1233
+ //#endregion
1234
+ //#region src/extensions/ordered-list.tsx
1235
+ const OrderedList = EmailNode.from(OrderedListBase, ({ children, node, style }) => /* @__PURE__ */ jsx("ol", {
1236
+ className: node.attrs?.class || void 0,
1237
+ start: node.attrs?.start,
1238
+ style: {
1239
+ ...style,
1240
+ ...inlineCssToJs(node.attrs?.style)
1241
+ },
1242
+ children
1243
+ }));
1244
+
1245
+ //#endregion
1246
+ //#region src/extensions/paragraph.tsx
1247
+ const Paragraph = EmailNode.from(ParagraphBase, ({ children, node, style }) => {
1248
+ const isEmpty = !node.content || node.content.length === 0;
1249
+ return /* @__PURE__ */ jsx("p", {
1250
+ className: node.attrs?.class || void 0,
1251
+ style: {
1252
+ ...style,
1253
+ ...inlineCssToJs(node.attrs?.style),
1254
+ ...getTextAlignment(node.attrs?.align || node.attrs?.alignment)
1255
+ },
1256
+ children: isEmpty ? /* @__PURE__ */ jsx("br", {}) : children
1257
+ });
1258
+ });
1259
+
997
1260
  //#endregion
998
1261
  //#region src/extensions/placeholder.ts
999
1262
  const Placeholder = TipTapPlaceholder.configure({
@@ -1005,8 +1268,8 @@ const Placeholder = TipTapPlaceholder.configure({
1005
1268
  });
1006
1269
 
1007
1270
  //#endregion
1008
- //#region src/extensions/preserved-style.ts
1009
- const PreservedStyle = Mark.create({
1271
+ //#region src/extensions/preserved-style.tsx
1272
+ const PreservedStyle = EmailMark.create({
1010
1273
  name: "preservedStyle",
1011
1274
  addAttributes() {
1012
1275
  return { style: {
@@ -1035,6 +1298,12 @@ const PreservedStyle = Mark.create({
1035
1298
  mergeAttributes(HTMLAttributes),
1036
1299
  0
1037
1300
  ];
1301
+ },
1302
+ renderToReactEmail({ children, mark }) {
1303
+ return /* @__PURE__ */ jsx("span", {
1304
+ style: mark.attrs?.style ? inlineCssToJs(mark.attrs.style) : void 0,
1305
+ children
1306
+ });
1038
1307
  }
1039
1308
  });
1040
1309
  const LINK_INDICATOR_STYLES = [
@@ -1080,7 +1349,7 @@ function processStylesForUnlink(styleString) {
1080
1349
 
1081
1350
  //#endregion
1082
1351
  //#region src/extensions/preview-text.ts
1083
- const PreviewText = Node.create({
1352
+ const PreviewText = Node$1.create({
1084
1353
  name: "previewText",
1085
1354
  group: "block",
1086
1355
  selectable: false,
@@ -1119,17 +1388,6 @@ const PreviewText = Node.create({
1119
1388
  }
1120
1389
  });
1121
1390
 
1122
- //#endregion
1123
- //#region src/utils/get-text-alignment.ts
1124
- function getTextAlignment(alignment) {
1125
- switch (alignment) {
1126
- case "left": return { textAlign: "left" };
1127
- case "center": return { textAlign: "center" };
1128
- case "right": return { textAlign: "right" };
1129
- default: return {};
1130
- }
1131
- }
1132
-
1133
1391
  //#endregion
1134
1392
  //#region src/extensions/section.tsx
1135
1393
  const Section = EmailNode.create({
@@ -1178,6 +1436,13 @@ const Section = EmailNode.create({
1178
1436
  }
1179
1437
  });
1180
1438
 
1439
+ //#endregion
1440
+ //#region src/extensions/strike.tsx
1441
+ const Strike = EmailMark.from(StrikeBase, ({ children, style }) => /* @__PURE__ */ jsx("s", {
1442
+ style,
1443
+ children
1444
+ }));
1445
+
1181
1446
  //#endregion
1182
1447
  //#region src/extensions/style-attribute.tsx
1183
1448
  const StyleAttribute = Extension.create({
@@ -1227,12 +1492,12 @@ const StyleAttribute = Extension.create({
1227
1492
  });
1228
1493
 
1229
1494
  //#endregion
1230
- //#region src/extensions/sup.ts
1495
+ //#region src/extensions/sup.tsx
1231
1496
  /**
1232
1497
  * This extension allows you to mark text as superscript.
1233
1498
  * @see https://tiptap.dev/api/marks/superscript
1234
1499
  */
1235
- const Sup = Mark.create({
1500
+ const Sup = EmailMark.create({
1236
1501
  name: "sup",
1237
1502
  addOptions() {
1238
1503
  return { HTMLAttributes: {} };
@@ -1247,6 +1512,12 @@ const Sup = Mark.create({
1247
1512
  0
1248
1513
  ];
1249
1514
  },
1515
+ renderToReactEmail({ children, style }) {
1516
+ return /* @__PURE__ */ jsx("sup", {
1517
+ style,
1518
+ children
1519
+ });
1520
+ },
1250
1521
  addCommands() {
1251
1522
  return {
1252
1523
  setSup: () => ({ commands }) => {
@@ -1412,7 +1683,7 @@ const TableCell = EmailNode.create({
1412
1683
  });
1413
1684
  }
1414
1685
  });
1415
- const TableHeader = Node.create({
1686
+ const TableHeader = Node$1.create({
1416
1687
  name: "tableHeader",
1417
1688
  group: "tableCell",
1418
1689
  content: "block+",
@@ -1449,8 +1720,8 @@ const TableHeader = Node.create({
1449
1720
  });
1450
1721
 
1451
1722
  //#endregion
1452
- //#region src/extensions/uppercase.ts
1453
- const Uppercase = Mark.create({
1723
+ //#region src/extensions/uppercase.tsx
1724
+ const Uppercase = EmailMark.create({
1454
1725
  name: "uppercase",
1455
1726
  addOptions() {
1456
1727
  return { HTMLAttributes: {} };
@@ -1471,6 +1742,15 @@ const Uppercase = Mark.create({
1471
1742
  0
1472
1743
  ];
1473
1744
  },
1745
+ renderToReactEmail({ children, style }) {
1746
+ return /* @__PURE__ */ jsx("span", {
1747
+ style: {
1748
+ ...style,
1749
+ textTransform: "uppercase"
1750
+ },
1751
+ children
1752
+ });
1753
+ },
1474
1754
  addCommands() {
1475
1755
  return {
1476
1756
  setUppercase: () => ({ commands }) => {
@@ -1666,17 +1946,17 @@ const coreExtensions = [
1666
1946
  underline: false,
1667
1947
  trailingNode: false,
1668
1948
  bold: false,
1949
+ italic: false,
1950
+ strike: false,
1951
+ code: false,
1952
+ paragraph: false,
1953
+ bulletList: false,
1954
+ orderedList: false,
1955
+ listItem: false,
1956
+ blockquote: false,
1957
+ hardBreak: false,
1669
1958
  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
1959
  codeBlock: false,
1676
- code: { HTMLAttributes: {
1677
- class: "node-inlineCode",
1678
- spellcheck: "false"
1679
- } },
1680
1960
  horizontalRule: false,
1681
1961
  dropcursor: {
1682
1962
  color: "#61a8f8",
@@ -1688,9 +1968,21 @@ const coreExtensions = [
1688
1968
  defaultLanguage: "javascript",
1689
1969
  HTMLAttributes: { class: "prism node-codeBlock" }
1690
1970
  }),
1971
+ Code.configure({ HTMLAttributes: {
1972
+ class: "node-inlineCode",
1973
+ spellcheck: "false"
1974
+ } }),
1975
+ Paragraph.configure({ HTMLAttributes: { class: "node-paragraph" } }),
1976
+ BulletList.configure({ HTMLAttributes: { class: "node-bulletList" } }),
1977
+ OrderedList.configure({ HTMLAttributes: { class: "node-orderedList" } }),
1978
+ Blockquote.configure({ HTMLAttributes: { class: "node-blockquote" } }),
1979
+ ListItem,
1980
+ HardBreak,
1981
+ Italic,
1691
1982
  Placeholder,
1692
1983
  PreviewText,
1693
1984
  Bold,
1985
+ Strike,
1694
1986
  Sup,
1695
1987
  Uppercase,
1696
1988
  PreservedStyle,
@@ -1777,6 +2069,221 @@ const coreExtensions = [
1777
2069
  })
1778
2070
  ];
1779
2071
 
2072
+ //#endregion
2073
+ //#region src/core/create-drop-handler.ts
2074
+ function createDropHandler({ onPaste, onUploadImage }) {
2075
+ return (view, event, _slice, moved) => {
2076
+ if (!moved && event.dataTransfer && event.dataTransfer.files && event.dataTransfer.files[0]) {
2077
+ event.preventDefault();
2078
+ const file = event.dataTransfer.files[0];
2079
+ if (onPaste?.(file, view)) return true;
2080
+ if (file.type.includes("image/") && onUploadImage) {
2081
+ onUploadImage(file, view, (view.posAtCoords({
2082
+ left: event.clientX,
2083
+ top: event.clientY
2084
+ })?.pos || 0) - 1);
2085
+ return true;
2086
+ }
2087
+ }
2088
+ return false;
2089
+ };
2090
+ }
2091
+
2092
+ //#endregion
2093
+ //#region src/utils/paste-sanitizer.ts
2094
+ /**
2095
+ * Sanitizes pasted HTML.
2096
+ * - From editor (has node-* classes): pass through as-is
2097
+ * - From external: strip all styles/classes, keep only semantic HTML
2098
+ */
2099
+ /**
2100
+ * Detects content from the Resend editor by checking for node-* class names.
2101
+ */
2102
+ const EDITOR_CLASS_PATTERN = /class="[^"]*node-/;
2103
+ /**
2104
+ * Attributes to preserve on specific elements for EXTERNAL content.
2105
+ * Only functional attributes - NO style or class.
2106
+ */
2107
+ const PRESERVED_ATTRIBUTES = {
2108
+ a: [
2109
+ "href",
2110
+ "target",
2111
+ "rel"
2112
+ ],
2113
+ img: [
2114
+ "src",
2115
+ "alt",
2116
+ "width",
2117
+ "height"
2118
+ ],
2119
+ td: ["colspan", "rowspan"],
2120
+ th: [
2121
+ "colspan",
2122
+ "rowspan",
2123
+ "scope"
2124
+ ],
2125
+ table: [
2126
+ "border",
2127
+ "cellpadding",
2128
+ "cellspacing"
2129
+ ],
2130
+ "*": ["id"]
2131
+ };
2132
+ function isFromEditor(html) {
2133
+ return EDITOR_CLASS_PATTERN.test(html);
2134
+ }
2135
+ function sanitizePastedHtml(html) {
2136
+ if (isFromEditor(html)) return html;
2137
+ const doc = new DOMParser().parseFromString(html, "text/html");
2138
+ sanitizeNode(doc.body);
2139
+ return doc.body.innerHTML;
2140
+ }
2141
+ function sanitizeNode(node) {
2142
+ if (node.nodeType === Node.ELEMENT_NODE) sanitizeElement(node);
2143
+ for (const child of Array.from(node.childNodes)) sanitizeNode(child);
2144
+ }
2145
+ function sanitizeElement(el) {
2146
+ const allowedForTag = PRESERVED_ATTRIBUTES[el.tagName.toLowerCase()] || [];
2147
+ const allowedGlobal = PRESERVED_ATTRIBUTES["*"] || [];
2148
+ const allowed = new Set([...allowedForTag, ...allowedGlobal]);
2149
+ const attributesToRemove = [];
2150
+ for (const attr of Array.from(el.attributes)) {
2151
+ if (attr.name.startsWith("data-")) {
2152
+ attributesToRemove.push(attr.name);
2153
+ continue;
2154
+ }
2155
+ if (!allowed.has(attr.name)) attributesToRemove.push(attr.name);
2156
+ }
2157
+ for (const attr of attributesToRemove) el.removeAttribute(attr);
2158
+ }
2159
+
2160
+ //#endregion
2161
+ //#region src/core/create-paste-handler.ts
2162
+ function createPasteHandler({ onPaste, onUploadImage, extensions }) {
2163
+ return (view, event, slice) => {
2164
+ const text = event.clipboardData?.getData("text/plain");
2165
+ if (text && onPaste?.(text, view)) {
2166
+ event.preventDefault();
2167
+ return true;
2168
+ }
2169
+ if (event.clipboardData?.files?.[0]) {
2170
+ const file = event.clipboardData.files[0];
2171
+ if (onPaste?.(file, view)) {
2172
+ event.preventDefault();
2173
+ return true;
2174
+ }
2175
+ if (file.type.includes("image/") && onUploadImage) {
2176
+ const pos = view.state.selection.from;
2177
+ onUploadImage(file, view, pos);
2178
+ return true;
2179
+ }
2180
+ }
2181
+ /**
2182
+ * If the coming content has a single child, we can assume
2183
+ * it's a plain text and doesn't need to be parsed and
2184
+ * be introduced in a new line
2185
+ */
2186
+ if (slice.content.childCount === 1) return false;
2187
+ if (event.clipboardData?.getData?.("text/html")) {
2188
+ event.preventDefault();
2189
+ const jsonContent = generateJSON(sanitizePastedHtml(event.clipboardData.getData("text/html")), extensions);
2190
+ const node = view.state.schema.nodeFromJSON(jsonContent);
2191
+ const transaction = view.state.tr.replaceSelectionWith(node, false);
2192
+ view.dispatch(transaction);
2193
+ return true;
2194
+ }
2195
+ return false;
2196
+ };
2197
+ }
2198
+
2199
+ //#endregion
2200
+ //#region src/core/is-document-visually-empty.ts
2201
+ function isDocumentVisuallyEmpty(doc) {
2202
+ let nonGlobalNodeCount = 0;
2203
+ let firstNonGlobalNode = null;
2204
+ for (let index = 0; index < doc.childCount; index += 1) {
2205
+ const node = doc.child(index);
2206
+ if (node.type.name === "globalContent") continue;
2207
+ nonGlobalNodeCount += 1;
2208
+ if (firstNonGlobalNode === null) firstNonGlobalNode = {
2209
+ type: node.type,
2210
+ textContent: node.textContent,
2211
+ childCount: node.content.childCount
2212
+ };
2213
+ }
2214
+ if (nonGlobalNodeCount === 0) return true;
2215
+ if (nonGlobalNodeCount !== 1) return false;
2216
+ return firstNonGlobalNode?.type.name === "paragraph" && firstNonGlobalNode.textContent.trim().length === 0 && firstNonGlobalNode.childCount === 0;
2217
+ }
2218
+
2219
+ //#endregion
2220
+ //#region src/core/use-editor.ts
2221
+ const COLLABORATION_EXTENSION_NAMES = new Set(["liveblocksExtension", "collaboration"]);
2222
+ function hasCollaborationExtension(exts) {
2223
+ return exts.some((ext) => COLLABORATION_EXTENSION_NAMES.has(ext.name));
2224
+ }
2225
+ function useEditor({ content, extensions = [], onUpdate, onPaste, onUploadImage, onReady, editable = true, ...rest }) {
2226
+ const [contentError, setContentError] = React.useState(null);
2227
+ const isCollaborative = hasCollaborationExtension(extensions);
2228
+ const effectiveExtensions = React.useMemo(() => [
2229
+ ...coreExtensions,
2230
+ ...isCollaborative ? [] : [UndoRedo],
2231
+ ...extensions
2232
+ ], [extensions, isCollaborative]);
2233
+ const editor = useEditor$1({
2234
+ content: isCollaborative ? void 0 : content,
2235
+ extensions: effectiveExtensions,
2236
+ immediatelyRender: false,
2237
+ enableContentCheck: true,
2238
+ onContentError({ editor: editor$1, error, disableCollaboration }) {
2239
+ disableCollaboration();
2240
+ setContentError(error);
2241
+ console.error(error);
2242
+ editor$1.setEditable(false);
2243
+ },
2244
+ onCreate({ editor: editor$1 }) {
2245
+ onReady?.(editor$1);
2246
+ },
2247
+ onUpdate({ editor: editor$1, transaction }) {
2248
+ onUpdate?.(editor$1, transaction);
2249
+ },
2250
+ editorProps: {
2251
+ handleDOMEvents: { click: (view, event) => {
2252
+ if (!view.editable) {
2253
+ if (event.target.closest("a")) {
2254
+ event.preventDefault();
2255
+ return true;
2256
+ }
2257
+ }
2258
+ return false;
2259
+ } },
2260
+ handlePaste: createPasteHandler({
2261
+ onPaste,
2262
+ onUploadImage,
2263
+ extensions: effectiveExtensions
2264
+ }),
2265
+ handleDrop: createDropHandler({
2266
+ onPaste,
2267
+ onUploadImage
2268
+ })
2269
+ },
2270
+ ...rest
2271
+ });
2272
+ return {
2273
+ editor,
2274
+ isEditorEmpty: useEditorState({
2275
+ editor,
2276
+ selector: (context) => {
2277
+ if (!context.editor) return true;
2278
+ return isDocumentVisuallyEmpty(context.editor.state.doc);
2279
+ }
2280
+ }) ?? true,
2281
+ extensions: effectiveExtensions,
2282
+ contentError,
2283
+ isCollaborative
2284
+ };
2285
+ }
2286
+
1780
2287
  //#endregion
1781
2288
  //#region src/utils/set-text-alignment.ts
1782
2289
  function setTextAlignment(editor, alignment) {
@@ -2216,7 +2723,7 @@ function NodeSelectorRoot({ omit = [], open: controlledOpen, onOpenChange, class
2216
2723
  },
2217
2724
  {
2218
2725
  name: "Code",
2219
- icon: Code,
2726
+ icon: Code$1,
2220
2727
  command: () => editor.chain().focus().clearNodes().toggleCodeBlock().run(),
2221
2728
  isActive: editorState?.isCodeBlockActive ?? false
2222
2729
  }
@@ -2881,5 +3388,463 @@ const LinkBubbleMenu = {
2881
3388
  };
2882
3389
 
2883
3390
  //#endregion
2884
- export { AlignmentAttribute, Body, Bold, BubbleMenu, BubbleMenuAlignCenter, BubbleMenuAlignLeft, BubbleMenuAlignRight, BubbleMenuBold, BubbleMenuCode, BubbleMenuDefault, BubbleMenuItalic, BubbleMenuItem, BubbleMenuItemGroup, BubbleMenuLinkSelector, BubbleMenuNodeSelector, BubbleMenuRoot, BubbleMenuSeparator, BubbleMenuStrike, BubbleMenuUnderline, BubbleMenuUppercase, Button, ButtonBubbleMenu, ButtonBubbleMenuDefault, ButtonBubbleMenuEditLink, ButtonBubbleMenuRoot, ButtonBubbleMenuToolbar, COLUMN_PARENT_TYPES, ClassAttribute, CodeBlockPrism, ColumnsColumn, Div, EmailNode, FourColumns, ImageBubbleMenu, ImageBubbleMenuDefault, ImageBubbleMenuEditLink, ImageBubbleMenuRoot, ImageBubbleMenuToolbar, LinkBubbleMenu, LinkBubbleMenuDefault, 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, useButtonBubbleMenuContext, useImageBubbleMenuContext, useLinkBubbleMenuContext };
3391
+ //#region src/ui/slash-command/utils.ts
3392
+ function isInsideNode(editor, type) {
3393
+ const { $from } = editor.state.selection;
3394
+ for (let d = $from.depth; d > 0; d--) if ($from.node(d).type.name === type) return true;
3395
+ return false;
3396
+ }
3397
+ function isAtMaxColumnsDepth(editor) {
3398
+ const { from } = editor.state.selection;
3399
+ return getColumnsDepth(editor.state.doc, from) >= MAX_COLUMNS_DEPTH;
3400
+ }
3401
+ function updateScrollView(container, item) {
3402
+ const containerRect = container.getBoundingClientRect();
3403
+ const itemRect = item.getBoundingClientRect();
3404
+ if (itemRect.top < containerRect.top) container.scrollTop -= containerRect.top - itemRect.top;
3405
+ else if (itemRect.bottom > containerRect.bottom) container.scrollTop += itemRect.bottom - containerRect.bottom;
3406
+ }
3407
+
3408
+ //#endregion
3409
+ //#region src/ui/slash-command/command-list.tsx
3410
+ const CATEGORY_ORDER = [
3411
+ "Text",
3412
+ "Media",
3413
+ "Layout",
3414
+ "Utility"
3415
+ ];
3416
+ function groupByCategory(items) {
3417
+ const seen = /* @__PURE__ */ new Map();
3418
+ for (const item of items) {
3419
+ const existing = seen.get(item.category);
3420
+ if (existing) existing.push(item);
3421
+ else seen.set(item.category, [item]);
3422
+ }
3423
+ const ordered = [];
3424
+ for (const cat of CATEGORY_ORDER) {
3425
+ const group = seen.get(cat);
3426
+ if (group) {
3427
+ ordered.push({
3428
+ category: cat,
3429
+ items: group
3430
+ });
3431
+ seen.delete(cat);
3432
+ }
3433
+ }
3434
+ for (const [category, group] of seen) ordered.push({
3435
+ category,
3436
+ items: group
3437
+ });
3438
+ return ordered;
3439
+ }
3440
+ function CommandItem({ item, selected, onSelect }) {
3441
+ return /* @__PURE__ */ jsxs("button", {
3442
+ "data-re-slash-command-item": "",
3443
+ "data-selected": selected || void 0,
3444
+ onClick: onSelect,
3445
+ type: "button",
3446
+ children: [item.icon, /* @__PURE__ */ jsx("span", { children: item.title })]
3447
+ });
3448
+ }
3449
+ function CommandList({ items, command, query, ref }) {
3450
+ const [selectedIndex, setSelectedIndex] = useState(0);
3451
+ const containerRef = useRef(null);
3452
+ useEffect(() => {
3453
+ setSelectedIndex(0);
3454
+ }, [items]);
3455
+ useLayoutEffect(() => {
3456
+ const container = containerRef.current;
3457
+ if (!container) return;
3458
+ const selected = container.querySelector("[data-selected]");
3459
+ if (selected) updateScrollView(container, selected);
3460
+ }, [selectedIndex]);
3461
+ const selectItem = useCallback((index) => {
3462
+ const item = items[index];
3463
+ if (item) command(item);
3464
+ }, [items, command]);
3465
+ useImperativeHandle(ref, () => ({ onKeyDown: ({ event }) => {
3466
+ if (items.length === 0) return false;
3467
+ if (event.key === "ArrowUp") {
3468
+ setSelectedIndex((i) => (i + items.length - 1) % items.length);
3469
+ return true;
3470
+ }
3471
+ if (event.key === "ArrowDown") {
3472
+ setSelectedIndex((i) => (i + 1) % items.length);
3473
+ return true;
3474
+ }
3475
+ if (event.key === "Enter") {
3476
+ selectItem(selectedIndex);
3477
+ return true;
3478
+ }
3479
+ return false;
3480
+ } }), [
3481
+ items.length,
3482
+ selectItem,
3483
+ selectedIndex
3484
+ ]);
3485
+ if (items.length === 0) return /* @__PURE__ */ jsx("div", {
3486
+ "data-re-slash-command": "",
3487
+ children: /* @__PURE__ */ jsx("div", {
3488
+ "data-re-slash-command-empty": "",
3489
+ children: "No results"
3490
+ })
3491
+ });
3492
+ if (query.trim().length > 0) return /* @__PURE__ */ jsx("div", {
3493
+ "data-re-slash-command": "",
3494
+ ref: containerRef,
3495
+ children: items.map((item, index) => /* @__PURE__ */ jsx(CommandItem, {
3496
+ item,
3497
+ onSelect: () => selectItem(index),
3498
+ selected: index === selectedIndex
3499
+ }, item.title))
3500
+ });
3501
+ const groups = groupByCategory(items);
3502
+ let flatIndex = 0;
3503
+ return /* @__PURE__ */ jsx("div", {
3504
+ "data-re-slash-command": "",
3505
+ ref: containerRef,
3506
+ children: groups.map((group) => /* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx("div", {
3507
+ "data-re-slash-command-category": "",
3508
+ children: group.category
3509
+ }), group.items.map((item) => {
3510
+ const currentIndex = flatIndex++;
3511
+ return /* @__PURE__ */ jsx(CommandItem, {
3512
+ item,
3513
+ onSelect: () => selectItem(currentIndex),
3514
+ selected: currentIndex === selectedIndex
3515
+ }, item.title);
3516
+ })] }, group.category))
3517
+ });
3518
+ }
3519
+
3520
+ //#endregion
3521
+ //#region src/ui/slash-command/commands.tsx
3522
+ const TEXT = {
3523
+ title: "Text",
3524
+ description: "Plain text block",
3525
+ icon: /* @__PURE__ */ jsx(Text, { size: 20 }),
3526
+ category: "Text",
3527
+ searchTerms: ["p", "paragraph"],
3528
+ command: ({ editor, range }) => {
3529
+ editor.chain().focus().deleteRange(range).toggleNode("paragraph", "paragraph").run();
3530
+ }
3531
+ };
3532
+ const H1 = {
3533
+ title: "Title",
3534
+ description: "Large heading",
3535
+ icon: /* @__PURE__ */ jsx(Heading1, { size: 20 }),
3536
+ category: "Text",
3537
+ searchTerms: [
3538
+ "title",
3539
+ "big",
3540
+ "large",
3541
+ "h1"
3542
+ ],
3543
+ command: ({ editor, range }) => {
3544
+ editor.chain().focus().deleteRange(range).setNode("heading", { level: 1 }).run();
3545
+ }
3546
+ };
3547
+ const H2 = {
3548
+ title: "Subtitle",
3549
+ description: "Medium heading",
3550
+ icon: /* @__PURE__ */ jsx(Heading2, { size: 20 }),
3551
+ category: "Text",
3552
+ searchTerms: [
3553
+ "subtitle",
3554
+ "medium",
3555
+ "h2"
3556
+ ],
3557
+ command: ({ editor, range }) => {
3558
+ editor.chain().focus().deleteRange(range).setNode("heading", { level: 2 }).run();
3559
+ }
3560
+ };
3561
+ const H3 = {
3562
+ title: "Heading",
3563
+ description: "Small heading",
3564
+ icon: /* @__PURE__ */ jsx(Heading3, { size: 20 }),
3565
+ category: "Text",
3566
+ searchTerms: [
3567
+ "subtitle",
3568
+ "small",
3569
+ "h3"
3570
+ ],
3571
+ command: ({ editor, range }) => {
3572
+ editor.chain().focus().deleteRange(range).setNode("heading", { level: 3 }).run();
3573
+ }
3574
+ };
3575
+ const BULLET_LIST = {
3576
+ title: "Bullet list",
3577
+ description: "Unordered list",
3578
+ icon: /* @__PURE__ */ jsx(List, { size: 20 }),
3579
+ category: "Text",
3580
+ searchTerms: ["unordered", "point"],
3581
+ command: ({ editor, range }) => {
3582
+ editor.chain().focus().deleteRange(range).toggleBulletList().run();
3583
+ }
3584
+ };
3585
+ const NUMBERED_LIST = {
3586
+ title: "Numbered list",
3587
+ description: "Ordered list",
3588
+ icon: /* @__PURE__ */ jsx(ListOrdered, { size: 20 }),
3589
+ category: "Text",
3590
+ searchTerms: ["ordered"],
3591
+ command: ({ editor, range }) => {
3592
+ editor.chain().focus().deleteRange(range).toggleOrderedList().run();
3593
+ }
3594
+ };
3595
+ const QUOTE = {
3596
+ title: "Quote",
3597
+ description: "Block quote",
3598
+ icon: /* @__PURE__ */ jsx(TextQuote, { size: 20 }),
3599
+ category: "Text",
3600
+ searchTerms: ["blockquote"],
3601
+ command: ({ editor, range }) => {
3602
+ editor.chain().focus().deleteRange(range).toggleNode("paragraph", "paragraph").toggleBlockquote().run();
3603
+ }
3604
+ };
3605
+ const CODE = {
3606
+ title: "Code block",
3607
+ description: "Code snippet",
3608
+ icon: /* @__PURE__ */ jsx(SquareCode, { size: 20 }),
3609
+ category: "Text",
3610
+ searchTerms: ["codeblock"],
3611
+ command: ({ editor, range }) => {
3612
+ editor.chain().focus().deleteRange(range).toggleCodeBlock().run();
3613
+ }
3614
+ };
3615
+ const BUTTON = {
3616
+ title: "Button",
3617
+ description: "Clickable button",
3618
+ icon: /* @__PURE__ */ jsx(MousePointer, { size: 20 }),
3619
+ category: "Layout",
3620
+ searchTerms: ["button"],
3621
+ command: ({ editor, range }) => {
3622
+ editor.chain().focus().deleteRange(range).setButton().run();
3623
+ }
3624
+ };
3625
+ const DIVIDER = {
3626
+ title: "Divider",
3627
+ description: "Horizontal separator",
3628
+ icon: /* @__PURE__ */ jsx(SplitSquareVertical, { size: 20 }),
3629
+ category: "Layout",
3630
+ searchTerms: [
3631
+ "hr",
3632
+ "divider",
3633
+ "separator"
3634
+ ],
3635
+ command: ({ editor, range }) => {
3636
+ editor.chain().focus().deleteRange(range).setHorizontalRule().run();
3637
+ }
3638
+ };
3639
+ const SECTION = {
3640
+ title: "Section",
3641
+ description: "Content section",
3642
+ icon: /* @__PURE__ */ jsx(Rows2, { size: 20 }),
3643
+ category: "Layout",
3644
+ searchTerms: [
3645
+ "section",
3646
+ "row",
3647
+ "container"
3648
+ ],
3649
+ command: ({ editor, range }) => {
3650
+ editor.chain().focus().deleteRange(range).insertSection().run();
3651
+ }
3652
+ };
3653
+ const TWO_COLUMNS = {
3654
+ title: "2 columns",
3655
+ description: "Two column layout",
3656
+ icon: /* @__PURE__ */ jsx(Columns2, { size: 20 }),
3657
+ category: "Layout",
3658
+ searchTerms: [
3659
+ "columns",
3660
+ "column",
3661
+ "layout",
3662
+ "grid",
3663
+ "split",
3664
+ "side-by-side",
3665
+ "multi-column",
3666
+ "row",
3667
+ "two",
3668
+ "2"
3669
+ ],
3670
+ command: ({ editor, range }) => {
3671
+ editor.chain().focus().deleteRange(range).insertColumns(2).run();
3672
+ }
3673
+ };
3674
+ const THREE_COLUMNS = {
3675
+ title: "3 columns",
3676
+ description: "Three column layout",
3677
+ icon: /* @__PURE__ */ jsx(Columns3, { size: 20 }),
3678
+ category: "Layout",
3679
+ searchTerms: [
3680
+ "columns",
3681
+ "column",
3682
+ "layout",
3683
+ "grid",
3684
+ "split",
3685
+ "multi-column",
3686
+ "row",
3687
+ "three",
3688
+ "3"
3689
+ ],
3690
+ command: ({ editor, range }) => {
3691
+ editor.chain().focus().deleteRange(range).insertColumns(3).run();
3692
+ }
3693
+ };
3694
+ const FOUR_COLUMNS = {
3695
+ title: "4 columns",
3696
+ description: "Four column layout",
3697
+ icon: /* @__PURE__ */ jsx(Columns4, { size: 20 }),
3698
+ category: "Layout",
3699
+ searchTerms: [
3700
+ "columns",
3701
+ "column",
3702
+ "layout",
3703
+ "grid",
3704
+ "split",
3705
+ "multi-column",
3706
+ "row",
3707
+ "four",
3708
+ "4"
3709
+ ],
3710
+ command: ({ editor, range }) => {
3711
+ editor.chain().focus().deleteRange(range).insertColumns(4).run();
3712
+ }
3713
+ };
3714
+ const defaultSlashCommands = [
3715
+ TEXT,
3716
+ H1,
3717
+ H2,
3718
+ H3,
3719
+ BULLET_LIST,
3720
+ NUMBERED_LIST,
3721
+ QUOTE,
3722
+ CODE,
3723
+ BUTTON,
3724
+ DIVIDER,
3725
+ SECTION,
3726
+ TWO_COLUMNS,
3727
+ THREE_COLUMNS,
3728
+ FOUR_COLUMNS
3729
+ ];
3730
+
3731
+ //#endregion
3732
+ //#region src/ui/slash-command/extension.ts
3733
+ const SlashCommandExtension = Extension.create({
3734
+ name: "slash-command",
3735
+ addOptions() {
3736
+ return { suggestion: {
3737
+ char: "/",
3738
+ allow: ({ editor }) => !editor.isActive("codeBlock"),
3739
+ command: ({ editor, range, props }) => {
3740
+ props.command({
3741
+ editor,
3742
+ range
3743
+ });
3744
+ }
3745
+ } };
3746
+ },
3747
+ addProseMirrorPlugins() {
3748
+ return [Suggestion({
3749
+ pluginKey: new PluginKey("slash-command"),
3750
+ editor: this.editor,
3751
+ ...this.options.suggestion
3752
+ })];
3753
+ }
3754
+ });
3755
+
3756
+ //#endregion
3757
+ //#region src/ui/slash-command/render.tsx
3758
+ function createRenderItems(component = CommandList) {
3759
+ return () => {
3760
+ let renderer = null;
3761
+ let popup = null;
3762
+ return {
3763
+ onStart: (props) => {
3764
+ renderer = new ReactRenderer(component, {
3765
+ props,
3766
+ editor: props.editor
3767
+ });
3768
+ if (!props.clientRect) return;
3769
+ popup = tippy("body", {
3770
+ getReferenceClientRect: props.clientRect,
3771
+ appendTo: () => document.body,
3772
+ content: renderer.element,
3773
+ showOnCreate: true,
3774
+ interactive: true,
3775
+ trigger: "manual",
3776
+ placement: "bottom-start"
3777
+ });
3778
+ },
3779
+ onUpdate: (props) => {
3780
+ if (!renderer) return;
3781
+ renderer.updateProps(props);
3782
+ if (popup?.[0] && props.clientRect) popup[0].setProps({ getReferenceClientRect: props.clientRect });
3783
+ },
3784
+ onKeyDown: (props) => {
3785
+ if (props.event.key === "Escape") {
3786
+ popup?.[0]?.hide();
3787
+ return true;
3788
+ }
3789
+ return renderer?.ref?.onKeyDown(props) ?? false;
3790
+ },
3791
+ onExit: () => {
3792
+ popup?.[0]?.destroy();
3793
+ renderer?.destroy();
3794
+ popup = null;
3795
+ renderer = null;
3796
+ }
3797
+ };
3798
+ };
3799
+ }
3800
+
3801
+ //#endregion
3802
+ //#region src/ui/slash-command/search.ts
3803
+ function scoreItem(item, query) {
3804
+ if (!query) return 100;
3805
+ const q = query.toLowerCase();
3806
+ const title = item.title.toLowerCase();
3807
+ const description = item.description.toLowerCase();
3808
+ const terms = item.searchTerms?.map((t) => t.toLowerCase()) ?? [];
3809
+ if (title === q) return 100;
3810
+ if (title.startsWith(q)) return 90;
3811
+ if (title.split(/\s+/).some((w) => w.startsWith(q))) return 80;
3812
+ if (terms.some((t) => t === q)) return 70;
3813
+ if (terms.some((t) => t.startsWith(q))) return 60;
3814
+ if (title.includes(q)) return 40;
3815
+ if (terms.some((t) => t.includes(q))) return 30;
3816
+ if (description.includes(q)) return 20;
3817
+ return 0;
3818
+ }
3819
+ function filterAndRankItems(items, query) {
3820
+ const trimmed = query.trim();
3821
+ if (!trimmed) return items;
3822
+ const scored = items.map((item) => ({
3823
+ item,
3824
+ score: scoreItem(item, trimmed)
3825
+ })).filter(({ score }) => score > 0);
3826
+ scored.sort((a, b) => b.score - a.score);
3827
+ return scored.map(({ item }) => item);
3828
+ }
3829
+
3830
+ //#endregion
3831
+ //#region src/ui/slash-command/create-slash-command.ts
3832
+ function defaultFilterItems(items, query, editor) {
3833
+ return filterAndRankItems(isAtMaxColumnsDepth(editor) ? items.filter((item) => item.category !== "Layout" || !item.title.includes("column")) : items, query);
3834
+ }
3835
+ function createSlashCommand(options) {
3836
+ const items = options?.items ?? defaultSlashCommands;
3837
+ const filterFn = options?.filterItems ?? defaultFilterItems;
3838
+ return SlashCommandExtension.configure({ suggestion: {
3839
+ items: ({ query, editor }) => filterFn(items, query, editor),
3840
+ render: createRenderItems(options?.component)
3841
+ } });
3842
+ }
3843
+
3844
+ //#endregion
3845
+ //#region src/ui/slash-command/index.ts
3846
+ const SlashCommand = createSlashCommand();
3847
+
3848
+ //#endregion
3849
+ export { AlignmentAttribute, BULLET_LIST, BUTTON, Blockquote, Body, Bold, BubbleMenu, BubbleMenuAlignCenter, BubbleMenuAlignLeft, BubbleMenuAlignRight, BubbleMenuBold, BubbleMenuCode, BubbleMenuDefault, BubbleMenuItalic, BubbleMenuItem, BubbleMenuItemGroup, BubbleMenuLinkSelector, BubbleMenuNodeSelector, BubbleMenuRoot, BubbleMenuSeparator, BubbleMenuStrike, BubbleMenuUnderline, BubbleMenuUppercase, BulletList, Button, ButtonBubbleMenu, ButtonBubbleMenuDefault, ButtonBubbleMenuEditLink, ButtonBubbleMenuRoot, ButtonBubbleMenuToolbar, CODE, COLUMN_PARENT_TYPES, ClassAttribute, Code, CodeBlockPrism, ColumnsColumn, CommandList, DIVIDER, Div, EmailNode, FOUR_COLUMNS, FourColumns, H1, H2, H3, HardBreak, ImageBubbleMenu, ImageBubbleMenuDefault, ImageBubbleMenuEditLink, ImageBubbleMenuRoot, ImageBubbleMenuToolbar, Italic, LinkBubbleMenu, LinkBubbleMenuDefault, LinkBubbleMenuEditLink, LinkBubbleMenuForm, LinkBubbleMenuOpenLink, LinkBubbleMenuRoot, LinkBubbleMenuToolbar, LinkBubbleMenuUnlink, ListItem, MAX_COLUMNS_DEPTH, MaxNesting, NUMBERED_LIST, NodeSelectorContent, NodeSelectorRoot, NodeSelectorTrigger, OrderedList, Paragraph, Placeholder, PreservedStyle, PreviewText, QUOTE, SECTION, Section, SlashCommand, Strike, StyleAttribute, Sup, TEXT, THREE_COLUMNS, TWO_COLUMNS, Table, TableCell, TableHeader, TableRow, ThreeColumns, TwoColumns, Uppercase, composeReactEmail, coreExtensions, createSlashCommand, defaultSlashCommands, editorEventBus, filterAndRankItems, getColumnsDepth, isAtMaxColumnsDepth, isInsideNode, processStylesForUnlink, scoreItem, setTextAlignment, useButtonBubbleMenuContext, useEditor, useImageBubbleMenuContext, useLinkBubbleMenuContext };
2885
3850
  //# sourceMappingURL=index.mjs.map