@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.cjs CHANGED
@@ -25,11 +25,21 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
25
25
  }) : target, mod));
26
26
 
27
27
  //#endregion
28
- let _tiptap_core = require("@tiptap/core");
29
- let _tiptap_starter_kit = require("@tiptap/starter-kit");
30
- let react_jsx_runtime = require("react/jsx-runtime");
31
28
  let _react_email_components = require("@react-email/components");
32
29
  _react_email_components = __toESM(_react_email_components);
30
+ let react_jsx_runtime = require("react/jsx-runtime");
31
+ let _tiptap_core = require("@tiptap/core");
32
+ let _tiptap_extensions = require("@tiptap/extensions");
33
+ let _tiptap_react = require("@tiptap/react");
34
+ let react = require("react");
35
+ react = __toESM(react);
36
+ let _tiptap_starter_kit = require("@tiptap/starter-kit");
37
+ let _tiptap_extension_blockquote = require("@tiptap/extension-blockquote");
38
+ _tiptap_extension_blockquote = __toESM(_tiptap_extension_blockquote);
39
+ let _tiptap_extension_bullet_list = require("@tiptap/extension-bullet-list");
40
+ _tiptap_extension_bullet_list = __toESM(_tiptap_extension_bullet_list);
41
+ let _tiptap_extension_code = require("@tiptap/extension-code");
42
+ _tiptap_extension_code = __toESM(_tiptap_extension_code);
33
43
  let _tiptap_extension_code_block = require("@tiptap/extension-code-block");
34
44
  _tiptap_extension_code_block = __toESM(_tiptap_extension_code_block);
35
45
  let _tiptap_pm_state = require("@tiptap/pm/state");
@@ -37,47 +47,30 @@ let _tiptap_pm_view = require("@tiptap/pm/view");
37
47
  let hast_util_from_html = require("hast-util-from-html");
38
48
  let prismjs = require("prismjs");
39
49
  prismjs = __toESM(prismjs);
50
+ let _tiptap_extension_hard_break = require("@tiptap/extension-hard-break");
51
+ _tiptap_extension_hard_break = __toESM(_tiptap_extension_hard_break);
52
+ let _tiptap_extension_italic = require("@tiptap/extension-italic");
53
+ _tiptap_extension_italic = __toESM(_tiptap_extension_italic);
54
+ let _tiptap_extension_list_item = require("@tiptap/extension-list-item");
55
+ _tiptap_extension_list_item = __toESM(_tiptap_extension_list_item);
56
+ let _tiptap_extension_ordered_list = require("@tiptap/extension-ordered-list");
57
+ _tiptap_extension_ordered_list = __toESM(_tiptap_extension_ordered_list);
58
+ let _tiptap_extension_paragraph = require("@tiptap/extension-paragraph");
59
+ _tiptap_extension_paragraph = __toESM(_tiptap_extension_paragraph);
40
60
  let _tiptap_extension_placeholder = require("@tiptap/extension-placeholder");
41
61
  _tiptap_extension_placeholder = __toESM(_tiptap_extension_placeholder);
42
- let _tiptap_react = require("@tiptap/react");
62
+ let _tiptap_extension_strike = require("@tiptap/extension-strike");
63
+ _tiptap_extension_strike = __toESM(_tiptap_extension_strike);
64
+ let _tiptap_html = require("@tiptap/html");
43
65
  let lucide_react = require("lucide-react");
44
- let react = require("react");
45
- react = __toESM(react);
46
66
  let _radix_ui_react_popover = require("@radix-ui/react-popover");
47
67
  _radix_ui_react_popover = __toESM(_radix_ui_react_popover);
48
68
  let _tiptap_react_menus = require("@tiptap/react/menus");
69
+ let _tiptap_suggestion = require("@tiptap/suggestion");
70
+ _tiptap_suggestion = __toESM(_tiptap_suggestion);
71
+ let tippy_js = require("tippy.js");
72
+ tippy_js = __toESM(tippy_js);
49
73
 
50
- //#region src/core/email-node.ts
51
- var EmailNode = class EmailNode extends _tiptap_core.Node {
52
- constructor(config) {
53
- super(config);
54
- }
55
- /**
56
- * Create a new Node instance
57
- * @param config - Node configuration object or a function that returns a configuration object
58
- */
59
- static create(config) {
60
- return new EmailNode(typeof config === "function" ? config() : config);
61
- }
62
- static from(node, renderToReactEmail) {
63
- const customNode = EmailNode.create({});
64
- Object.assign(customNode, { ...node });
65
- customNode.config = {
66
- ...node.config,
67
- renderToReactEmail
68
- };
69
- return customNode;
70
- }
71
- configure(options) {
72
- return super.configure(options);
73
- }
74
- extend(extendedConfig) {
75
- const resolvedConfig = typeof extendedConfig === "function" ? extendedConfig() : extendedConfig;
76
- return super.extend(resolvedConfig);
77
- }
78
- };
79
-
80
- //#endregion
81
74
  //#region src/core/event-bus.ts
82
75
  const EVENT_PREFIX = "@react-email/editor:";
83
76
  var EditorEventBus = class {
@@ -119,139 +112,6 @@ var EditorEventBus = class {
119
112
  };
120
113
  const editorEventBus = new EditorEventBus();
121
114
 
122
- //#endregion
123
- //#region src/extensions/alignment-attribute.tsx
124
- const AlignmentAttribute = _tiptap_core.Extension.create({
125
- name: "alignmentAttribute",
126
- addOptions() {
127
- return {
128
- types: [],
129
- alignments: [
130
- "left",
131
- "center",
132
- "right",
133
- "justify"
134
- ]
135
- };
136
- },
137
- addGlobalAttributes() {
138
- return [{
139
- types: this.options.types,
140
- attributes: { alignment: {
141
- parseHTML: (element) => {
142
- const explicitAlign = element.getAttribute("align") || element.getAttribute("alignment") || element.style.textAlign;
143
- if (explicitAlign && this.options.alignments.includes(explicitAlign)) return explicitAlign;
144
- return null;
145
- },
146
- renderHTML: (attributes) => {
147
- if (attributes.alignment === "left") return {};
148
- return { alignment: attributes.alignment };
149
- }
150
- } }
151
- }];
152
- },
153
- addCommands() {
154
- return { setAlignment: (alignment) => ({ commands }) => {
155
- if (!this.options.alignments.includes(alignment)) return false;
156
- return this.options.types.every((type) => commands.updateAttributes(type, { alignment }));
157
- } };
158
- },
159
- addKeyboardShortcuts() {
160
- return {
161
- Enter: () => {
162
- const { from } = this.editor.state.selection;
163
- const currentAlignment = this.editor.state.doc.nodeAt(from)?.attrs?.alignment;
164
- if (currentAlignment) requestAnimationFrame(() => {
165
- this.editor.commands.setAlignment(currentAlignment);
166
- });
167
- return false;
168
- },
169
- "Mod-Shift-l": () => this.editor.commands.setAlignment("left"),
170
- "Mod-Shift-e": () => this.editor.commands.setAlignment("center"),
171
- "Mod-Shift-r": () => this.editor.commands.setAlignment("right"),
172
- "Mod-Shift-j": () => this.editor.commands.setAlignment("justify")
173
- };
174
- }
175
- });
176
-
177
- //#endregion
178
- //#region src/utils/attribute-helpers.ts
179
- /**
180
- * Creates TipTap attribute definitions for a list of HTML attributes.
181
- * Each attribute will have the same pattern:
182
- * - default: null
183
- * - parseHTML: extracts the attribute from the element
184
- * - renderHTML: conditionally renders the attribute if it has a value
185
- *
186
- * @param attributeNames - Array of HTML attribute names to create definitions for
187
- * @returns Object with TipTap attribute definitions
188
- *
189
- * @example
190
- * const attrs = createStandardAttributes(['class', 'id', 'title']);
191
- * // Returns:
192
- * // {
193
- * // class: {
194
- * // default: null,
195
- * // parseHTML: (element) => element.getAttribute('class'),
196
- * // renderHTML: (attributes) => attributes.class ? { class: attributes.class } : {}
197
- * // },
198
- * // ...
199
- * // }
200
- */
201
- function createStandardAttributes(attributeNames) {
202
- return Object.fromEntries(attributeNames.map((attr) => [attr, {
203
- default: null,
204
- parseHTML: (element) => element.getAttribute(attr),
205
- renderHTML: (attributes) => {
206
- if (!attributes[attr]) return {};
207
- return { [attr]: attributes[attr] };
208
- }
209
- }]));
210
- }
211
- /**
212
- * Common HTML attributes used across multiple extensions.
213
- * These preserve attributes during HTML import and editing for better
214
- * fidelity when importing existing email templates.
215
- */
216
- const COMMON_HTML_ATTRIBUTES = [
217
- "id",
218
- "class",
219
- "title",
220
- "lang",
221
- "dir",
222
- "data-id"
223
- ];
224
- /**
225
- * Layout-specific HTML attributes used for positioning and sizing.
226
- */
227
- const LAYOUT_ATTRIBUTES = [
228
- "align",
229
- "width",
230
- "height"
231
- ];
232
- /**
233
- * Table-specific HTML attributes used for table layout and styling.
234
- */
235
- const TABLE_ATTRIBUTES = [
236
- "border",
237
- "cellpadding",
238
- "cellspacing"
239
- ];
240
- /**
241
- * Table cell-specific HTML attributes.
242
- */
243
- const TABLE_CELL_ATTRIBUTES = [
244
- "valign",
245
- "bgcolor",
246
- "colspan",
247
- "rowspan"
248
- ];
249
- /**
250
- * Table header cell-specific HTML attributes.
251
- * These are additional attributes that only apply to <th> elements.
252
- */
253
- const TABLE_HEADER_ATTRIBUTES = [...TABLE_CELL_ATTRIBUTES, "scope"];
254
-
255
115
  //#endregion
256
116
  //#region src/utils/styles.ts
257
117
  const WHITE_SPACE_REGEX = /\s+/;
@@ -404,22 +264,358 @@ function convertBorderValue(value) {
404
264
  }
405
265
  }
406
266
  /**
407
- * Resolves conflicts between reset styles and inline styles by expanding
408
- * shorthand properties (margin, padding) to longhand before merging.
409
- * This prevents shorthand properties from overriding specific longhand properties.
410
- *
411
- * @param resetStyles - Base reset styles that may contain shorthand properties
412
- * @param inlineStyles - Inline styles that should override reset styles
413
- * @returns Merged styles with inline styles taking precedence
267
+ * Resolves conflicts between reset styles and inline styles by expanding
268
+ * shorthand properties (margin, padding) to longhand before merging.
269
+ * This prevents shorthand properties from overriding specific longhand properties.
270
+ *
271
+ * @param resetStyles - Base reset styles that may contain shorthand properties
272
+ * @param inlineStyles - Inline styles that should override reset styles
273
+ * @returns Merged styles with inline styles taking precedence
274
+ */
275
+ function resolveConflictingStyles(resetStyles, inlineStyles) {
276
+ const expandedResetStyles = expandShorthandProperties(resetStyles);
277
+ const expandedInlineStyles = expandShorthandProperties(inlineStyles);
278
+ return {
279
+ ...expandedResetStyles,
280
+ ...expandedInlineStyles
281
+ };
282
+ }
283
+
284
+ //#endregion
285
+ //#region src/core/serializer/default-base-template.tsx
286
+ function DefaultBaseTemplate({ children, previewText }) {
287
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(_react_email_components.Html, { children: [
288
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(_react_email_components.Head, { children: [
289
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("meta", {
290
+ content: "width=device-width",
291
+ name: "viewport"
292
+ }),
293
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("meta", {
294
+ content: "IE=edge",
295
+ httpEquiv: "X-UA-Compatible"
296
+ }),
297
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("meta", { name: "x-apple-disable-message-reformatting" }),
298
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("meta", {
299
+ content: "telephone=no,address=no,email=no,date=no,url=no",
300
+ name: "format-detection"
301
+ })
302
+ ] }),
303
+ previewText && previewText !== "" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_react_email_components.Preview, { children: previewText }),
304
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_react_email_components.Body, { children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_react_email_components.Section, {
305
+ width: "100%",
306
+ align: "center",
307
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_react_email_components.Section, {
308
+ style: { width: "100%" },
309
+ children
310
+ })
311
+ }) })
312
+ ] });
313
+ }
314
+
315
+ //#endregion
316
+ //#region src/core/serializer/email-mark.ts
317
+ var EmailMark = class EmailMark extends _tiptap_core.Mark {
318
+ constructor(config) {
319
+ super(config);
320
+ }
321
+ /**
322
+ * Create a new Mark instance
323
+ * @param config - Mark configuration object or a function that returns a configuration object
324
+ */
325
+ static create(config) {
326
+ return new EmailMark(typeof config === "function" ? config() : config);
327
+ }
328
+ static from(mark, renderToReactEmail) {
329
+ const customMark = EmailMark.create({});
330
+ Object.assign(customMark, { ...mark });
331
+ customMark.config = {
332
+ ...mark.config,
333
+ renderToReactEmail
334
+ };
335
+ return customMark;
336
+ }
337
+ configure(options) {
338
+ return super.configure(options);
339
+ }
340
+ extend(extendedConfig) {
341
+ const resolvedConfig = typeof extendedConfig === "function" ? extendedConfig() : extendedConfig;
342
+ return super.extend(resolvedConfig);
343
+ }
344
+ };
345
+
346
+ //#endregion
347
+ //#region src/core/serializer/email-node.ts
348
+ var EmailNode = class EmailNode extends _tiptap_core.Node {
349
+ constructor(config) {
350
+ super(config);
351
+ }
352
+ /**
353
+ * Create a new Node instance
354
+ * @param config - Node configuration object or a function that returns a configuration object
355
+ */
356
+ static create(config) {
357
+ return new EmailNode(typeof config === "function" ? config() : config);
358
+ }
359
+ static from(node, renderToReactEmail) {
360
+ const customNode = EmailNode.create({});
361
+ Object.assign(customNode, { ...node });
362
+ customNode.config = {
363
+ ...node.config,
364
+ renderToReactEmail
365
+ };
366
+ return customNode;
367
+ }
368
+ configure(options) {
369
+ return super.configure(options);
370
+ }
371
+ extend(extendedConfig) {
372
+ const resolvedConfig = typeof extendedConfig === "function" ? extendedConfig() : extendedConfig;
373
+ return super.extend(resolvedConfig);
374
+ }
375
+ };
376
+
377
+ //#endregion
378
+ //#region src/core/serializer/compose-react-email.tsx
379
+ const MARK_ORDER = {
380
+ preservedStyle: 0,
381
+ italic: 1,
382
+ strike: 2,
383
+ underline: 3,
384
+ link: 4,
385
+ bold: 5,
386
+ code: 6
387
+ };
388
+ const NODES_WITH_INCREMENTED_CHILD_DEPTH = new Set(["bulletList", "orderedList"]);
389
+ function getOrderedMarks(marks) {
390
+ if (!marks) return [];
391
+ return [...marks].sort((a, b) => (MARK_ORDER[a.type] ?? Number.MAX_SAFE_INTEGER) - (MARK_ORDER[b.type] ?? Number.MAX_SAFE_INTEGER));
392
+ }
393
+ const composeReactEmail = async ({ editor, preview }) => {
394
+ const data = editor.getJSON();
395
+ const extensions = editor.extensionManager.extensions;
396
+ const serializerPlugin = extensions.map((ext) => ext.options?.serializerPlugin).filter((p) => Boolean(p)).at(-1);
397
+ const emailNodeComponentRegistry = Object.fromEntries(extensions.filter((ext) => ext instanceof EmailNode).map((extension) => [extension.name, extension.config.renderToReactEmail]));
398
+ const emailMarkComponentRegistry = Object.fromEntries(extensions.filter((ext) => ext instanceof EmailMark).map((extension) => [extension.name, extension.config.renderToReactEmail]));
399
+ function renderMark(mark, node, children, depth) {
400
+ const markStyle = serializerPlugin?.getNodeStyles({
401
+ type: mark.type,
402
+ attrs: mark.attrs ?? {}
403
+ }, depth, editor) ?? {};
404
+ const markRenderer = emailMarkComponentRegistry[mark.type];
405
+ if (markRenderer) return markRenderer({
406
+ mark,
407
+ node,
408
+ style: markStyle,
409
+ children
410
+ });
411
+ return children;
412
+ }
413
+ function parseContent(content, depth = 0) {
414
+ if (!content) return;
415
+ return content.map((node, index) => {
416
+ const style = serializerPlugin?.getNodeStyles(node, depth, editor) ?? {};
417
+ const inlineStyles = inlineCssToJs(node.attrs?.style);
418
+ if (node.type && emailNodeComponentRegistry[node.type]) {
419
+ const Component = emailNodeComponentRegistry[node.type];
420
+ const childDepth = NODES_WITH_INCREMENTED_CHILD_DEPTH.has(node.type) ? depth + 1 : depth;
421
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Component, {
422
+ node: node.type === "table" && inlineStyles.width && !node.attrs?.width ? {
423
+ ...node,
424
+ attrs: {
425
+ ...node.attrs,
426
+ width: inlineStyles.width
427
+ }
428
+ } : node,
429
+ style,
430
+ children: parseContent(node.content, childDepth)
431
+ }, index);
432
+ }
433
+ switch (node.type) {
434
+ case "text": {
435
+ let wrappedText = node.text;
436
+ getOrderedMarks(node.marks).forEach((mark) => {
437
+ wrappedText = renderMark(mark, node, wrappedText, depth);
438
+ });
439
+ const textAttributes = node.marks?.find((mark) => mark.type === "textStyle")?.attrs;
440
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
441
+ style: {
442
+ ...textAttributes,
443
+ ...style
444
+ },
445
+ children: wrappedText
446
+ }, index);
447
+ }
448
+ default: return null;
449
+ }
450
+ });
451
+ }
452
+ const unformattedHtml = await (0, _react_email_components.render)(/* @__PURE__ */ (0, react_jsx_runtime.jsx)(serializerPlugin?.BaseTemplate ?? DefaultBaseTemplate, {
453
+ previewText: preview,
454
+ editor,
455
+ children: parseContent(data.content)
456
+ }));
457
+ const [prettyHtml, text] = await Promise.all([(0, _react_email_components.pretty)(unformattedHtml), (0, _react_email_components.toPlainText)(unformattedHtml)]);
458
+ return {
459
+ html: prettyHtml,
460
+ text
461
+ };
462
+ };
463
+
464
+ //#endregion
465
+ //#region src/extensions/alignment-attribute.tsx
466
+ const AlignmentAttribute = _tiptap_core.Extension.create({
467
+ name: "alignmentAttribute",
468
+ addOptions() {
469
+ return {
470
+ types: [],
471
+ alignments: [
472
+ "left",
473
+ "center",
474
+ "right",
475
+ "justify"
476
+ ]
477
+ };
478
+ },
479
+ addGlobalAttributes() {
480
+ return [{
481
+ types: this.options.types,
482
+ attributes: { alignment: {
483
+ parseHTML: (element) => {
484
+ const explicitAlign = element.getAttribute("align") || element.getAttribute("alignment") || element.style.textAlign;
485
+ if (explicitAlign && this.options.alignments.includes(explicitAlign)) return explicitAlign;
486
+ return null;
487
+ },
488
+ renderHTML: (attributes) => {
489
+ if (attributes.alignment === "left") return {};
490
+ return { alignment: attributes.alignment };
491
+ }
492
+ } }
493
+ }];
494
+ },
495
+ addCommands() {
496
+ return { setAlignment: (alignment) => ({ commands }) => {
497
+ if (!this.options.alignments.includes(alignment)) return false;
498
+ return this.options.types.every((type) => commands.updateAttributes(type, { alignment }));
499
+ } };
500
+ },
501
+ addKeyboardShortcuts() {
502
+ return {
503
+ Enter: () => {
504
+ const { from } = this.editor.state.selection;
505
+ const currentAlignment = this.editor.state.doc.nodeAt(from)?.attrs?.alignment;
506
+ if (currentAlignment) requestAnimationFrame(() => {
507
+ this.editor.commands.setAlignment(currentAlignment);
508
+ });
509
+ return false;
510
+ },
511
+ "Mod-Shift-l": () => this.editor.commands.setAlignment("left"),
512
+ "Mod-Shift-e": () => this.editor.commands.setAlignment("center"),
513
+ "Mod-Shift-r": () => this.editor.commands.setAlignment("right"),
514
+ "Mod-Shift-j": () => this.editor.commands.setAlignment("justify")
515
+ };
516
+ }
517
+ });
518
+
519
+ //#endregion
520
+ //#region src/utils/get-text-alignment.ts
521
+ function getTextAlignment(alignment) {
522
+ switch (alignment) {
523
+ case "left": return { textAlign: "left" };
524
+ case "center": return { textAlign: "center" };
525
+ case "right": return { textAlign: "right" };
526
+ default: return {};
527
+ }
528
+ }
529
+
530
+ //#endregion
531
+ //#region src/extensions/blockquote.tsx
532
+ const Blockquote = EmailNode.from(_tiptap_extension_blockquote.default, ({ children, node, style }) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("blockquote", {
533
+ className: node.attrs?.class || void 0,
534
+ style: {
535
+ ...style,
536
+ ...inlineCssToJs(node.attrs?.style),
537
+ ...getTextAlignment(node.attrs?.align || node.attrs?.alignment)
538
+ },
539
+ children
540
+ }));
541
+
542
+ //#endregion
543
+ //#region src/utils/attribute-helpers.ts
544
+ /**
545
+ * Creates TipTap attribute definitions for a list of HTML attributes.
546
+ * Each attribute will have the same pattern:
547
+ * - default: null
548
+ * - parseHTML: extracts the attribute from the element
549
+ * - renderHTML: conditionally renders the attribute if it has a value
550
+ *
551
+ * @param attributeNames - Array of HTML attribute names to create definitions for
552
+ * @returns Object with TipTap attribute definitions
553
+ *
554
+ * @example
555
+ * const attrs = createStandardAttributes(['class', 'id', 'title']);
556
+ * // Returns:
557
+ * // {
558
+ * // class: {
559
+ * // default: null,
560
+ * // parseHTML: (element) => element.getAttribute('class'),
561
+ * // renderHTML: (attributes) => attributes.class ? { class: attributes.class } : {}
562
+ * // },
563
+ * // ...
564
+ * // }
565
+ */
566
+ function createStandardAttributes(attributeNames) {
567
+ return Object.fromEntries(attributeNames.map((attr) => [attr, {
568
+ default: null,
569
+ parseHTML: (element) => element.getAttribute(attr),
570
+ renderHTML: (attributes) => {
571
+ if (!attributes[attr]) return {};
572
+ return { [attr]: attributes[attr] };
573
+ }
574
+ }]));
575
+ }
576
+ /**
577
+ * Common HTML attributes used across multiple extensions.
578
+ * These preserve attributes during HTML import and editing for better
579
+ * fidelity when importing existing email templates.
580
+ */
581
+ const COMMON_HTML_ATTRIBUTES = [
582
+ "id",
583
+ "class",
584
+ "title",
585
+ "lang",
586
+ "dir",
587
+ "data-id"
588
+ ];
589
+ /**
590
+ * Layout-specific HTML attributes used for positioning and sizing.
414
591
  */
415
- function resolveConflictingStyles(resetStyles, inlineStyles) {
416
- const expandedResetStyles = expandShorthandProperties(resetStyles);
417
- const expandedInlineStyles = expandShorthandProperties(inlineStyles);
418
- return {
419
- ...expandedResetStyles,
420
- ...expandedInlineStyles
421
- };
422
- }
592
+ const LAYOUT_ATTRIBUTES = [
593
+ "align",
594
+ "width",
595
+ "height"
596
+ ];
597
+ /**
598
+ * Table-specific HTML attributes used for table layout and styling.
599
+ */
600
+ const TABLE_ATTRIBUTES = [
601
+ "border",
602
+ "cellpadding",
603
+ "cellspacing"
604
+ ];
605
+ /**
606
+ * Table cell-specific HTML attributes.
607
+ */
608
+ const TABLE_CELL_ATTRIBUTES = [
609
+ "valign",
610
+ "bgcolor",
611
+ "colspan",
612
+ "rowspan"
613
+ ];
614
+ /**
615
+ * Table header cell-specific HTML attributes.
616
+ * These are additional attributes that only apply to <th> elements.
617
+ */
618
+ const TABLE_HEADER_ATTRIBUTES = [...TABLE_CELL_ATTRIBUTES, "scope"];
423
619
 
424
620
  //#endregion
425
621
  //#region src/extensions/body.tsx
@@ -467,7 +663,7 @@ const Body = EmailNode.create({
467
663
  });
468
664
 
469
665
  //#endregion
470
- //#region src/extensions/bold.ts
666
+ //#region src/extensions/bold.tsx
471
667
  /**
472
668
  * Matches bold text via `**` as input.
473
669
  */
@@ -488,7 +684,7 @@ const underscorePasteRegex = /(?:^|\s)(__(?!\s+__)((?:[^_]+))__(?!\s+__))/g;
488
684
  * This extension allows you to mark text as bold.
489
685
  * @see https://tiptap.dev/api/marks/bold
490
686
  */
491
- const Bold = _tiptap_core.Mark.create({
687
+ const Bold = EmailMark.create({
492
688
  name: "bold",
493
689
  addOptions() {
494
690
  return { HTMLAttributes: {} };
@@ -513,6 +709,12 @@ const Bold = _tiptap_core.Mark.create({
513
709
  0
514
710
  ];
515
711
  },
712
+ renderToReactEmail({ children, style }) {
713
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("strong", {
714
+ style,
715
+ children
716
+ });
717
+ },
516
718
  addCommands() {
517
719
  return {
518
720
  setBold: () => ({ commands }) => {
@@ -552,6 +754,17 @@ const Bold = _tiptap_core.Mark.create({
552
754
  }
553
755
  });
554
756
 
757
+ //#endregion
758
+ //#region src/extensions/bullet-list.tsx
759
+ const BulletList = EmailNode.from(_tiptap_extension_bullet_list.default, ({ children, node, style }) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("ul", {
760
+ className: node.attrs?.class || void 0,
761
+ style: {
762
+ ...style,
763
+ ...inlineCssToJs(node.attrs?.style)
764
+ },
765
+ children
766
+ }));
767
+
555
768
  //#endregion
556
769
  //#region src/extensions/button.tsx
557
770
  const Button = EmailNode.create({
@@ -673,6 +886,16 @@ const ClassAttribute = _tiptap_core.Extension.create({
673
886
  }
674
887
  });
675
888
 
889
+ //#endregion
890
+ //#region src/extensions/code.tsx
891
+ const Code = EmailMark.from(_tiptap_extension_code.default, ({ children, node, style }) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("code", {
892
+ style: {
893
+ ...style,
894
+ ...inlineCssToJs(node.attrs?.style)
895
+ },
896
+ children
897
+ }));
898
+
676
899
  //#endregion
677
900
  //#region src/utils/prism-utils.ts
678
901
  const publicURL = "/styles/prism";
@@ -951,6 +1174,29 @@ const Div = EmailNode.create({
951
1174
  }
952
1175
  });
953
1176
 
1177
+ //#endregion
1178
+ //#region src/extensions/hard-break.tsx
1179
+ const HardBreak = EmailNode.from(_tiptap_extension_hard_break.default, () => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("br", {}));
1180
+
1181
+ //#endregion
1182
+ //#region src/extensions/italic.tsx
1183
+ const Italic = EmailMark.from(_tiptap_extension_italic.default, ({ children, style }) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("em", {
1184
+ style,
1185
+ children
1186
+ }));
1187
+
1188
+ //#endregion
1189
+ //#region src/extensions/list-item.tsx
1190
+ const ListItem = EmailNode.from(_tiptap_extension_list_item.default, ({ children, node, style }) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("li", {
1191
+ className: node.attrs?.class || void 0,
1192
+ style: {
1193
+ ...style,
1194
+ ...inlineCssToJs(node.attrs?.style),
1195
+ ...getTextAlignment(node.attrs?.align || node.attrs?.alignment)
1196
+ },
1197
+ children
1198
+ }));
1199
+
954
1200
  //#endregion
955
1201
  //#region src/extensions/max-nesting.ts
956
1202
  const MaxNesting = _tiptap_core.Extension.create({
@@ -1026,6 +1272,33 @@ const MaxNesting = _tiptap_core.Extension.create({
1026
1272
  }
1027
1273
  });
1028
1274
 
1275
+ //#endregion
1276
+ //#region src/extensions/ordered-list.tsx
1277
+ const OrderedList = EmailNode.from(_tiptap_extension_ordered_list.default, ({ children, node, style }) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("ol", {
1278
+ className: node.attrs?.class || void 0,
1279
+ start: node.attrs?.start,
1280
+ style: {
1281
+ ...style,
1282
+ ...inlineCssToJs(node.attrs?.style)
1283
+ },
1284
+ children
1285
+ }));
1286
+
1287
+ //#endregion
1288
+ //#region src/extensions/paragraph.tsx
1289
+ const Paragraph = EmailNode.from(_tiptap_extension_paragraph.default, ({ children, node, style }) => {
1290
+ const isEmpty = !node.content || node.content.length === 0;
1291
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1292
+ className: node.attrs?.class || void 0,
1293
+ style: {
1294
+ ...style,
1295
+ ...inlineCssToJs(node.attrs?.style),
1296
+ ...getTextAlignment(node.attrs?.align || node.attrs?.alignment)
1297
+ },
1298
+ children: isEmpty ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("br", {}) : children
1299
+ });
1300
+ });
1301
+
1029
1302
  //#endregion
1030
1303
  //#region src/extensions/placeholder.ts
1031
1304
  const Placeholder = _tiptap_extension_placeholder.default.configure({
@@ -1037,8 +1310,8 @@ const Placeholder = _tiptap_extension_placeholder.default.configure({
1037
1310
  });
1038
1311
 
1039
1312
  //#endregion
1040
- //#region src/extensions/preserved-style.ts
1041
- const PreservedStyle = _tiptap_core.Mark.create({
1313
+ //#region src/extensions/preserved-style.tsx
1314
+ const PreservedStyle = EmailMark.create({
1042
1315
  name: "preservedStyle",
1043
1316
  addAttributes() {
1044
1317
  return { style: {
@@ -1067,6 +1340,12 @@ const PreservedStyle = _tiptap_core.Mark.create({
1067
1340
  (0, _tiptap_core.mergeAttributes)(HTMLAttributes),
1068
1341
  0
1069
1342
  ];
1343
+ },
1344
+ renderToReactEmail({ children, mark }) {
1345
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1346
+ style: mark.attrs?.style ? inlineCssToJs(mark.attrs.style) : void 0,
1347
+ children
1348
+ });
1070
1349
  }
1071
1350
  });
1072
1351
  const LINK_INDICATOR_STYLES = [
@@ -1151,17 +1430,6 @@ const PreviewText = _tiptap_core.Node.create({
1151
1430
  }
1152
1431
  });
1153
1432
 
1154
- //#endregion
1155
- //#region src/utils/get-text-alignment.ts
1156
- function getTextAlignment(alignment) {
1157
- switch (alignment) {
1158
- case "left": return { textAlign: "left" };
1159
- case "center": return { textAlign: "center" };
1160
- case "right": return { textAlign: "right" };
1161
- default: return {};
1162
- }
1163
- }
1164
-
1165
1433
  //#endregion
1166
1434
  //#region src/extensions/section.tsx
1167
1435
  const Section = EmailNode.create({
@@ -1210,6 +1478,13 @@ const Section = EmailNode.create({
1210
1478
  }
1211
1479
  });
1212
1480
 
1481
+ //#endregion
1482
+ //#region src/extensions/strike.tsx
1483
+ const Strike = EmailMark.from(_tiptap_extension_strike.default, ({ children, style }) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("s", {
1484
+ style,
1485
+ children
1486
+ }));
1487
+
1213
1488
  //#endregion
1214
1489
  //#region src/extensions/style-attribute.tsx
1215
1490
  const StyleAttribute = _tiptap_core.Extension.create({
@@ -1259,12 +1534,12 @@ const StyleAttribute = _tiptap_core.Extension.create({
1259
1534
  });
1260
1535
 
1261
1536
  //#endregion
1262
- //#region src/extensions/sup.ts
1537
+ //#region src/extensions/sup.tsx
1263
1538
  /**
1264
1539
  * This extension allows you to mark text as superscript.
1265
1540
  * @see https://tiptap.dev/api/marks/superscript
1266
1541
  */
1267
- const Sup = _tiptap_core.Mark.create({
1542
+ const Sup = EmailMark.create({
1268
1543
  name: "sup",
1269
1544
  addOptions() {
1270
1545
  return { HTMLAttributes: {} };
@@ -1279,6 +1554,12 @@ const Sup = _tiptap_core.Mark.create({
1279
1554
  0
1280
1555
  ];
1281
1556
  },
1557
+ renderToReactEmail({ children, style }) {
1558
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("sup", {
1559
+ style,
1560
+ children
1561
+ });
1562
+ },
1282
1563
  addCommands() {
1283
1564
  return {
1284
1565
  setSup: () => ({ commands }) => {
@@ -1481,8 +1762,8 @@ const TableHeader = _tiptap_core.Node.create({
1481
1762
  });
1482
1763
 
1483
1764
  //#endregion
1484
- //#region src/extensions/uppercase.ts
1485
- const Uppercase = _tiptap_core.Mark.create({
1765
+ //#region src/extensions/uppercase.tsx
1766
+ const Uppercase = EmailMark.create({
1486
1767
  name: "uppercase",
1487
1768
  addOptions() {
1488
1769
  return { HTMLAttributes: {} };
@@ -1503,6 +1784,15 @@ const Uppercase = _tiptap_core.Mark.create({
1503
1784
  0
1504
1785
  ];
1505
1786
  },
1787
+ renderToReactEmail({ children, style }) {
1788
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1789
+ style: {
1790
+ ...style,
1791
+ textTransform: "uppercase"
1792
+ },
1793
+ children
1794
+ });
1795
+ },
1506
1796
  addCommands() {
1507
1797
  return {
1508
1798
  setUppercase: () => ({ commands }) => {
@@ -1698,17 +1988,17 @@ const coreExtensions = [
1698
1988
  underline: false,
1699
1989
  trailingNode: false,
1700
1990
  bold: false,
1991
+ italic: false,
1992
+ strike: false,
1993
+ code: false,
1994
+ paragraph: false,
1995
+ bulletList: false,
1996
+ orderedList: false,
1997
+ listItem: false,
1998
+ blockquote: false,
1999
+ hardBreak: false,
1701
2000
  gapcursor: false,
1702
- listItem: {},
1703
- bulletList: { HTMLAttributes: { class: "node-bulletList" } },
1704
- paragraph: { HTMLAttributes: { class: "node-paragraph" } },
1705
- orderedList: { HTMLAttributes: { class: "node-orderedList" } },
1706
- blockquote: { HTMLAttributes: { class: "node-blockquote" } },
1707
2001
  codeBlock: false,
1708
- code: { HTMLAttributes: {
1709
- class: "node-inlineCode",
1710
- spellcheck: "false"
1711
- } },
1712
2002
  horizontalRule: false,
1713
2003
  dropcursor: {
1714
2004
  color: "#61a8f8",
@@ -1720,9 +2010,21 @@ const coreExtensions = [
1720
2010
  defaultLanguage: "javascript",
1721
2011
  HTMLAttributes: { class: "prism node-codeBlock" }
1722
2012
  }),
2013
+ Code.configure({ HTMLAttributes: {
2014
+ class: "node-inlineCode",
2015
+ spellcheck: "false"
2016
+ } }),
2017
+ Paragraph.configure({ HTMLAttributes: { class: "node-paragraph" } }),
2018
+ BulletList.configure({ HTMLAttributes: { class: "node-bulletList" } }),
2019
+ OrderedList.configure({ HTMLAttributes: { class: "node-orderedList" } }),
2020
+ Blockquote.configure({ HTMLAttributes: { class: "node-blockquote" } }),
2021
+ ListItem,
2022
+ HardBreak,
2023
+ Italic,
1723
2024
  Placeholder,
1724
2025
  PreviewText,
1725
2026
  Bold,
2027
+ Strike,
1726
2028
  Sup,
1727
2029
  Uppercase,
1728
2030
  PreservedStyle,
@@ -1809,6 +2111,221 @@ const coreExtensions = [
1809
2111
  })
1810
2112
  ];
1811
2113
 
2114
+ //#endregion
2115
+ //#region src/core/create-drop-handler.ts
2116
+ function createDropHandler({ onPaste, onUploadImage }) {
2117
+ return (view, event, _slice, moved) => {
2118
+ if (!moved && event.dataTransfer && event.dataTransfer.files && event.dataTransfer.files[0]) {
2119
+ event.preventDefault();
2120
+ const file = event.dataTransfer.files[0];
2121
+ if (onPaste?.(file, view)) return true;
2122
+ if (file.type.includes("image/") && onUploadImage) {
2123
+ onUploadImage(file, view, (view.posAtCoords({
2124
+ left: event.clientX,
2125
+ top: event.clientY
2126
+ })?.pos || 0) - 1);
2127
+ return true;
2128
+ }
2129
+ }
2130
+ return false;
2131
+ };
2132
+ }
2133
+
2134
+ //#endregion
2135
+ //#region src/utils/paste-sanitizer.ts
2136
+ /**
2137
+ * Sanitizes pasted HTML.
2138
+ * - From editor (has node-* classes): pass through as-is
2139
+ * - From external: strip all styles/classes, keep only semantic HTML
2140
+ */
2141
+ /**
2142
+ * Detects content from the Resend editor by checking for node-* class names.
2143
+ */
2144
+ const EDITOR_CLASS_PATTERN = /class="[^"]*node-/;
2145
+ /**
2146
+ * Attributes to preserve on specific elements for EXTERNAL content.
2147
+ * Only functional attributes - NO style or class.
2148
+ */
2149
+ const PRESERVED_ATTRIBUTES = {
2150
+ a: [
2151
+ "href",
2152
+ "target",
2153
+ "rel"
2154
+ ],
2155
+ img: [
2156
+ "src",
2157
+ "alt",
2158
+ "width",
2159
+ "height"
2160
+ ],
2161
+ td: ["colspan", "rowspan"],
2162
+ th: [
2163
+ "colspan",
2164
+ "rowspan",
2165
+ "scope"
2166
+ ],
2167
+ table: [
2168
+ "border",
2169
+ "cellpadding",
2170
+ "cellspacing"
2171
+ ],
2172
+ "*": ["id"]
2173
+ };
2174
+ function isFromEditor(html) {
2175
+ return EDITOR_CLASS_PATTERN.test(html);
2176
+ }
2177
+ function sanitizePastedHtml(html) {
2178
+ if (isFromEditor(html)) return html;
2179
+ const doc = new DOMParser().parseFromString(html, "text/html");
2180
+ sanitizeNode(doc.body);
2181
+ return doc.body.innerHTML;
2182
+ }
2183
+ function sanitizeNode(node) {
2184
+ if (node.nodeType === Node.ELEMENT_NODE) sanitizeElement(node);
2185
+ for (const child of Array.from(node.childNodes)) sanitizeNode(child);
2186
+ }
2187
+ function sanitizeElement(el) {
2188
+ const allowedForTag = PRESERVED_ATTRIBUTES[el.tagName.toLowerCase()] || [];
2189
+ const allowedGlobal = PRESERVED_ATTRIBUTES["*"] || [];
2190
+ const allowed = new Set([...allowedForTag, ...allowedGlobal]);
2191
+ const attributesToRemove = [];
2192
+ for (const attr of Array.from(el.attributes)) {
2193
+ if (attr.name.startsWith("data-")) {
2194
+ attributesToRemove.push(attr.name);
2195
+ continue;
2196
+ }
2197
+ if (!allowed.has(attr.name)) attributesToRemove.push(attr.name);
2198
+ }
2199
+ for (const attr of attributesToRemove) el.removeAttribute(attr);
2200
+ }
2201
+
2202
+ //#endregion
2203
+ //#region src/core/create-paste-handler.ts
2204
+ function createPasteHandler({ onPaste, onUploadImage, extensions }) {
2205
+ return (view, event, slice) => {
2206
+ const text = event.clipboardData?.getData("text/plain");
2207
+ if (text && onPaste?.(text, view)) {
2208
+ event.preventDefault();
2209
+ return true;
2210
+ }
2211
+ if (event.clipboardData?.files?.[0]) {
2212
+ const file = event.clipboardData.files[0];
2213
+ if (onPaste?.(file, view)) {
2214
+ event.preventDefault();
2215
+ return true;
2216
+ }
2217
+ if (file.type.includes("image/") && onUploadImage) {
2218
+ const pos = view.state.selection.from;
2219
+ onUploadImage(file, view, pos);
2220
+ return true;
2221
+ }
2222
+ }
2223
+ /**
2224
+ * If the coming content has a single child, we can assume
2225
+ * it's a plain text and doesn't need to be parsed and
2226
+ * be introduced in a new line
2227
+ */
2228
+ if (slice.content.childCount === 1) return false;
2229
+ if (event.clipboardData?.getData?.("text/html")) {
2230
+ event.preventDefault();
2231
+ const jsonContent = (0, _tiptap_html.generateJSON)(sanitizePastedHtml(event.clipboardData.getData("text/html")), extensions);
2232
+ const node = view.state.schema.nodeFromJSON(jsonContent);
2233
+ const transaction = view.state.tr.replaceSelectionWith(node, false);
2234
+ view.dispatch(transaction);
2235
+ return true;
2236
+ }
2237
+ return false;
2238
+ };
2239
+ }
2240
+
2241
+ //#endregion
2242
+ //#region src/core/is-document-visually-empty.ts
2243
+ function isDocumentVisuallyEmpty(doc) {
2244
+ let nonGlobalNodeCount = 0;
2245
+ let firstNonGlobalNode = null;
2246
+ for (let index = 0; index < doc.childCount; index += 1) {
2247
+ const node = doc.child(index);
2248
+ if (node.type.name === "globalContent") continue;
2249
+ nonGlobalNodeCount += 1;
2250
+ if (firstNonGlobalNode === null) firstNonGlobalNode = {
2251
+ type: node.type,
2252
+ textContent: node.textContent,
2253
+ childCount: node.content.childCount
2254
+ };
2255
+ }
2256
+ if (nonGlobalNodeCount === 0) return true;
2257
+ if (nonGlobalNodeCount !== 1) return false;
2258
+ return firstNonGlobalNode?.type.name === "paragraph" && firstNonGlobalNode.textContent.trim().length === 0 && firstNonGlobalNode.childCount === 0;
2259
+ }
2260
+
2261
+ //#endregion
2262
+ //#region src/core/use-editor.ts
2263
+ const COLLABORATION_EXTENSION_NAMES = new Set(["liveblocksExtension", "collaboration"]);
2264
+ function hasCollaborationExtension(exts) {
2265
+ return exts.some((ext) => COLLABORATION_EXTENSION_NAMES.has(ext.name));
2266
+ }
2267
+ function useEditor({ content, extensions = [], onUpdate, onPaste, onUploadImage, onReady, editable = true, ...rest }) {
2268
+ const [contentError, setContentError] = react.useState(null);
2269
+ const isCollaborative = hasCollaborationExtension(extensions);
2270
+ const effectiveExtensions = react.useMemo(() => [
2271
+ ...coreExtensions,
2272
+ ...isCollaborative ? [] : [_tiptap_extensions.UndoRedo],
2273
+ ...extensions
2274
+ ], [extensions, isCollaborative]);
2275
+ const editor = (0, _tiptap_react.useEditor)({
2276
+ content: isCollaborative ? void 0 : content,
2277
+ extensions: effectiveExtensions,
2278
+ immediatelyRender: false,
2279
+ enableContentCheck: true,
2280
+ onContentError({ editor: editor$1, error, disableCollaboration }) {
2281
+ disableCollaboration();
2282
+ setContentError(error);
2283
+ console.error(error);
2284
+ editor$1.setEditable(false);
2285
+ },
2286
+ onCreate({ editor: editor$1 }) {
2287
+ onReady?.(editor$1);
2288
+ },
2289
+ onUpdate({ editor: editor$1, transaction }) {
2290
+ onUpdate?.(editor$1, transaction);
2291
+ },
2292
+ editorProps: {
2293
+ handleDOMEvents: { click: (view, event) => {
2294
+ if (!view.editable) {
2295
+ if (event.target.closest("a")) {
2296
+ event.preventDefault();
2297
+ return true;
2298
+ }
2299
+ }
2300
+ return false;
2301
+ } },
2302
+ handlePaste: createPasteHandler({
2303
+ onPaste,
2304
+ onUploadImage,
2305
+ extensions: effectiveExtensions
2306
+ }),
2307
+ handleDrop: createDropHandler({
2308
+ onPaste,
2309
+ onUploadImage
2310
+ })
2311
+ },
2312
+ ...rest
2313
+ });
2314
+ return {
2315
+ editor,
2316
+ isEditorEmpty: (0, _tiptap_react.useEditorState)({
2317
+ editor,
2318
+ selector: (context) => {
2319
+ if (!context.editor) return true;
2320
+ return isDocumentVisuallyEmpty(context.editor.state.doc);
2321
+ }
2322
+ }) ?? true,
2323
+ extensions: effectiveExtensions,
2324
+ contentError,
2325
+ isCollaborative
2326
+ };
2327
+ }
2328
+
1812
2329
  //#endregion
1813
2330
  //#region src/utils/set-text-alignment.ts
1814
2331
  function setTextAlignment(editor, alignment) {
@@ -2912,8 +3429,469 @@ const LinkBubbleMenu = {
2912
3429
  Default: LinkBubbleMenuDefault
2913
3430
  };
2914
3431
 
3432
+ //#endregion
3433
+ //#region src/ui/slash-command/utils.ts
3434
+ function isInsideNode(editor, type) {
3435
+ const { $from } = editor.state.selection;
3436
+ for (let d = $from.depth; d > 0; d--) if ($from.node(d).type.name === type) return true;
3437
+ return false;
3438
+ }
3439
+ function isAtMaxColumnsDepth(editor) {
3440
+ const { from } = editor.state.selection;
3441
+ return getColumnsDepth(editor.state.doc, from) >= MAX_COLUMNS_DEPTH;
3442
+ }
3443
+ function updateScrollView(container, item) {
3444
+ const containerRect = container.getBoundingClientRect();
3445
+ const itemRect = item.getBoundingClientRect();
3446
+ if (itemRect.top < containerRect.top) container.scrollTop -= containerRect.top - itemRect.top;
3447
+ else if (itemRect.bottom > containerRect.bottom) container.scrollTop += itemRect.bottom - containerRect.bottom;
3448
+ }
3449
+
3450
+ //#endregion
3451
+ //#region src/ui/slash-command/command-list.tsx
3452
+ const CATEGORY_ORDER = [
3453
+ "Text",
3454
+ "Media",
3455
+ "Layout",
3456
+ "Utility"
3457
+ ];
3458
+ function groupByCategory(items) {
3459
+ const seen = /* @__PURE__ */ new Map();
3460
+ for (const item of items) {
3461
+ const existing = seen.get(item.category);
3462
+ if (existing) existing.push(item);
3463
+ else seen.set(item.category, [item]);
3464
+ }
3465
+ const ordered = [];
3466
+ for (const cat of CATEGORY_ORDER) {
3467
+ const group = seen.get(cat);
3468
+ if (group) {
3469
+ ordered.push({
3470
+ category: cat,
3471
+ items: group
3472
+ });
3473
+ seen.delete(cat);
3474
+ }
3475
+ }
3476
+ for (const [category, group] of seen) ordered.push({
3477
+ category,
3478
+ items: group
3479
+ });
3480
+ return ordered;
3481
+ }
3482
+ function CommandItem({ item, selected, onSelect }) {
3483
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
3484
+ "data-re-slash-command-item": "",
3485
+ "data-selected": selected || void 0,
3486
+ onClick: onSelect,
3487
+ type: "button",
3488
+ children: [item.icon, /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: item.title })]
3489
+ });
3490
+ }
3491
+ function CommandList({ items, command, query, ref }) {
3492
+ const [selectedIndex, setSelectedIndex] = (0, react.useState)(0);
3493
+ const containerRef = (0, react.useRef)(null);
3494
+ (0, react.useEffect)(() => {
3495
+ setSelectedIndex(0);
3496
+ }, [items]);
3497
+ (0, react.useLayoutEffect)(() => {
3498
+ const container = containerRef.current;
3499
+ if (!container) return;
3500
+ const selected = container.querySelector("[data-selected]");
3501
+ if (selected) updateScrollView(container, selected);
3502
+ }, [selectedIndex]);
3503
+ const selectItem = (0, react.useCallback)((index) => {
3504
+ const item = items[index];
3505
+ if (item) command(item);
3506
+ }, [items, command]);
3507
+ (0, react.useImperativeHandle)(ref, () => ({ onKeyDown: ({ event }) => {
3508
+ if (items.length === 0) return false;
3509
+ if (event.key === "ArrowUp") {
3510
+ setSelectedIndex((i) => (i + items.length - 1) % items.length);
3511
+ return true;
3512
+ }
3513
+ if (event.key === "ArrowDown") {
3514
+ setSelectedIndex((i) => (i + 1) % items.length);
3515
+ return true;
3516
+ }
3517
+ if (event.key === "Enter") {
3518
+ selectItem(selectedIndex);
3519
+ return true;
3520
+ }
3521
+ return false;
3522
+ } }), [
3523
+ items.length,
3524
+ selectItem,
3525
+ selectedIndex
3526
+ ]);
3527
+ if (items.length === 0) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
3528
+ "data-re-slash-command": "",
3529
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
3530
+ "data-re-slash-command-empty": "",
3531
+ children: "No results"
3532
+ })
3533
+ });
3534
+ if (query.trim().length > 0) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
3535
+ "data-re-slash-command": "",
3536
+ ref: containerRef,
3537
+ children: items.map((item, index) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CommandItem, {
3538
+ item,
3539
+ onSelect: () => selectItem(index),
3540
+ selected: index === selectedIndex
3541
+ }, item.title))
3542
+ });
3543
+ const groups = groupByCategory(items);
3544
+ let flatIndex = 0;
3545
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
3546
+ "data-re-slash-command": "",
3547
+ ref: containerRef,
3548
+ children: groups.map((group) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
3549
+ "data-re-slash-command-category": "",
3550
+ children: group.category
3551
+ }), group.items.map((item) => {
3552
+ const currentIndex = flatIndex++;
3553
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CommandItem, {
3554
+ item,
3555
+ onSelect: () => selectItem(currentIndex),
3556
+ selected: currentIndex === selectedIndex
3557
+ }, item.title);
3558
+ })] }, group.category))
3559
+ });
3560
+ }
3561
+
3562
+ //#endregion
3563
+ //#region src/ui/slash-command/commands.tsx
3564
+ const TEXT = {
3565
+ title: "Text",
3566
+ description: "Plain text block",
3567
+ icon: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.Text, { size: 20 }),
3568
+ category: "Text",
3569
+ searchTerms: ["p", "paragraph"],
3570
+ command: ({ editor, range }) => {
3571
+ editor.chain().focus().deleteRange(range).toggleNode("paragraph", "paragraph").run();
3572
+ }
3573
+ };
3574
+ const H1 = {
3575
+ title: "Title",
3576
+ description: "Large heading",
3577
+ icon: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.Heading1, { size: 20 }),
3578
+ category: "Text",
3579
+ searchTerms: [
3580
+ "title",
3581
+ "big",
3582
+ "large",
3583
+ "h1"
3584
+ ],
3585
+ command: ({ editor, range }) => {
3586
+ editor.chain().focus().deleteRange(range).setNode("heading", { level: 1 }).run();
3587
+ }
3588
+ };
3589
+ const H2 = {
3590
+ title: "Subtitle",
3591
+ description: "Medium heading",
3592
+ icon: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.Heading2, { size: 20 }),
3593
+ category: "Text",
3594
+ searchTerms: [
3595
+ "subtitle",
3596
+ "medium",
3597
+ "h2"
3598
+ ],
3599
+ command: ({ editor, range }) => {
3600
+ editor.chain().focus().deleteRange(range).setNode("heading", { level: 2 }).run();
3601
+ }
3602
+ };
3603
+ const H3 = {
3604
+ title: "Heading",
3605
+ description: "Small heading",
3606
+ icon: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.Heading3, { size: 20 }),
3607
+ category: "Text",
3608
+ searchTerms: [
3609
+ "subtitle",
3610
+ "small",
3611
+ "h3"
3612
+ ],
3613
+ command: ({ editor, range }) => {
3614
+ editor.chain().focus().deleteRange(range).setNode("heading", { level: 3 }).run();
3615
+ }
3616
+ };
3617
+ const BULLET_LIST = {
3618
+ title: "Bullet list",
3619
+ description: "Unordered list",
3620
+ icon: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.List, { size: 20 }),
3621
+ category: "Text",
3622
+ searchTerms: ["unordered", "point"],
3623
+ command: ({ editor, range }) => {
3624
+ editor.chain().focus().deleteRange(range).toggleBulletList().run();
3625
+ }
3626
+ };
3627
+ const NUMBERED_LIST = {
3628
+ title: "Numbered list",
3629
+ description: "Ordered list",
3630
+ icon: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.ListOrdered, { size: 20 }),
3631
+ category: "Text",
3632
+ searchTerms: ["ordered"],
3633
+ command: ({ editor, range }) => {
3634
+ editor.chain().focus().deleteRange(range).toggleOrderedList().run();
3635
+ }
3636
+ };
3637
+ const QUOTE = {
3638
+ title: "Quote",
3639
+ description: "Block quote",
3640
+ icon: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.TextQuote, { size: 20 }),
3641
+ category: "Text",
3642
+ searchTerms: ["blockquote"],
3643
+ command: ({ editor, range }) => {
3644
+ editor.chain().focus().deleteRange(range).toggleNode("paragraph", "paragraph").toggleBlockquote().run();
3645
+ }
3646
+ };
3647
+ const CODE = {
3648
+ title: "Code block",
3649
+ description: "Code snippet",
3650
+ icon: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.SquareCode, { size: 20 }),
3651
+ category: "Text",
3652
+ searchTerms: ["codeblock"],
3653
+ command: ({ editor, range }) => {
3654
+ editor.chain().focus().deleteRange(range).toggleCodeBlock().run();
3655
+ }
3656
+ };
3657
+ const BUTTON = {
3658
+ title: "Button",
3659
+ description: "Clickable button",
3660
+ icon: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.MousePointer, { size: 20 }),
3661
+ category: "Layout",
3662
+ searchTerms: ["button"],
3663
+ command: ({ editor, range }) => {
3664
+ editor.chain().focus().deleteRange(range).setButton().run();
3665
+ }
3666
+ };
3667
+ const DIVIDER = {
3668
+ title: "Divider",
3669
+ description: "Horizontal separator",
3670
+ icon: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.SplitSquareVertical, { size: 20 }),
3671
+ category: "Layout",
3672
+ searchTerms: [
3673
+ "hr",
3674
+ "divider",
3675
+ "separator"
3676
+ ],
3677
+ command: ({ editor, range }) => {
3678
+ editor.chain().focus().deleteRange(range).setHorizontalRule().run();
3679
+ }
3680
+ };
3681
+ const SECTION = {
3682
+ title: "Section",
3683
+ description: "Content section",
3684
+ icon: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.Rows2, { size: 20 }),
3685
+ category: "Layout",
3686
+ searchTerms: [
3687
+ "section",
3688
+ "row",
3689
+ "container"
3690
+ ],
3691
+ command: ({ editor, range }) => {
3692
+ editor.chain().focus().deleteRange(range).insertSection().run();
3693
+ }
3694
+ };
3695
+ const TWO_COLUMNS = {
3696
+ title: "2 columns",
3697
+ description: "Two column layout",
3698
+ icon: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.Columns2, { size: 20 }),
3699
+ category: "Layout",
3700
+ searchTerms: [
3701
+ "columns",
3702
+ "column",
3703
+ "layout",
3704
+ "grid",
3705
+ "split",
3706
+ "side-by-side",
3707
+ "multi-column",
3708
+ "row",
3709
+ "two",
3710
+ "2"
3711
+ ],
3712
+ command: ({ editor, range }) => {
3713
+ editor.chain().focus().deleteRange(range).insertColumns(2).run();
3714
+ }
3715
+ };
3716
+ const THREE_COLUMNS = {
3717
+ title: "3 columns",
3718
+ description: "Three column layout",
3719
+ icon: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.Columns3, { size: 20 }),
3720
+ category: "Layout",
3721
+ searchTerms: [
3722
+ "columns",
3723
+ "column",
3724
+ "layout",
3725
+ "grid",
3726
+ "split",
3727
+ "multi-column",
3728
+ "row",
3729
+ "three",
3730
+ "3"
3731
+ ],
3732
+ command: ({ editor, range }) => {
3733
+ editor.chain().focus().deleteRange(range).insertColumns(3).run();
3734
+ }
3735
+ };
3736
+ const FOUR_COLUMNS = {
3737
+ title: "4 columns",
3738
+ description: "Four column layout",
3739
+ icon: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.Columns4, { size: 20 }),
3740
+ category: "Layout",
3741
+ searchTerms: [
3742
+ "columns",
3743
+ "column",
3744
+ "layout",
3745
+ "grid",
3746
+ "split",
3747
+ "multi-column",
3748
+ "row",
3749
+ "four",
3750
+ "4"
3751
+ ],
3752
+ command: ({ editor, range }) => {
3753
+ editor.chain().focus().deleteRange(range).insertColumns(4).run();
3754
+ }
3755
+ };
3756
+ const defaultSlashCommands = [
3757
+ TEXT,
3758
+ H1,
3759
+ H2,
3760
+ H3,
3761
+ BULLET_LIST,
3762
+ NUMBERED_LIST,
3763
+ QUOTE,
3764
+ CODE,
3765
+ BUTTON,
3766
+ DIVIDER,
3767
+ SECTION,
3768
+ TWO_COLUMNS,
3769
+ THREE_COLUMNS,
3770
+ FOUR_COLUMNS
3771
+ ];
3772
+
3773
+ //#endregion
3774
+ //#region src/ui/slash-command/extension.ts
3775
+ const SlashCommandExtension = _tiptap_core.Extension.create({
3776
+ name: "slash-command",
3777
+ addOptions() {
3778
+ return { suggestion: {
3779
+ char: "/",
3780
+ allow: ({ editor }) => !editor.isActive("codeBlock"),
3781
+ command: ({ editor, range, props }) => {
3782
+ props.command({
3783
+ editor,
3784
+ range
3785
+ });
3786
+ }
3787
+ } };
3788
+ },
3789
+ addProseMirrorPlugins() {
3790
+ return [(0, _tiptap_suggestion.default)({
3791
+ pluginKey: new _tiptap_pm_state.PluginKey("slash-command"),
3792
+ editor: this.editor,
3793
+ ...this.options.suggestion
3794
+ })];
3795
+ }
3796
+ });
3797
+
3798
+ //#endregion
3799
+ //#region src/ui/slash-command/render.tsx
3800
+ function createRenderItems(component = CommandList) {
3801
+ return () => {
3802
+ let renderer = null;
3803
+ let popup = null;
3804
+ return {
3805
+ onStart: (props) => {
3806
+ renderer = new _tiptap_react.ReactRenderer(component, {
3807
+ props,
3808
+ editor: props.editor
3809
+ });
3810
+ if (!props.clientRect) return;
3811
+ popup = (0, tippy_js.default)("body", {
3812
+ getReferenceClientRect: props.clientRect,
3813
+ appendTo: () => document.body,
3814
+ content: renderer.element,
3815
+ showOnCreate: true,
3816
+ interactive: true,
3817
+ trigger: "manual",
3818
+ placement: "bottom-start"
3819
+ });
3820
+ },
3821
+ onUpdate: (props) => {
3822
+ if (!renderer) return;
3823
+ renderer.updateProps(props);
3824
+ if (popup?.[0] && props.clientRect) popup[0].setProps({ getReferenceClientRect: props.clientRect });
3825
+ },
3826
+ onKeyDown: (props) => {
3827
+ if (props.event.key === "Escape") {
3828
+ popup?.[0]?.hide();
3829
+ return true;
3830
+ }
3831
+ return renderer?.ref?.onKeyDown(props) ?? false;
3832
+ },
3833
+ onExit: () => {
3834
+ popup?.[0]?.destroy();
3835
+ renderer?.destroy();
3836
+ popup = null;
3837
+ renderer = null;
3838
+ }
3839
+ };
3840
+ };
3841
+ }
3842
+
3843
+ //#endregion
3844
+ //#region src/ui/slash-command/search.ts
3845
+ function scoreItem(item, query) {
3846
+ if (!query) return 100;
3847
+ const q = query.toLowerCase();
3848
+ const title = item.title.toLowerCase();
3849
+ const description = item.description.toLowerCase();
3850
+ const terms = item.searchTerms?.map((t) => t.toLowerCase()) ?? [];
3851
+ if (title === q) return 100;
3852
+ if (title.startsWith(q)) return 90;
3853
+ if (title.split(/\s+/).some((w) => w.startsWith(q))) return 80;
3854
+ if (terms.some((t) => t === q)) return 70;
3855
+ if (terms.some((t) => t.startsWith(q))) return 60;
3856
+ if (title.includes(q)) return 40;
3857
+ if (terms.some((t) => t.includes(q))) return 30;
3858
+ if (description.includes(q)) return 20;
3859
+ return 0;
3860
+ }
3861
+ function filterAndRankItems(items, query) {
3862
+ const trimmed = query.trim();
3863
+ if (!trimmed) return items;
3864
+ const scored = items.map((item) => ({
3865
+ item,
3866
+ score: scoreItem(item, trimmed)
3867
+ })).filter(({ score }) => score > 0);
3868
+ scored.sort((a, b) => b.score - a.score);
3869
+ return scored.map(({ item }) => item);
3870
+ }
3871
+
3872
+ //#endregion
3873
+ //#region src/ui/slash-command/create-slash-command.ts
3874
+ function defaultFilterItems(items, query, editor) {
3875
+ return filterAndRankItems(isAtMaxColumnsDepth(editor) ? items.filter((item) => item.category !== "Layout" || !item.title.includes("column")) : items, query);
3876
+ }
3877
+ function createSlashCommand(options) {
3878
+ const items = options?.items ?? defaultSlashCommands;
3879
+ const filterFn = options?.filterItems ?? defaultFilterItems;
3880
+ return SlashCommandExtension.configure({ suggestion: {
3881
+ items: ({ query, editor }) => filterFn(items, query, editor),
3882
+ render: createRenderItems(options?.component)
3883
+ } });
3884
+ }
3885
+
3886
+ //#endregion
3887
+ //#region src/ui/slash-command/index.ts
3888
+ const SlashCommand = createSlashCommand();
3889
+
2915
3890
  //#endregion
2916
3891
  exports.AlignmentAttribute = AlignmentAttribute;
3892
+ exports.BULLET_LIST = BULLET_LIST;
3893
+ exports.BUTTON = BUTTON;
3894
+ exports.Blockquote = Blockquote;
2917
3895
  exports.Body = Body;
2918
3896
  exports.Bold = Bold;
2919
3897
  exports.BubbleMenu = BubbleMenu;
@@ -2933,24 +3911,35 @@ exports.BubbleMenuSeparator = BubbleMenuSeparator;
2933
3911
  exports.BubbleMenuStrike = BubbleMenuStrike;
2934
3912
  exports.BubbleMenuUnderline = BubbleMenuUnderline;
2935
3913
  exports.BubbleMenuUppercase = BubbleMenuUppercase;
3914
+ exports.BulletList = BulletList;
2936
3915
  exports.Button = Button;
2937
3916
  exports.ButtonBubbleMenu = ButtonBubbleMenu;
2938
3917
  exports.ButtonBubbleMenuDefault = ButtonBubbleMenuDefault;
2939
3918
  exports.ButtonBubbleMenuEditLink = ButtonBubbleMenuEditLink;
2940
3919
  exports.ButtonBubbleMenuRoot = ButtonBubbleMenuRoot;
2941
3920
  exports.ButtonBubbleMenuToolbar = ButtonBubbleMenuToolbar;
3921
+ exports.CODE = CODE;
2942
3922
  exports.COLUMN_PARENT_TYPES = COLUMN_PARENT_TYPES;
2943
3923
  exports.ClassAttribute = ClassAttribute;
3924
+ exports.Code = Code;
2944
3925
  exports.CodeBlockPrism = CodeBlockPrism;
2945
3926
  exports.ColumnsColumn = ColumnsColumn;
3927
+ exports.CommandList = CommandList;
3928
+ exports.DIVIDER = DIVIDER;
2946
3929
  exports.Div = Div;
2947
3930
  exports.EmailNode = EmailNode;
3931
+ exports.FOUR_COLUMNS = FOUR_COLUMNS;
2948
3932
  exports.FourColumns = FourColumns;
3933
+ exports.H1 = H1;
3934
+ exports.H2 = H2;
3935
+ exports.H3 = H3;
3936
+ exports.HardBreak = HardBreak;
2949
3937
  exports.ImageBubbleMenu = ImageBubbleMenu;
2950
3938
  exports.ImageBubbleMenuDefault = ImageBubbleMenuDefault;
2951
3939
  exports.ImageBubbleMenuEditLink = ImageBubbleMenuEditLink;
2952
3940
  exports.ImageBubbleMenuRoot = ImageBubbleMenuRoot;
2953
3941
  exports.ImageBubbleMenuToolbar = ImageBubbleMenuToolbar;
3942
+ exports.Italic = Italic;
2954
3943
  exports.LinkBubbleMenu = LinkBubbleMenu;
2955
3944
  exports.LinkBubbleMenuDefault = LinkBubbleMenuDefault;
2956
3945
  exports.LinkBubbleMenuEditLink = LinkBubbleMenuEditLink;
@@ -2959,17 +3948,28 @@ exports.LinkBubbleMenuOpenLink = LinkBubbleMenuOpenLink;
2959
3948
  exports.LinkBubbleMenuRoot = LinkBubbleMenuRoot;
2960
3949
  exports.LinkBubbleMenuToolbar = LinkBubbleMenuToolbar;
2961
3950
  exports.LinkBubbleMenuUnlink = LinkBubbleMenuUnlink;
3951
+ exports.ListItem = ListItem;
2962
3952
  exports.MAX_COLUMNS_DEPTH = MAX_COLUMNS_DEPTH;
2963
3953
  exports.MaxNesting = MaxNesting;
3954
+ exports.NUMBERED_LIST = NUMBERED_LIST;
2964
3955
  exports.NodeSelectorContent = NodeSelectorContent;
2965
3956
  exports.NodeSelectorRoot = NodeSelectorRoot;
2966
3957
  exports.NodeSelectorTrigger = NodeSelectorTrigger;
3958
+ exports.OrderedList = OrderedList;
3959
+ exports.Paragraph = Paragraph;
2967
3960
  exports.Placeholder = Placeholder;
2968
3961
  exports.PreservedStyle = PreservedStyle;
2969
3962
  exports.PreviewText = PreviewText;
3963
+ exports.QUOTE = QUOTE;
3964
+ exports.SECTION = SECTION;
2970
3965
  exports.Section = Section;
3966
+ exports.SlashCommand = SlashCommand;
3967
+ exports.Strike = Strike;
2971
3968
  exports.StyleAttribute = StyleAttribute;
2972
3969
  exports.Sup = Sup;
3970
+ exports.TEXT = TEXT;
3971
+ exports.THREE_COLUMNS = THREE_COLUMNS;
3972
+ exports.TWO_COLUMNS = TWO_COLUMNS;
2973
3973
  exports.Table = Table;
2974
3974
  exports.TableCell = TableCell;
2975
3975
  exports.TableHeader = TableHeader;
@@ -2977,11 +3977,19 @@ exports.TableRow = TableRow;
2977
3977
  exports.ThreeColumns = ThreeColumns;
2978
3978
  exports.TwoColumns = TwoColumns;
2979
3979
  exports.Uppercase = Uppercase;
3980
+ exports.composeReactEmail = composeReactEmail;
2980
3981
  exports.coreExtensions = coreExtensions;
3982
+ exports.createSlashCommand = createSlashCommand;
3983
+ exports.defaultSlashCommands = defaultSlashCommands;
2981
3984
  exports.editorEventBus = editorEventBus;
3985
+ exports.filterAndRankItems = filterAndRankItems;
2982
3986
  exports.getColumnsDepth = getColumnsDepth;
3987
+ exports.isAtMaxColumnsDepth = isAtMaxColumnsDepth;
3988
+ exports.isInsideNode = isInsideNode;
2983
3989
  exports.processStylesForUnlink = processStylesForUnlink;
3990
+ exports.scoreItem = scoreItem;
2984
3991
  exports.setTextAlignment = setTextAlignment;
2985
3992
  exports.useButtonBubbleMenuContext = useButtonBubbleMenuContext;
3993
+ exports.useEditor = useEditor;
2986
3994
  exports.useImageBubbleMenuContext = useImageBubbleMenuContext;
2987
3995
  exports.useLinkBubbleMenuContext = useLinkBubbleMenuContext;