@react-email/editor 0.0.0-experimental.2 → 0.0.0-experimental.21

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,176 +1,100 @@
1
- import { Extension, Mark, Node, findChildren, markInputRule, markPasteRule, mergeAttributes } from "@tiptap/core";
2
- import { jsx } from "react/jsx-runtime";
3
1
  import * as ReactEmailComponents from "@react-email/components";
4
- 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, Heading as Heading$1, Hr, Html, Link as Link$1, 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, InputRule, Mark, Node as Node$1, findChildren, mergeAttributes } from "@tiptap/core";
5
+ import { UndoRedo } from "@tiptap/extensions";
6
+ import { NodeViewContent, NodeViewWrapper, ReactNodeViewRenderer, 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 TipTapStarterKit from "@tiptap/starter-kit";
10
+ import BlockquoteBase from "@tiptap/extension-blockquote";
11
+ import BoldBase from "@tiptap/extension-bold";
12
+ import BulletListBase from "@tiptap/extension-bullet-list";
13
+ import CodeBase from "@tiptap/extension-code";
5
14
  import CodeBlock$1 from "@tiptap/extension-code-block";
6
- import { Plugin, PluginKey } from "@tiptap/pm/state";
15
+ import { Plugin, PluginKey, TextSelection } from "@tiptap/pm/state";
7
16
  import { Decoration, DecorationSet } from "@tiptap/pm/view";
8
17
  import { fromHtml } from "hast-util-from-html";
9
18
  import Prism from "prismjs";
10
- import Placeholder from "@tiptap/extension-placeholder";
19
+ import HorizontalRule from "@tiptap/extension-horizontal-rule";
20
+ import HardBreakBase from "@tiptap/extension-hard-break";
21
+ import { Heading as Heading$2 } from "@tiptap/extension-heading";
22
+ import ItalicBase from "@tiptap/extension-italic";
23
+ import TiptapLink from "@tiptap/extension-link";
24
+ import ListItemBase from "@tiptap/extension-list-item";
25
+ import OrderedListBase from "@tiptap/extension-ordered-list";
26
+ import ParagraphBase from "@tiptap/extension-paragraph";
27
+ import TipTapPlaceholder from "@tiptap/extension-placeholder";
28
+ import StrikeBase from "@tiptap/extension-strike";
29
+ import SuperscriptBase from "@tiptap/extension-superscript";
30
+ import UnderlineBase from "@tiptap/extension-underline";
31
+ import { generateJSON } from "@tiptap/html";
32
+ 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";
33
+ import * as Popover from "@radix-ui/react-popover";
34
+ import { BubbleMenu as BubbleMenu$1 } from "@tiptap/react/menus";
35
+ import Suggestion from "@tiptap/suggestion";
36
+ import tippy from "tippy.js";
11
37
 
12
- //#region src/core/email-node.ts
13
- var EmailNode = class EmailNode extends Node {
14
- constructor(config) {
15
- super(config);
38
+ //#region src/core/event-bus.ts
39
+ const EVENT_PREFIX = "@react-email/editor:";
40
+ var EditorEventBus = class {
41
+ prefixEventName(eventName) {
42
+ return `${EVENT_PREFIX}${String(eventName)}`;
16
43
  }
17
- /**
18
- * Create a new Node instance
19
- * @param config - Node configuration object or a function that returns a configuration object
20
- */
21
- static create(config) {
22
- return new EmailNode(typeof config === "function" ? config() : config);
44
+ dispatch(eventName, payload, options) {
45
+ const target = options?.target ?? window;
46
+ const prefixedEventName = this.prefixEventName(eventName);
47
+ const event = new CustomEvent(prefixedEventName, {
48
+ detail: payload,
49
+ bubbles: false,
50
+ cancelable: false
51
+ });
52
+ target.dispatchEvent(event);
23
53
  }
24
- static from(node, renderToReactEmail) {
25
- const customNode = EmailNode.create({});
26
- Object.assign(customNode, { ...node });
27
- customNode.config = {
28
- ...node.config,
29
- renderToReactEmail
54
+ on(eventName, handler, options) {
55
+ const target = options?.target ?? window;
56
+ const prefixedEventName = this.prefixEventName(eventName);
57
+ const abortController = new AbortController();
58
+ const wrappedHandler = (event) => {
59
+ const customEvent = event;
60
+ const result = handler(customEvent.detail);
61
+ if (result instanceof Promise) result.catch((error) => {
62
+ console.error(`Error in async event handler for ${prefixedEventName}:`, {
63
+ event: customEvent.detail,
64
+ error
65
+ });
66
+ });
30
67
  };
31
- return customNode;
32
- }
33
- configure(options) {
34
- return super.configure(options);
35
- }
36
- extend(extendedConfig) {
37
- const resolvedConfig = typeof extendedConfig === "function" ? extendedConfig() : extendedConfig;
38
- return super.extend(resolvedConfig);
68
+ target.addEventListener(prefixedEventName, wrappedHandler, {
69
+ ...options,
70
+ signal: abortController.signal
71
+ });
72
+ return { unsubscribe: () => {
73
+ abortController.abort();
74
+ } };
39
75
  }
40
76
  };
77
+ const editorEventBus = new EditorEventBus();
41
78
 
42
79
  //#endregion
43
- //#region src/extensions/alignment-attribute.tsx
44
- const AlignmentAttribute = Extension.create({
45
- name: "alignmentAttribute",
46
- addOptions() {
47
- return {
48
- types: [],
49
- alignments: [
50
- "left",
51
- "center",
52
- "right",
53
- "justify"
54
- ]
55
- };
56
- },
57
- addGlobalAttributes() {
58
- return [{
59
- types: this.options.types,
60
- attributes: { alignment: {
61
- parseHTML: (element) => {
62
- const explicitAlign = element.getAttribute("align") || element.getAttribute("alignment") || element.style.textAlign;
63
- if (explicitAlign && this.options.alignments.includes(explicitAlign)) return explicitAlign;
64
- return null;
65
- },
66
- renderHTML: (attributes) => {
67
- if (attributes.alignment === "left") return {};
68
- return { alignment: attributes.alignment };
69
- }
70
- } }
71
- }];
72
- },
73
- addCommands() {
74
- return { setAlignment: (alignment) => ({ commands }) => {
75
- if (!this.options.alignments.includes(alignment)) return false;
76
- return this.options.types.every((type) => commands.updateAttributes(type, { alignment }));
77
- } };
78
- },
79
- addKeyboardShortcuts() {
80
- return {
81
- Enter: () => {
82
- const { from } = this.editor.state.selection;
83
- const currentAlignment = this.editor.state.doc.nodeAt(from)?.attrs?.alignment;
84
- if (currentAlignment) requestAnimationFrame(() => {
85
- this.editor.commands.setAlignment(currentAlignment);
86
- });
87
- return false;
88
- },
89
- "Mod-Shift-l": () => this.editor.commands.setAlignment("left"),
90
- "Mod-Shift-e": () => this.editor.commands.setAlignment("center"),
91
- "Mod-Shift-r": () => this.editor.commands.setAlignment("right"),
92
- "Mod-Shift-j": () => this.editor.commands.setAlignment("justify")
80
+ //#region src/core/is-document-visually-empty.ts
81
+ function isDocumentVisuallyEmpty(doc) {
82
+ let nonGlobalNodeCount = 0;
83
+ let firstNonGlobalNode = null;
84
+ for (let index = 0; index < doc.childCount; index += 1) {
85
+ const node = doc.child(index);
86
+ if (node.type.name === "globalContent") continue;
87
+ nonGlobalNodeCount += 1;
88
+ if (firstNonGlobalNode === null) firstNonGlobalNode = {
89
+ type: node.type,
90
+ textContent: node.textContent,
91
+ childCount: node.content.childCount
93
92
  };
94
93
  }
95
- });
96
-
97
- //#endregion
98
- //#region src/utils/attribute-helpers.ts
99
- /**
100
- * Creates TipTap attribute definitions for a list of HTML attributes.
101
- * Each attribute will have the same pattern:
102
- * - default: null
103
- * - parseHTML: extracts the attribute from the element
104
- * - renderHTML: conditionally renders the attribute if it has a value
105
- *
106
- * @param attributeNames - Array of HTML attribute names to create definitions for
107
- * @returns Object with TipTap attribute definitions
108
- *
109
- * @example
110
- * const attrs = createStandardAttributes(['class', 'id', 'title']);
111
- * // Returns:
112
- * // {
113
- * // class: {
114
- * // default: null,
115
- * // parseHTML: (element) => element.getAttribute('class'),
116
- * // renderHTML: (attributes) => attributes.class ? { class: attributes.class } : {}
117
- * // },
118
- * // ...
119
- * // }
120
- */
121
- function createStandardAttributes(attributeNames) {
122
- return Object.fromEntries(attributeNames.map((attr) => [attr, {
123
- default: null,
124
- parseHTML: (element) => element.getAttribute(attr),
125
- renderHTML: (attributes) => {
126
- if (!attributes[attr]) return {};
127
- return { [attr]: attributes[attr] };
128
- }
129
- }]));
94
+ if (nonGlobalNodeCount === 0) return true;
95
+ if (nonGlobalNodeCount !== 1) return false;
96
+ return firstNonGlobalNode?.type.name === "paragraph" && firstNonGlobalNode.textContent.trim().length === 0 && firstNonGlobalNode.childCount === 0;
130
97
  }
131
- /**
132
- * Common HTML attributes used across multiple extensions.
133
- * These preserve attributes during HTML import and editing for better
134
- * fidelity when importing existing email templates.
135
- */
136
- const COMMON_HTML_ATTRIBUTES = [
137
- "id",
138
- "class",
139
- "title",
140
- "lang",
141
- "dir",
142
- "data-id"
143
- ];
144
- /**
145
- * Layout-specific HTML attributes used for positioning and sizing.
146
- */
147
- const LAYOUT_ATTRIBUTES = [
148
- "align",
149
- "width",
150
- "height"
151
- ];
152
- /**
153
- * Table-specific HTML attributes used for table layout and styling.
154
- */
155
- const TABLE_ATTRIBUTES = [
156
- "border",
157
- "cellpadding",
158
- "cellspacing"
159
- ];
160
- /**
161
- * Table cell-specific HTML attributes.
162
- */
163
- const TABLE_CELL_ATTRIBUTES = [
164
- "valign",
165
- "bgcolor",
166
- "colspan",
167
- "rowspan"
168
- ];
169
- /**
170
- * Table header cell-specific HTML attributes.
171
- * These are additional attributes that only apply to <th> elements.
172
- */
173
- const TABLE_HEADER_ATTRIBUTES = [...TABLE_CELL_ATTRIBUTES, "scope"];
174
98
 
175
99
  //#endregion
176
100
  //#region src/utils/styles.ts
@@ -342,137 +266,418 @@ function resolveConflictingStyles(resetStyles, inlineStyles) {
342
266
  }
343
267
 
344
268
  //#endregion
345
- //#region src/extensions/body.tsx
346
- const Body = EmailNode.create({
347
- name: "body",
348
- group: "block",
349
- content: "block+",
350
- defining: true,
351
- isolating: true,
352
- addAttributes() {
353
- return { ...createStandardAttributes([...COMMON_HTML_ATTRIBUTES, ...LAYOUT_ATTRIBUTES]) };
354
- },
355
- parseHTML() {
356
- return [{
357
- tag: "body",
358
- getAttrs: (node) => {
359
- if (typeof node === "string") return false;
360
- const element = node;
361
- const attrs = {};
362
- Array.from(element.attributes).forEach((attr) => {
363
- attrs[attr.name] = attr.value;
364
- });
365
- return attrs;
366
- }
367
- }];
368
- },
369
- renderHTML({ HTMLAttributes }) {
370
- return [
371
- "div",
372
- mergeAttributes(this.options.HTMLAttributes, HTMLAttributes),
373
- 0
374
- ];
375
- },
376
- renderToReactEmail({ children, node, styles }) {
377
- const inlineStyles = inlineCssToJs(node.attrs?.style);
378
- return /* @__PURE__ */ jsx("div", {
379
- className: node.attrs?.class || void 0,
380
- style: {
381
- ...styles.reset,
382
- ...inlineStyles
383
- },
384
- children
385
- });
386
- }
387
- });
269
+ //#region src/core/serializer/default-base-template.tsx
270
+ function DefaultBaseTemplate({ children, previewText }) {
271
+ return /* @__PURE__ */ jsxs(Html, { children: [
272
+ /* @__PURE__ */ jsxs(Head, { children: [
273
+ /* @__PURE__ */ jsx("meta", {
274
+ content: "width=device-width",
275
+ name: "viewport"
276
+ }),
277
+ /* @__PURE__ */ jsx("meta", {
278
+ content: "IE=edge",
279
+ httpEquiv: "X-UA-Compatible"
280
+ }),
281
+ /* @__PURE__ */ jsx("meta", { name: "x-apple-disable-message-reformatting" }),
282
+ /* @__PURE__ */ jsx("meta", {
283
+ content: "telephone=no,address=no,email=no,date=no,url=no",
284
+ name: "format-detection"
285
+ })
286
+ ] }),
287
+ previewText && previewText !== "" && /* @__PURE__ */ jsx(Preview, { children: previewText }),
288
+ /* @__PURE__ */ jsx(Body$1, { children: /* @__PURE__ */ jsx(Section$1, {
289
+ width: "100%",
290
+ align: "center",
291
+ children: /* @__PURE__ */ jsx(Section$1, {
292
+ style: { width: "100%" },
293
+ children
294
+ })
295
+ }) })
296
+ ] });
297
+ }
388
298
 
389
299
  //#endregion
390
- //#region src/extensions/bold.ts
391
- /**
392
- * Matches bold text via `**` as input.
393
- */
394
- const starInputRegex = /(?:^|\s)(\*\*(?!\s+\*\*)((?:[^*]+))\*\*(?!\s+\*\*))$/;
395
- /**
396
- * Matches bold text via `**` while pasting.
397
- */
398
- const starPasteRegex = /(?:^|\s)(\*\*(?!\s+\*\*)((?:[^*]+))\*\*(?!\s+\*\*))/g;
399
- /**
400
- * Matches bold text via `__` as input.
401
- */
402
- const underscoreInputRegex = /(?:^|\s)(__(?!\s+__)((?:[^_]+))__(?!\s+__))$/;
403
- /**
404
- * Matches bold text via `__` while pasting.
405
- */
406
- const underscorePasteRegex = /(?:^|\s)(__(?!\s+__)((?:[^_]+))__(?!\s+__))/g;
407
- /**
408
- * This extension allows you to mark text as bold.
409
- * @see https://tiptap.dev/api/marks/bold
410
- */
411
- const Bold = Mark.create({
412
- name: "bold",
413
- addOptions() {
414
- return { HTMLAttributes: {} };
415
- },
416
- parseHTML() {
417
- return [
418
- { tag: "strong" },
419
- {
420
- tag: "b",
421
- getAttrs: (node) => node.style.fontWeight !== "normal" && null
422
- },
423
- {
424
- style: "font-weight=400",
425
- clearMark: (mark) => mark.type.name === this.name
426
- }
427
- ];
428
- },
429
- renderHTML({ HTMLAttributes }) {
430
- return [
431
- "strong",
432
- mergeAttributes(this.options.HTMLAttributes, HTMLAttributes),
433
- 0
434
- ];
435
- },
436
- addCommands() {
437
- return {
438
- setBold: () => ({ commands }) => {
439
- return commands.setMark(this.name);
440
- },
441
- toggleBold: () => ({ commands }) => {
442
- return commands.toggleMark(this.name);
443
- },
444
- unsetBold: () => ({ commands }) => {
445
- return commands.unsetMark(this.name);
446
- }
447
- };
448
- },
449
- addKeyboardShortcuts() {
450
- return {
451
- "Mod-b": () => this.editor.commands.toggleBold(),
452
- "Mod-B": () => this.editor.commands.toggleBold()
300
+ //#region src/core/serializer/email-mark.ts
301
+ var EmailMark = class EmailMark extends Mark {
302
+ constructor(config) {
303
+ super(config);
304
+ }
305
+ /**
306
+ * Create a new Mark instance
307
+ * @param config - Mark configuration object or a function that returns a configuration object
308
+ */
309
+ static create(config) {
310
+ return new EmailMark(typeof config === "function" ? config() : config);
311
+ }
312
+ static from(mark, renderToReactEmail) {
313
+ const customMark = EmailMark.create({});
314
+ Object.assign(customMark, { ...mark });
315
+ customMark.config = {
316
+ ...mark.config,
317
+ renderToReactEmail
453
318
  };
454
- },
455
- addInputRules() {
456
- return [markInputRule({
457
- find: starInputRegex,
458
- type: this.type
459
- }), markInputRule({
460
- find: underscoreInputRegex,
461
- type: this.type
462
- })];
463
- },
464
- addPasteRules() {
465
- return [markPasteRule({
466
- find: starPasteRegex,
467
- type: this.type
468
- }), markPasteRule({
469
- find: underscorePasteRegex,
470
- type: this.type
471
- })];
319
+ return customMark;
472
320
  }
473
- });
474
-
475
- //#endregion
321
+ configure(options) {
322
+ return super.configure(options);
323
+ }
324
+ extend(extendedConfig) {
325
+ const resolvedConfig = typeof extendedConfig === "function" ? extendedConfig() : extendedConfig;
326
+ return super.extend(resolvedConfig);
327
+ }
328
+ };
329
+
330
+ //#endregion
331
+ //#region src/core/serializer/email-node.ts
332
+ var EmailNode = class EmailNode extends Node$1 {
333
+ constructor(config) {
334
+ super(config);
335
+ }
336
+ /**
337
+ * Create a new Node instance
338
+ * @param config - Node configuration object or a function that returns a configuration object
339
+ */
340
+ static create(config) {
341
+ return new EmailNode(typeof config === "function" ? config() : config);
342
+ }
343
+ static from(node, renderToReactEmail) {
344
+ const customNode = EmailNode.create({});
345
+ Object.assign(customNode, { ...node });
346
+ customNode.config = {
347
+ ...node.config,
348
+ renderToReactEmail
349
+ };
350
+ return customNode;
351
+ }
352
+ configure(options) {
353
+ return super.configure(options);
354
+ }
355
+ extend(extendedConfig) {
356
+ const resolvedConfig = typeof extendedConfig === "function" ? extendedConfig() : extendedConfig;
357
+ return super.extend(resolvedConfig);
358
+ }
359
+ };
360
+
361
+ //#endregion
362
+ //#region src/core/serializer/compose-react-email.tsx
363
+ const MARK_ORDER = {
364
+ preservedStyle: 0,
365
+ italic: 1,
366
+ strike: 2,
367
+ underline: 3,
368
+ link: 4,
369
+ bold: 5,
370
+ code: 6
371
+ };
372
+ const NODES_WITH_INCREMENTED_CHILD_DEPTH = new Set(["bulletList", "orderedList"]);
373
+ function getOrderedMarks(marks) {
374
+ if (!marks) return [];
375
+ return [...marks].sort((a, b) => (MARK_ORDER[a.type] ?? Number.MAX_SAFE_INTEGER) - (MARK_ORDER[b.type] ?? Number.MAX_SAFE_INTEGER));
376
+ }
377
+ const composeReactEmail = async ({ editor, preview }) => {
378
+ const data = editor.getJSON();
379
+ const extensions = editor.extensionManager.extensions;
380
+ const serializerPlugin = extensions.map((ext) => ext.options?.serializerPlugin).filter((p) => Boolean(p)).at(-1);
381
+ const emailNodeComponentRegistry = Object.fromEntries(extensions.filter((ext) => ext instanceof EmailNode).map((extension) => [extension.name, extension.config.renderToReactEmail]));
382
+ const emailMarkComponentRegistry = Object.fromEntries(extensions.filter((ext) => ext instanceof EmailMark).map((extension) => [extension.name, extension.config.renderToReactEmail]));
383
+ function renderMark(mark, node, children, depth) {
384
+ const markStyle = serializerPlugin?.getNodeStyles({
385
+ type: mark.type,
386
+ attrs: mark.attrs ?? {}
387
+ }, depth, editor) ?? {};
388
+ const markRenderer = emailMarkComponentRegistry[mark.type];
389
+ if (markRenderer) return markRenderer({
390
+ mark,
391
+ node,
392
+ style: markStyle,
393
+ children
394
+ });
395
+ return children;
396
+ }
397
+ function parseContent(content, depth = 0) {
398
+ if (!content) return;
399
+ return content.map((node, index) => {
400
+ const style = serializerPlugin?.getNodeStyles(node, depth, editor) ?? {};
401
+ const inlineStyles = inlineCssToJs(node.attrs?.style);
402
+ if (node.type && emailNodeComponentRegistry[node.type]) {
403
+ const Component = emailNodeComponentRegistry[node.type];
404
+ const childDepth = NODES_WITH_INCREMENTED_CHILD_DEPTH.has(node.type) ? depth + 1 : depth;
405
+ return /* @__PURE__ */ jsx(Component, {
406
+ node: node.type === "table" && inlineStyles.width && !node.attrs?.width ? {
407
+ ...node,
408
+ attrs: {
409
+ ...node.attrs,
410
+ width: inlineStyles.width
411
+ }
412
+ } : node,
413
+ style,
414
+ children: parseContent(node.content, childDepth)
415
+ }, index);
416
+ }
417
+ switch (node.type) {
418
+ case "text": {
419
+ let wrappedText = node.text;
420
+ getOrderedMarks(node.marks).forEach((mark) => {
421
+ wrappedText = renderMark(mark, node, wrappedText, depth);
422
+ });
423
+ const textAttributes = node.marks?.find((mark) => mark.type === "textStyle")?.attrs;
424
+ return /* @__PURE__ */ jsx("span", {
425
+ style: {
426
+ ...textAttributes,
427
+ ...style
428
+ },
429
+ children: wrappedText
430
+ }, index);
431
+ }
432
+ default: return null;
433
+ }
434
+ });
435
+ }
436
+ const unformattedHtml = await render(/* @__PURE__ */ jsx(serializerPlugin?.BaseTemplate ?? DefaultBaseTemplate, {
437
+ previewText: preview,
438
+ editor,
439
+ children: parseContent(data.content)
440
+ }));
441
+ const [prettyHtml, text] = await Promise.all([pretty(unformattedHtml), toPlainText(unformattedHtml)]);
442
+ return {
443
+ html: prettyHtml,
444
+ text
445
+ };
446
+ };
447
+
448
+ //#endregion
449
+ //#region src/extensions/alignment-attribute.tsx
450
+ const AlignmentAttribute = Extension.create({
451
+ name: "alignmentAttribute",
452
+ addOptions() {
453
+ return {
454
+ types: [],
455
+ alignments: [
456
+ "left",
457
+ "center",
458
+ "right",
459
+ "justify"
460
+ ]
461
+ };
462
+ },
463
+ addGlobalAttributes() {
464
+ return [{
465
+ types: this.options.types,
466
+ attributes: { alignment: {
467
+ parseHTML: (element) => {
468
+ const explicitAlign = element.getAttribute("align") || element.getAttribute("alignment") || element.style.textAlign;
469
+ if (explicitAlign && this.options.alignments.includes(explicitAlign)) return explicitAlign;
470
+ return null;
471
+ },
472
+ renderHTML: (attributes) => {
473
+ if (attributes.alignment === "left") return {};
474
+ return { alignment: attributes.alignment };
475
+ }
476
+ } }
477
+ }];
478
+ },
479
+ addCommands() {
480
+ return { setAlignment: (alignment) => ({ commands }) => {
481
+ if (!this.options.alignments.includes(alignment)) return false;
482
+ return this.options.types.every((type) => commands.updateAttributes(type, { alignment }));
483
+ } };
484
+ },
485
+ addKeyboardShortcuts() {
486
+ return {
487
+ Enter: () => {
488
+ const { from } = this.editor.state.selection;
489
+ const currentAlignment = this.editor.state.doc.nodeAt(from)?.attrs?.alignment;
490
+ if (currentAlignment) requestAnimationFrame(() => {
491
+ this.editor.commands.setAlignment(currentAlignment);
492
+ });
493
+ return false;
494
+ },
495
+ "Mod-Shift-l": () => this.editor.commands.setAlignment("left"),
496
+ "Mod-Shift-e": () => this.editor.commands.setAlignment("center"),
497
+ "Mod-Shift-r": () => this.editor.commands.setAlignment("right"),
498
+ "Mod-Shift-j": () => this.editor.commands.setAlignment("justify")
499
+ };
500
+ }
501
+ });
502
+
503
+ //#endregion
504
+ //#region src/utils/get-text-alignment.ts
505
+ function getTextAlignment(alignment) {
506
+ switch (alignment) {
507
+ case "left": return { textAlign: "left" };
508
+ case "center": return { textAlign: "center" };
509
+ case "right": return { textAlign: "right" };
510
+ default: return {};
511
+ }
512
+ }
513
+
514
+ //#endregion
515
+ //#region src/extensions/blockquote.tsx
516
+ const Blockquote = EmailNode.from(BlockquoteBase, ({ children, node, style }) => /* @__PURE__ */ jsx("blockquote", {
517
+ className: node.attrs?.class || void 0,
518
+ style: {
519
+ ...style,
520
+ ...inlineCssToJs(node.attrs?.style),
521
+ ...getTextAlignment(node.attrs?.align || node.attrs?.alignment)
522
+ },
523
+ children
524
+ }));
525
+
526
+ //#endregion
527
+ //#region src/utils/attribute-helpers.ts
528
+ /**
529
+ * Creates TipTap attribute definitions for a list of HTML attributes.
530
+ * Each attribute will have the same pattern:
531
+ * - default: null
532
+ * - parseHTML: extracts the attribute from the element
533
+ * - renderHTML: conditionally renders the attribute if it has a value
534
+ *
535
+ * @param attributeNames - Array of HTML attribute names to create definitions for
536
+ * @returns Object with TipTap attribute definitions
537
+ *
538
+ * @example
539
+ * const attrs = createStandardAttributes(['class', 'id', 'title']);
540
+ * // Returns:
541
+ * // {
542
+ * // class: {
543
+ * // default: null,
544
+ * // parseHTML: (element) => element.getAttribute('class'),
545
+ * // renderHTML: (attributes) => attributes.class ? { class: attributes.class } : {}
546
+ * // },
547
+ * // ...
548
+ * // }
549
+ */
550
+ function createStandardAttributes(attributeNames) {
551
+ return Object.fromEntries(attributeNames.map((attr) => [attr, {
552
+ default: null,
553
+ parseHTML: (element) => element.getAttribute(attr),
554
+ renderHTML: (attributes) => {
555
+ if (!attributes[attr]) return {};
556
+ return { [attr]: attributes[attr] };
557
+ }
558
+ }]));
559
+ }
560
+ /**
561
+ * Common HTML attributes used across multiple extensions.
562
+ * These preserve attributes during HTML import and editing for better
563
+ * fidelity when importing existing email templates.
564
+ */
565
+ const COMMON_HTML_ATTRIBUTES = [
566
+ "id",
567
+ "class",
568
+ "title",
569
+ "lang",
570
+ "dir",
571
+ "data-id"
572
+ ];
573
+ /**
574
+ * Layout-specific HTML attributes used for positioning and sizing.
575
+ */
576
+ const LAYOUT_ATTRIBUTES = [
577
+ "align",
578
+ "width",
579
+ "height"
580
+ ];
581
+ /**
582
+ * Table-specific HTML attributes used for table layout and styling.
583
+ */
584
+ const TABLE_ATTRIBUTES = [
585
+ "border",
586
+ "cellpadding",
587
+ "cellspacing"
588
+ ];
589
+ /**
590
+ * Table cell-specific HTML attributes.
591
+ */
592
+ const TABLE_CELL_ATTRIBUTES = [
593
+ "valign",
594
+ "bgcolor",
595
+ "colspan",
596
+ "rowspan"
597
+ ];
598
+ /**
599
+ * Table header cell-specific HTML attributes.
600
+ * These are additional attributes that only apply to <th> elements.
601
+ */
602
+ const TABLE_HEADER_ATTRIBUTES = [...TABLE_CELL_ATTRIBUTES, "scope"];
603
+
604
+ //#endregion
605
+ //#region src/extensions/body.tsx
606
+ const Body = EmailNode.create({
607
+ name: "body",
608
+ group: "block",
609
+ content: "block+",
610
+ defining: true,
611
+ isolating: true,
612
+ addAttributes() {
613
+ return { ...createStandardAttributes([...COMMON_HTML_ATTRIBUTES, ...LAYOUT_ATTRIBUTES]) };
614
+ },
615
+ parseHTML() {
616
+ return [{
617
+ tag: "body",
618
+ getAttrs: (node) => {
619
+ if (typeof node === "string") return false;
620
+ const element = node;
621
+ const attrs = {};
622
+ Array.from(element.attributes).forEach((attr) => {
623
+ attrs[attr.name] = attr.value;
624
+ });
625
+ return attrs;
626
+ }
627
+ }];
628
+ },
629
+ renderHTML({ HTMLAttributes }) {
630
+ return [
631
+ "div",
632
+ mergeAttributes(this.options.HTMLAttributes, HTMLAttributes),
633
+ 0
634
+ ];
635
+ },
636
+ renderToReactEmail({ children, node, style }) {
637
+ const inlineStyles = inlineCssToJs(node.attrs?.style);
638
+ return /* @__PURE__ */ jsx("div", {
639
+ className: node.attrs?.class || void 0,
640
+ style: {
641
+ ...style,
642
+ ...inlineStyles
643
+ },
644
+ children
645
+ });
646
+ }
647
+ });
648
+
649
+ //#endregion
650
+ //#region src/extensions/bold.tsx
651
+ const BoldWithoutFontWeightInference = BoldBase.extend({ parseHTML() {
652
+ return [
653
+ { tag: "strong" },
654
+ {
655
+ tag: "b",
656
+ getAttrs: (node) => node.style.fontWeight !== "normal" && null
657
+ },
658
+ {
659
+ style: "font-weight=400",
660
+ clearMark: (mark) => mark.type.name === this.name
661
+ }
662
+ ];
663
+ } });
664
+ const Bold = EmailMark.from(BoldWithoutFontWeightInference, ({ children, style }) => /* @__PURE__ */ jsx("strong", {
665
+ style,
666
+ children
667
+ }));
668
+
669
+ //#endregion
670
+ //#region src/extensions/bullet-list.tsx
671
+ const BulletList = EmailNode.from(BulletListBase, ({ children, node, style }) => /* @__PURE__ */ jsx("ul", {
672
+ className: node.attrs?.class || void 0,
673
+ style: {
674
+ ...style,
675
+ ...inlineCssToJs(node.attrs?.style)
676
+ },
677
+ children
678
+ }));
679
+
680
+ //#endregion
476
681
  //#region src/extensions/button.tsx
477
682
  const Button = EmailNode.create({
478
683
  name: "button",
@@ -534,7 +739,7 @@ const Button = EmailNode.create({
534
739
  }
535
740
  };
536
741
  },
537
- renderToReactEmail({ children, node, styles }) {
742
+ renderToReactEmail({ children, node, style }) {
538
743
  const inlineStyles = inlineCssToJs(node.attrs?.style);
539
744
  return /* @__PURE__ */ jsx(Row, { children: /* @__PURE__ */ jsx(Column, {
540
745
  align: node.attrs?.align || node.attrs?.alignment,
@@ -542,8 +747,7 @@ const Button = EmailNode.create({
542
747
  className: node.attrs?.class || void 0,
543
748
  href: node.attrs?.href,
544
749
  style: {
545
- ...styles.reset,
546
- ...styles.button,
750
+ ...style,
547
751
  ...inlineStyles
548
752
  },
549
753
  children
@@ -594,6 +798,16 @@ const ClassAttribute = Extension.create({
594
798
  }
595
799
  });
596
800
 
801
+ //#endregion
802
+ //#region src/extensions/code.tsx
803
+ const Code = EmailMark.from(CodeBase, ({ children, node, style }) => /* @__PURE__ */ jsx("code", {
804
+ style: {
805
+ ...style,
806
+ ...inlineCssToJs(node.attrs?.style)
807
+ },
808
+ children
809
+ }));
810
+
597
811
  //#endregion
598
812
  //#region src/utils/prism-utils.ts
599
813
  const publicURL = "/styles/prism";
@@ -772,6 +986,25 @@ const CodeBlockPrism = EmailNode.from(CodeBlock$1.extend({
772
986
  ]
773
987
  ];
774
988
  },
989
+ addKeyboardShortcuts() {
990
+ return {
991
+ ...this.parent?.(),
992
+ "Mod-a": ({ editor }) => {
993
+ const { state } = editor;
994
+ const { selection } = state;
995
+ const { $from } = selection;
996
+ for (let depth = $from.depth; depth >= 1; depth--) if ($from.node(depth).type.name === this.name) {
997
+ const blockStart = $from.start(depth);
998
+ const blockEnd = $from.end(depth);
999
+ if (selection.from === blockStart && selection.to === blockEnd) return false;
1000
+ const tr = state.tr.setSelection(TextSelection.create(state.doc, blockStart, blockEnd));
1001
+ editor.view.dispatch(tr);
1002
+ return true;
1003
+ }
1004
+ return false;
1005
+ }
1006
+ };
1007
+ },
775
1008
  addProseMirrorPlugins() {
776
1009
  return [...this.parent?.() || [], PrismPlugin({
777
1010
  name: this.name,
@@ -779,7 +1012,7 @@ const CodeBlockPrism = EmailNode.from(CodeBlock$1.extend({
779
1012
  defaultTheme: this.options.defaultTheme
780
1013
  })];
781
1014
  }
782
- }), ({ node, styles }) => {
1015
+ }), ({ node, style }) => {
783
1016
  const language = node.attrs?.language ? `${node.attrs.language}` : "javascript";
784
1017
  const userTheme = ReactEmailComponents[node.attrs?.theme];
785
1018
  const theme = userTheme ? {
@@ -803,7 +1036,7 @@ const CodeBlockPrism = EmailNode.from(CodeBlock$1.extend({
803
1036
  theme,
804
1037
  style: {
805
1038
  width: "auto",
806
- ...styles.codeBlock
1039
+ ...style
807
1040
  }
808
1041
  });
809
1042
  });
@@ -840,12 +1073,12 @@ const Div = EmailNode.create({
840
1073
  addAttributes() {
841
1074
  return { ...createStandardAttributes([...COMMON_HTML_ATTRIBUTES, ...LAYOUT_ATTRIBUTES]) };
842
1075
  },
843
- renderToReactEmail({ children, node, styles }) {
1076
+ renderToReactEmail({ children, node, style }) {
844
1077
  const inlineStyles = inlineCssToJs(node.attrs?.style);
845
1078
  return /* @__PURE__ */ jsx("div", {
846
1079
  className: node.attrs?.class || void 0,
847
1080
  style: {
848
- ...styles.reset,
1081
+ ...style,
849
1082
  ...inlineStyles
850
1083
  },
851
1084
  children
@@ -854,97 +1087,162 @@ const Div = EmailNode.create({
854
1087
  });
855
1088
 
856
1089
  //#endregion
857
- //#region src/extensions/max-nesting.ts
858
- const MaxNesting = Extension.create({
859
- name: "maxNesting",
1090
+ //#region src/extensions/divider.tsx
1091
+ const Divider = EmailNode.from(HorizontalRule.extend({
1092
+ addAttributes() {
1093
+ return { class: { default: "divider" } };
1094
+ },
1095
+ addInputRules() {
1096
+ return [new InputRule({
1097
+ find: /^(?:---|—-|___\s|\*\*\*\s)$/,
1098
+ handler: ({ state, range }) => {
1099
+ const attributes = {};
1100
+ const { tr } = state;
1101
+ const start = range.from;
1102
+ const end = range.to;
1103
+ tr.insert(start - 1, this.type.create(attributes)).delete(tr.mapping.map(start), tr.mapping.map(end));
1104
+ }
1105
+ })];
1106
+ },
1107
+ addNodeView() {
1108
+ return ReactNodeViewRenderer((props) => {
1109
+ const node = props.node;
1110
+ const { class: className, ...rest } = node.attrs;
1111
+ return /* @__PURE__ */ jsx(NodeViewWrapper, { children: /* @__PURE__ */ jsx(Hr, {
1112
+ ...rest,
1113
+ className: "node-hr",
1114
+ style: inlineCssToJs(node.attrs.style)
1115
+ }) });
1116
+ });
1117
+ }
1118
+ }), ({ node, style }) => {
1119
+ return /* @__PURE__ */ jsx(Hr, {
1120
+ className: node.attrs?.class || void 0,
1121
+ style: {
1122
+ ...style,
1123
+ ...inlineCssToJs(node.attrs?.style)
1124
+ }
1125
+ });
1126
+ });
1127
+
1128
+ //#endregion
1129
+ //#region src/extensions/global-content.ts
1130
+ const GLOBAL_CONTENT_NODE_TYPE = "globalContent";
1131
+ let cachedGlobalPosition = null;
1132
+ function findGlobalContentPositions(doc) {
1133
+ const positions = [];
1134
+ doc.descendants((node, position) => {
1135
+ if (node.type.name === GLOBAL_CONTENT_NODE_TYPE) positions.push(position);
1136
+ });
1137
+ return positions;
1138
+ }
1139
+ function getCachedGlobalContentPosition(doc) {
1140
+ if (cachedGlobalPosition != null) try {
1141
+ if (doc.nodeAt(cachedGlobalPosition)?.type.name === GLOBAL_CONTENT_NODE_TYPE) return cachedGlobalPosition;
1142
+ } catch {
1143
+ cachedGlobalPosition = null;
1144
+ }
1145
+ cachedGlobalPosition = findGlobalContentPositions(doc)[0] ?? null;
1146
+ return cachedGlobalPosition;
1147
+ }
1148
+ function getGlobalContent(key, editor) {
1149
+ const position = getCachedGlobalContentPosition(editor.state.doc);
1150
+ if (cachedGlobalPosition == null) return null;
1151
+ return editor.state.doc.nodeAt(position)?.attrs.data[key] ?? null;
1152
+ }
1153
+ const GlobalContent = Node$1.create({
1154
+ name: GLOBAL_CONTENT_NODE_TYPE,
860
1155
  addOptions() {
861
1156
  return {
862
- maxDepth: 3,
863
- nodeTypes: void 0
1157
+ key: GLOBAL_CONTENT_NODE_TYPE,
1158
+ data: {}
864
1159
  };
865
1160
  },
866
- addProseMirrorPlugins() {
867
- const { maxDepth, nodeTypes } = this.options;
868
- if (typeof maxDepth !== "number" || maxDepth < 1) throw new Error("maxDepth must be a positive number");
869
- return [new Plugin({
870
- key: new PluginKey("maxNesting"),
871
- appendTransaction(transactions, _oldState, newState) {
872
- if (!transactions.some((tr$1) => tr$1.docChanged)) return null;
873
- const rangesToLift = [];
874
- newState.doc.descendants((node, pos) => {
875
- let depth = 0;
876
- let currentPos = pos;
877
- let currentNode = node;
878
- while (currentNode && depth <= maxDepth) {
879
- if (!nodeTypes || nodeTypes.includes(currentNode.type.name)) depth++;
880
- const $pos = newState.doc.resolve(currentPos);
881
- if ($pos.depth === 0) break;
882
- currentPos = $pos.before($pos.depth);
883
- currentNode = newState.doc.nodeAt(currentPos);
884
- }
885
- if (depth > maxDepth) {
886
- const $pos = newState.doc.resolve(pos);
887
- if ($pos.depth > 0) {
888
- const range = $pos.blockRange();
889
- if (range && "canReplace" in newState.schema.nodes.doc && typeof newState.schema.nodes.doc.canReplace === "function" && newState.schema.nodes.doc.canReplace(range.start - 1, range.end + 1, newState.doc.slice(range.start, range.end).content)) rangesToLift.push({
890
- range,
891
- target: range.start - 1
892
- });
893
- }
894
- }
895
- });
896
- if (rangesToLift.length === 0) return null;
897
- const tr = newState.tr;
898
- for (let i = rangesToLift.length - 1; i >= 0; i--) {
899
- const { range, target } = rangesToLift[i];
900
- tr.lift(range, target);
1161
+ group: "block",
1162
+ selectable: false,
1163
+ draggable: false,
1164
+ atom: true,
1165
+ addAttributes() {
1166
+ return { data: { default: this.options.data } };
1167
+ },
1168
+ parseHTML() {
1169
+ return [{ tag: `div[data-type="${this.name}"]` }];
1170
+ },
1171
+ renderHTML({ HTMLAttributes }) {
1172
+ return ["div", mergeAttributes(HTMLAttributes, {
1173
+ "data-type": this.name,
1174
+ style: "width: 100%; height: 1px; visibility: hidden;"
1175
+ })];
1176
+ },
1177
+ addCommands() {
1178
+ return { setGlobalContent: (key, value) => ({ tr, dispatch }) => {
1179
+ const ensureGlobalPosition = () => {
1180
+ const positions = findGlobalContentPositions(tr.doc);
1181
+ for (let i = positions.length - 1; i > 0; i--) tr.delete(positions[i], positions[i] + 1);
1182
+ const pos = positions[0] ?? -1;
1183
+ if (pos >= 0) cachedGlobalPosition = pos;
1184
+ else {
1185
+ cachedGlobalPosition = 0;
1186
+ tr.insert(0, this.type.create());
901
1187
  }
902
- return tr;
903
- },
904
- filterTransaction(tr) {
905
- if (!tr.docChanged) return true;
906
- let wouldCreateDeepNesting = false;
907
- const newDoc = tr.doc;
908
- newDoc.descendants((node, pos) => {
909
- if (wouldCreateDeepNesting) return false;
910
- let depth = 0;
911
- let currentPos = pos;
912
- let currentNode = node;
913
- while (currentNode && depth <= maxDepth) {
914
- if (!nodeTypes || nodeTypes.includes(currentNode.type.name)) depth++;
915
- const $pos = newDoc.resolve(currentPos);
916
- if ($pos.depth === 0) break;
917
- currentPos = $pos.before($pos.depth);
918
- currentNode = newDoc.nodeAt(currentPos);
919
- }
920
- if (depth > maxDepth) {
921
- wouldCreateDeepNesting = true;
922
- return false;
923
- }
1188
+ };
1189
+ if (dispatch) {
1190
+ ensureGlobalPosition();
1191
+ if (cachedGlobalPosition == null) return false;
1192
+ tr.setNodeAttribute(cachedGlobalPosition, "data", {
1193
+ ...tr.doc.nodeAt(cachedGlobalPosition)?.attrs.data,
1194
+ [key]: value
924
1195
  });
925
- return !wouldCreateDeepNesting;
926
1196
  }
927
- })];
1197
+ return true;
1198
+ } };
928
1199
  }
929
1200
  });
930
1201
 
931
1202
  //#endregion
932
- //#region src/extensions/placeholder.ts
933
- const createPlaceholderExtension = (options) => {
934
- return Placeholder.configure({
935
- placeholder: ({ node }) => {
936
- if (node.type.name === "heading") return `Heading ${node.attrs.level}`;
937
- return "Press '/' for commands";
1203
+ //#region src/extensions/hard-break.tsx
1204
+ const HardBreak = EmailNode.from(HardBreakBase, () => /* @__PURE__ */ jsx("br", {}));
1205
+
1206
+ //#endregion
1207
+ //#region src/extensions/heading.tsx
1208
+ const Heading = EmailNode.from(Heading$2.extend({ addNodeView() {
1209
+ return ReactNodeViewRenderer(({ node }) => {
1210
+ const level = node.attrs.level ?? 1;
1211
+ const { class: className, ...rest } = node.attrs;
1212
+ const attrs = {
1213
+ ...rest,
1214
+ className: `node-h${level} ${className}`,
1215
+ style: inlineCssToJs(node.attrs.style)
1216
+ };
1217
+ return /* @__PURE__ */ jsx(NodeViewWrapper, { children: /* @__PURE__ */ jsx(Heading$1, {
1218
+ as: `h${level}`,
1219
+ ...attrs,
1220
+ children: /* @__PURE__ */ jsx(NodeViewContent, {})
1221
+ }) });
1222
+ });
1223
+ } }), ({ children, node, style }) => {
1224
+ return /* @__PURE__ */ jsx(Heading$1, {
1225
+ as: `h${node.attrs?.level ?? 1}`,
1226
+ className: node.attrs?.class || void 0,
1227
+ style: {
1228
+ ...style,
1229
+ ...inlineCssToJs(node.attrs?.style),
1230
+ ...getTextAlignment(node.attrs?.align ?? node.attrs?.alignment)
938
1231
  },
939
- includeChildren: true,
940
- ...options
1232
+ children
941
1233
  });
942
- };
943
- const placeholder = createPlaceholderExtension();
1234
+ });
1235
+
1236
+ //#endregion
1237
+ //#region src/extensions/italic.tsx
1238
+ const Italic = EmailMark.from(ItalicBase, ({ children, style }) => /* @__PURE__ */ jsx("em", {
1239
+ style,
1240
+ children
1241
+ }));
944
1242
 
945
1243
  //#endregion
946
- //#region src/extensions/preserved-style.ts
947
- const PreservedStyle = Mark.create({
1244
+ //#region src/extensions/preserved-style.tsx
1245
+ const PreservedStyle = EmailMark.create({
948
1246
  name: "preservedStyle",
949
1247
  addAttributes() {
950
1248
  return { style: {
@@ -973,6 +1271,12 @@ const PreservedStyle = Mark.create({
973
1271
  mergeAttributes(HTMLAttributes),
974
1272
  0
975
1273
  ];
1274
+ },
1275
+ renderToReactEmail({ children, mark }) {
1276
+ return /* @__PURE__ */ jsx("span", {
1277
+ style: mark.attrs?.style ? inlineCssToJs(mark.attrs.style) : void 0,
1278
+ children
1279
+ });
976
1280
  }
977
1281
  });
978
1282
  const LINK_INDICATOR_STYLES = [
@@ -1017,139 +1321,358 @@ function processStylesForUnlink(styleString) {
1017
1321
  }
1018
1322
 
1019
1323
  //#endregion
1020
- //#region src/utils/get-text-alignment.ts
1021
- function getTextAlignment(alignment) {
1022
- switch (alignment) {
1023
- case "left": return { textAlign: "left" };
1024
- case "center": return { textAlign: "center" };
1025
- case "right": return { textAlign: "right" };
1026
- default: return {};
1027
- }
1028
- }
1029
-
1030
- //#endregion
1031
- //#region src/extensions/section.tsx
1032
- const Section = EmailNode.create({
1033
- name: "section",
1034
- group: "block",
1035
- content: "block+",
1036
- isolating: true,
1037
- defining: true,
1324
+ //#region src/extensions/link.tsx
1325
+ const Link = EmailMark.from(TiptapLink, ({ children, mark, style }) => {
1326
+ const linkMarkStyle = mark.attrs?.style ? inlineCssToJs(mark.attrs.style) : {};
1327
+ return /* @__PURE__ */ jsx(Link$1, {
1328
+ href: mark.attrs?.href ?? void 0,
1329
+ rel: mark.attrs?.rel ?? void 0,
1330
+ style: {
1331
+ ...style,
1332
+ ...linkMarkStyle
1333
+ },
1334
+ target: mark.attrs?.target ?? void 0,
1335
+ ...mark.attrs?.["ses:no-track"] ? { "ses:no-track": mark.attrs["ses:no-track"] } : {},
1336
+ children
1337
+ });
1338
+ }).extend({
1038
1339
  parseHTML() {
1039
- return [{ tag: "section[data-type=\"section\"]" }];
1040
- },
1041
- renderHTML({ HTMLAttributes }) {
1042
- return [
1043
- "section",
1044
- mergeAttributes({
1045
- "data-type": "section",
1046
- class: "node-section"
1047
- }, HTMLAttributes),
1048
- 0
1049
- ];
1050
- },
1051
- addCommands() {
1052
- return { insertSection: () => ({ commands }) => {
1053
- return commands.insertContent({
1054
- type: this.name,
1055
- content: [{
1056
- type: "paragraph",
1057
- content: []
1058
- }]
1059
- });
1060
- } };
1340
+ return [{
1341
+ tag: "a[target]:not([data-id=\"react-email-button\"])",
1342
+ getAttrs: (node) => {
1343
+ if (typeof node === "string") return false;
1344
+ const element = node;
1345
+ const attrs = {};
1346
+ Array.from(element.attributes).forEach((attr) => {
1347
+ attrs[attr.name] = attr.value;
1348
+ });
1349
+ return attrs;
1350
+ }
1351
+ }, {
1352
+ tag: "a[href]:not([data-id=\"react-email-button\"])",
1353
+ getAttrs: (node) => {
1354
+ if (typeof node === "string") return false;
1355
+ const element = node;
1356
+ const attrs = {};
1357
+ Array.from(element.attributes).forEach((attr) => {
1358
+ attrs[attr.name] = attr.value;
1359
+ });
1360
+ return attrs;
1361
+ }
1362
+ }];
1061
1363
  },
1062
- renderToReactEmail({ children, node, styles }) {
1063
- const inlineStyles = inlineCssToJs(node.attrs?.style);
1064
- const textAlign = node.attrs?.align || node.attrs?.alignment;
1065
- return /* @__PURE__ */ jsx(Section$1, {
1066
- className: node.attrs?.class || void 0,
1067
- align: textAlign,
1068
- style: {
1069
- ...styles.section,
1070
- ...inlineStyles,
1071
- ...getTextAlignment(textAlign)
1072
- },
1073
- children
1074
- });
1075
- }
1076
- });
1077
-
1078
- //#endregion
1079
- //#region src/extensions/style-attribute.tsx
1080
- const StyleAttribute = Extension.create({
1081
- name: "styleAttribute",
1082
- priority: 101,
1083
- addOptions() {
1364
+ addAttributes() {
1084
1365
  return {
1085
- types: [],
1086
- style: []
1366
+ ...this.parent?.(),
1367
+ "ses:no-track": {
1368
+ default: null,
1369
+ parseHTML: (element) => element.getAttribute("ses:no-track")
1370
+ }
1087
1371
  };
1088
1372
  },
1089
- addGlobalAttributes() {
1090
- return [{
1091
- types: this.options.types,
1092
- attributes: { style: {
1093
- default: "",
1094
- parseHTML: (element) => element.getAttribute("style") || "",
1095
- renderHTML: (attributes) => {
1096
- return { style: attributes.style ?? "" };
1097
- }
1098
- } }
1099
- }];
1100
- },
1101
1373
  addCommands() {
1102
1374
  return {
1103
- unsetStyle: () => ({ commands }) => {
1104
- return this.options.types.every((type) => commands.resetAttributes(type, "style"));
1105
- },
1106
- setStyle: (style) => ({ commands }) => {
1107
- return this.options.types.every((type) => commands.updateAttributes(type, { style }));
1375
+ ...this.parent?.(),
1376
+ unsetLink: () => ({ state, chain }) => {
1377
+ const { from } = state.selection;
1378
+ const linkStyle = state.doc.resolve(from).marks().find((m) => m.type.name === "link")?.attrs?.style ?? null;
1379
+ const preservedStyle = processStylesForUnlink(linkStyle);
1380
+ const shouldRemoveUnderline = preservedStyle !== linkStyle;
1381
+ if (preservedStyle) {
1382
+ const cmd = chain().extendMarkRange("link").unsetMark("link").setMark("preservedStyle", { style: preservedStyle });
1383
+ return shouldRemoveUnderline ? cmd.unsetMark("underline").run() : cmd.run();
1384
+ }
1385
+ return chain().extendMarkRange("link").unsetMark("link").unsetMark("underline").run();
1108
1386
  }
1109
1387
  };
1110
1388
  },
1111
1389
  addKeyboardShortcuts() {
1112
- return { Enter: ({ editor }) => {
1113
- const { state } = editor.view;
1114
- const { selection } = state;
1115
- const { $from } = selection;
1116
- const textBefore = $from.nodeBefore?.text || "";
1117
- if (textBefore.includes("{{") || textBefore.includes("{{{")) return false;
1118
- requestAnimationFrame(() => {
1119
- editor.commands.resetAttributes("paragraph", "style");
1120
- });
1121
- return false;
1390
+ return { "Mod-k": () => {
1391
+ editorEventBus.dispatch("bubble-menu:add-link", void 0);
1392
+ return this.editor.chain().focus().toggleLink({ href: "" }).run();
1122
1393
  } };
1123
1394
  }
1124
1395
  });
1125
1396
 
1126
1397
  //#endregion
1127
- //#region src/extensions/sup.ts
1128
- /**
1129
- * This extension allows you to mark text as superscript.
1130
- * @see https://tiptap.dev/api/marks/superscript
1131
- */
1132
- const Sup = Mark.create({
1133
- name: "sup",
1134
- addOptions() {
1135
- return { HTMLAttributes: {} };
1136
- },
1137
- parseHTML() {
1138
- return [{ tag: "sup" }];
1139
- },
1140
- renderHTML({ HTMLAttributes }) {
1141
- return [
1142
- "sup",
1143
- mergeAttributes(this.options.HTMLAttributes, HTMLAttributes),
1144
- 0
1145
- ];
1398
+ //#region src/extensions/list-item.tsx
1399
+ const ListItem = EmailNode.from(ListItemBase, ({ children, node, style }) => /* @__PURE__ */ jsx("li", {
1400
+ className: node.attrs?.class || void 0,
1401
+ style: {
1402
+ ...style,
1403
+ ...inlineCssToJs(node.attrs?.style),
1404
+ ...getTextAlignment(node.attrs?.align || node.attrs?.alignment)
1146
1405
  },
1147
- addCommands() {
1406
+ children
1407
+ }));
1408
+
1409
+ //#endregion
1410
+ //#region src/extensions/max-nesting.ts
1411
+ const MaxNesting = Extension.create({
1412
+ name: "maxNesting",
1413
+ addOptions() {
1148
1414
  return {
1149
- setSup: () => ({ commands }) => {
1150
- return commands.setMark(this.name);
1151
- },
1152
- toggleSup: () => ({ commands }) => {
1415
+ maxDepth: 3,
1416
+ nodeTypes: void 0
1417
+ };
1418
+ },
1419
+ addProseMirrorPlugins() {
1420
+ const { maxDepth, nodeTypes } = this.options;
1421
+ if (typeof maxDepth !== "number" || maxDepth < 1) throw new Error("maxDepth must be a positive number");
1422
+ return [new Plugin({
1423
+ key: new PluginKey("maxNesting"),
1424
+ appendTransaction(transactions, _oldState, newState) {
1425
+ if (!transactions.some((tr$1) => tr$1.docChanged)) return null;
1426
+ const rangesToLift = [];
1427
+ newState.doc.descendants((node, pos) => {
1428
+ let depth = 0;
1429
+ let currentPos = pos;
1430
+ let currentNode = node;
1431
+ while (currentNode && depth <= maxDepth) {
1432
+ if (!nodeTypes || nodeTypes.includes(currentNode.type.name)) depth++;
1433
+ const $pos = newState.doc.resolve(currentPos);
1434
+ if ($pos.depth === 0) break;
1435
+ currentPos = $pos.before($pos.depth);
1436
+ currentNode = newState.doc.nodeAt(currentPos);
1437
+ }
1438
+ if (depth > maxDepth) {
1439
+ const $pos = newState.doc.resolve(pos);
1440
+ if ($pos.depth > 0) {
1441
+ const range = $pos.blockRange();
1442
+ if (range && "canReplace" in newState.schema.nodes.doc && typeof newState.schema.nodes.doc.canReplace === "function" && newState.schema.nodes.doc.canReplace(range.start - 1, range.end + 1, newState.doc.slice(range.start, range.end).content)) rangesToLift.push({
1443
+ range,
1444
+ target: range.start - 1
1445
+ });
1446
+ }
1447
+ }
1448
+ });
1449
+ if (rangesToLift.length === 0) return null;
1450
+ const tr = newState.tr;
1451
+ for (let i = rangesToLift.length - 1; i >= 0; i--) {
1452
+ const { range, target } = rangesToLift[i];
1453
+ tr.lift(range, target);
1454
+ }
1455
+ return tr;
1456
+ },
1457
+ filterTransaction(tr) {
1458
+ if (!tr.docChanged) return true;
1459
+ let wouldCreateDeepNesting = false;
1460
+ const newDoc = tr.doc;
1461
+ newDoc.descendants((node, pos) => {
1462
+ if (wouldCreateDeepNesting) return false;
1463
+ let depth = 0;
1464
+ let currentPos = pos;
1465
+ let currentNode = node;
1466
+ while (currentNode && depth <= maxDepth) {
1467
+ if (!nodeTypes || nodeTypes.includes(currentNode.type.name)) depth++;
1468
+ const $pos = newDoc.resolve(currentPos);
1469
+ if ($pos.depth === 0) break;
1470
+ currentPos = $pos.before($pos.depth);
1471
+ currentNode = newDoc.nodeAt(currentPos);
1472
+ }
1473
+ if (depth > maxDepth) {
1474
+ wouldCreateDeepNesting = true;
1475
+ return false;
1476
+ }
1477
+ });
1478
+ return !wouldCreateDeepNesting;
1479
+ }
1480
+ })];
1481
+ }
1482
+ });
1483
+
1484
+ //#endregion
1485
+ //#region src/extensions/ordered-list.tsx
1486
+ const OrderedList = EmailNode.from(OrderedListBase, ({ children, node, style }) => /* @__PURE__ */ jsx("ol", {
1487
+ className: node.attrs?.class || void 0,
1488
+ start: node.attrs?.start,
1489
+ style: {
1490
+ ...style,
1491
+ ...inlineCssToJs(node.attrs?.style)
1492
+ },
1493
+ children
1494
+ }));
1495
+
1496
+ //#endregion
1497
+ //#region src/extensions/paragraph.tsx
1498
+ const Paragraph = EmailNode.from(ParagraphBase, ({ children, node, style }) => {
1499
+ const isEmpty = !node.content || node.content.length === 0;
1500
+ return /* @__PURE__ */ jsx("p", {
1501
+ className: node.attrs?.class || void 0,
1502
+ style: {
1503
+ ...style,
1504
+ ...inlineCssToJs(node.attrs?.style),
1505
+ ...getTextAlignment(node.attrs?.align || node.attrs?.alignment)
1506
+ },
1507
+ children: isEmpty ? /* @__PURE__ */ jsx("br", {}) : children
1508
+ });
1509
+ });
1510
+
1511
+ //#endregion
1512
+ //#region src/extensions/placeholder.ts
1513
+ const Placeholder = TipTapPlaceholder.configure({
1514
+ placeholder: ({ node }) => {
1515
+ if (node.type.name === "heading") return `Heading ${node.attrs.level}`;
1516
+ return "Press '/' for commands";
1517
+ },
1518
+ includeChildren: true
1519
+ });
1520
+
1521
+ //#endregion
1522
+ //#region src/extensions/preview-text.ts
1523
+ const PreviewText = Node$1.create({
1524
+ name: "previewText",
1525
+ group: "block",
1526
+ selectable: false,
1527
+ draggable: false,
1528
+ atom: true,
1529
+ addOptions() {
1530
+ return { HTMLAttributes: {} };
1531
+ },
1532
+ addStorage() {
1533
+ return { previewText: null };
1534
+ },
1535
+ renderHTML() {
1536
+ return ["div", { style: "display: none" }];
1537
+ },
1538
+ parseHTML() {
1539
+ return [{
1540
+ tag: "div[data-skip-in-text=\"true\"]",
1541
+ getAttrs: (node) => {
1542
+ if (typeof node === "string") return false;
1543
+ const element = node;
1544
+ let directText = "";
1545
+ for (const child of element.childNodes) if (child.nodeType === 3) directText += child.textContent || "";
1546
+ const cleanText = directText.trim();
1547
+ if (cleanText) this.storage.previewText = cleanText;
1548
+ return false;
1549
+ }
1550
+ }, {
1551
+ tag: "span.preheader",
1552
+ getAttrs: (node) => {
1553
+ if (typeof node === "string") return false;
1554
+ const preheaderText = node.textContent?.trim();
1555
+ if (preheaderText) this.storage.previewText = preheaderText;
1556
+ return false;
1557
+ }
1558
+ }];
1559
+ }
1560
+ });
1561
+
1562
+ //#endregion
1563
+ //#region src/extensions/section.tsx
1564
+ const Section = EmailNode.create({
1565
+ name: "section",
1566
+ group: "block",
1567
+ content: "block+",
1568
+ isolating: true,
1569
+ defining: true,
1570
+ parseHTML() {
1571
+ return [{ tag: "section[data-type=\"section\"]" }];
1572
+ },
1573
+ renderHTML({ HTMLAttributes }) {
1574
+ return [
1575
+ "section",
1576
+ mergeAttributes({
1577
+ "data-type": "section",
1578
+ class: "node-section"
1579
+ }, HTMLAttributes),
1580
+ 0
1581
+ ];
1582
+ },
1583
+ addCommands() {
1584
+ return { insertSection: () => ({ commands }) => {
1585
+ return commands.insertContent({
1586
+ type: this.name,
1587
+ content: [{
1588
+ type: "paragraph",
1589
+ content: []
1590
+ }]
1591
+ });
1592
+ } };
1593
+ },
1594
+ renderToReactEmail({ children, node, style }) {
1595
+ const inlineStyles = inlineCssToJs(node.attrs?.style);
1596
+ const textAlign = node.attrs?.align || node.attrs?.alignment;
1597
+ return /* @__PURE__ */ jsx(Section$1, {
1598
+ className: node.attrs?.class || void 0,
1599
+ align: textAlign,
1600
+ style: {
1601
+ ...style,
1602
+ ...inlineStyles,
1603
+ ...getTextAlignment(textAlign)
1604
+ },
1605
+ children
1606
+ });
1607
+ }
1608
+ });
1609
+
1610
+ //#endregion
1611
+ //#region src/extensions/strike.tsx
1612
+ const Strike = EmailMark.from(StrikeBase, ({ children, style }) => /* @__PURE__ */ jsx("s", {
1613
+ style,
1614
+ children
1615
+ }));
1616
+
1617
+ //#endregion
1618
+ //#region src/extensions/style-attribute.tsx
1619
+ const StyleAttribute = Extension.create({
1620
+ name: "styleAttribute",
1621
+ priority: 101,
1622
+ addOptions() {
1623
+ return {
1624
+ types: [],
1625
+ style: []
1626
+ };
1627
+ },
1628
+ addGlobalAttributes() {
1629
+ return [{
1630
+ types: this.options.types,
1631
+ attributes: { style: {
1632
+ default: "",
1633
+ parseHTML: (element) => element.getAttribute("style") || "",
1634
+ renderHTML: (attributes) => {
1635
+ return { style: attributes.style ?? "" };
1636
+ }
1637
+ } }
1638
+ }];
1639
+ },
1640
+ addCommands() {
1641
+ return {
1642
+ unsetStyle: () => ({ commands }) => {
1643
+ return this.options.types.every((type) => commands.resetAttributes(type, "style"));
1644
+ },
1645
+ setStyle: (style) => ({ commands }) => {
1646
+ return this.options.types.every((type) => commands.updateAttributes(type, { style }));
1647
+ }
1648
+ };
1649
+ },
1650
+ addKeyboardShortcuts() {
1651
+ return { Enter: ({ editor }) => {
1652
+ const { state } = editor.view;
1653
+ const { selection } = state;
1654
+ const { $from } = selection;
1655
+ const textBefore = $from.nodeBefore?.text || "";
1656
+ if (textBefore.includes("{{") || textBefore.includes("{{{")) return false;
1657
+ requestAnimationFrame(() => {
1658
+ editor.commands.resetAttributes("paragraph", "style");
1659
+ });
1660
+ return false;
1661
+ } };
1662
+ }
1663
+ });
1664
+
1665
+ //#endregion
1666
+ //#region src/extensions/sup.tsx
1667
+ const SupBase = SuperscriptBase.extend({
1668
+ name: "sup",
1669
+ addCommands() {
1670
+ return {
1671
+ ...this.parent?.(),
1672
+ setSup: () => ({ commands }) => {
1673
+ return commands.setMark(this.name);
1674
+ },
1675
+ toggleSup: () => ({ commands }) => {
1153
1676
  return commands.toggleMark(this.name);
1154
1677
  },
1155
1678
  unsetSup: () => ({ commands }) => {
@@ -1158,6 +1681,10 @@ const Sup = Mark.create({
1158
1681
  };
1159
1682
  }
1160
1683
  });
1684
+ const Sup = EmailMark.from(SupBase, ({ children, style }) => /* @__PURE__ */ jsx("sup", {
1685
+ style,
1686
+ children
1687
+ }));
1161
1688
 
1162
1689
  //#endregion
1163
1690
  //#region src/extensions/table.tsx
@@ -1199,7 +1726,7 @@ const Table = EmailNode.create({
1199
1726
  ]
1200
1727
  ];
1201
1728
  },
1202
- renderToReactEmail({ children, node, styles }) {
1729
+ renderToReactEmail({ children, node, style }) {
1203
1730
  const inlineStyles = inlineCssToJs(node.attrs?.style);
1204
1731
  const alignment = node.attrs?.align || node.attrs?.alignment;
1205
1732
  const width = node.attrs?.width;
@@ -1210,7 +1737,7 @@ const Table = EmailNode.create({
1210
1737
  return /* @__PURE__ */ jsx(Section$1, {
1211
1738
  className: node.attrs?.class || void 0,
1212
1739
  align: alignment,
1213
- style: resolveConflictingStyles(styles.reset, {
1740
+ style: resolveConflictingStyles(style, {
1214
1741
  ...inlineStyles,
1215
1742
  ...centeringStyles
1216
1743
  }),
@@ -1251,12 +1778,12 @@ const TableRow = EmailNode.create({
1251
1778
  0
1252
1779
  ];
1253
1780
  },
1254
- renderToReactEmail({ children, node, styles }) {
1781
+ renderToReactEmail({ children, node, style }) {
1255
1782
  const inlineStyles = inlineCssToJs(node.attrs?.style);
1256
1783
  return /* @__PURE__ */ jsx("tr", {
1257
1784
  className: node.attrs?.class || void 0,
1258
1785
  style: {
1259
- ...styles.reset,
1786
+ ...style,
1260
1787
  ...inlineStyles
1261
1788
  },
1262
1789
  children
@@ -1296,20 +1823,20 @@ const TableCell = EmailNode.create({
1296
1823
  0
1297
1824
  ];
1298
1825
  },
1299
- renderToReactEmail({ children, node, styles }) {
1826
+ renderToReactEmail({ children, node, style }) {
1300
1827
  const inlineStyles = inlineCssToJs(node.attrs?.style);
1301
1828
  return /* @__PURE__ */ jsx(Column, {
1302
1829
  className: node.attrs?.class || void 0,
1303
1830
  align: node.attrs?.align || node.attrs?.alignment,
1304
1831
  style: {
1305
- ...styles.reset,
1832
+ ...style,
1306
1833
  ...inlineStyles
1307
1834
  },
1308
1835
  children
1309
1836
  });
1310
1837
  }
1311
1838
  });
1312
- const TableHeader = Node.create({
1839
+ const TableHeader = Node$1.create({
1313
1840
  name: "tableHeader",
1314
1841
  group: "tableCell",
1315
1842
  content: "block+",
@@ -1346,5 +1873,2175 @@ const TableHeader = Node.create({
1346
1873
  });
1347
1874
 
1348
1875
  //#endregion
1349
- export { AlignmentAttribute, Body, Bold, Button, ClassAttribute, CodeBlockPrism, Div, EmailNode, MaxNesting, PreservedStyle, Section, StyleAttribute, Sup, Table, TableCell, TableHeader, TableRow, createPlaceholderExtension, placeholder, processStylesForUnlink };
1876
+ //#region src/extensions/underline.tsx
1877
+ const Underline = EmailMark.from(UnderlineBase, ({ children, style }) => /* @__PURE__ */ jsx("u", {
1878
+ style,
1879
+ children
1880
+ }));
1881
+
1882
+ //#endregion
1883
+ //#region src/extensions/uppercase.tsx
1884
+ const Uppercase = EmailMark.create({
1885
+ name: "uppercase",
1886
+ addOptions() {
1887
+ return { HTMLAttributes: {} };
1888
+ },
1889
+ parseHTML() {
1890
+ return [{
1891
+ tag: "span",
1892
+ getAttrs: (node) => {
1893
+ if (node.style.textTransform === "uppercase") return {};
1894
+ return false;
1895
+ }
1896
+ }];
1897
+ },
1898
+ renderHTML({ HTMLAttributes }) {
1899
+ return [
1900
+ "span",
1901
+ mergeAttributes(this.options.HTMLAttributes, HTMLAttributes, { style: "text-transform: uppercase" }),
1902
+ 0
1903
+ ];
1904
+ },
1905
+ renderToReactEmail({ children, style }) {
1906
+ return /* @__PURE__ */ jsx("span", {
1907
+ style: {
1908
+ ...style,
1909
+ textTransform: "uppercase"
1910
+ },
1911
+ children
1912
+ });
1913
+ },
1914
+ addCommands() {
1915
+ return {
1916
+ setUppercase: () => ({ commands }) => {
1917
+ return commands.setMark(this.name);
1918
+ },
1919
+ toggleUppercase: () => ({ commands }) => {
1920
+ return commands.toggleMark(this.name);
1921
+ },
1922
+ unsetUppercase: () => ({ commands }) => {
1923
+ return commands.unsetMark(this.name);
1924
+ }
1925
+ };
1926
+ }
1927
+ });
1928
+
1929
+ //#endregion
1930
+ //#region src/extensions/columns.tsx
1931
+ const COLUMN_PARENT_TYPES = [
1932
+ "twoColumns",
1933
+ "threeColumns",
1934
+ "fourColumns"
1935
+ ];
1936
+ const COLUMN_PARENT_SET = new Set(COLUMN_PARENT_TYPES);
1937
+ const MAX_COLUMNS_DEPTH = 3;
1938
+ function getColumnsDepth(doc, from) {
1939
+ const $from = doc.resolve(from);
1940
+ let depth = 0;
1941
+ for (let d = $from.depth; d > 0; d--) if (COLUMN_PARENT_SET.has($from.node(d).type.name)) depth++;
1942
+ return depth;
1943
+ }
1944
+ const VARIANTS = [
1945
+ {
1946
+ name: "twoColumns",
1947
+ columnCount: 2,
1948
+ content: "columnsColumn columnsColumn",
1949
+ dataType: "two-columns"
1950
+ },
1951
+ {
1952
+ name: "threeColumns",
1953
+ columnCount: 3,
1954
+ content: "columnsColumn columnsColumn columnsColumn",
1955
+ dataType: "three-columns"
1956
+ },
1957
+ {
1958
+ name: "fourColumns",
1959
+ columnCount: 4,
1960
+ content: "columnsColumn{4}",
1961
+ dataType: "four-columns"
1962
+ }
1963
+ ];
1964
+ const NODE_TYPE_MAP = {
1965
+ 2: "twoColumns",
1966
+ 3: "threeColumns",
1967
+ 4: "fourColumns"
1968
+ };
1969
+ function createColumnsNode(config, includeCommands) {
1970
+ return EmailNode.create({
1971
+ name: config.name,
1972
+ group: "block",
1973
+ content: config.content,
1974
+ isolating: true,
1975
+ defining: true,
1976
+ addAttributes() {
1977
+ return createStandardAttributes([...LAYOUT_ATTRIBUTES, ...COMMON_HTML_ATTRIBUTES]);
1978
+ },
1979
+ parseHTML() {
1980
+ return [{ tag: `div[data-type="${config.dataType}"]` }];
1981
+ },
1982
+ renderHTML({ HTMLAttributes }) {
1983
+ return [
1984
+ "div",
1985
+ mergeAttributes({
1986
+ "data-type": config.dataType,
1987
+ class: "node-columns"
1988
+ }, HTMLAttributes),
1989
+ 0
1990
+ ];
1991
+ },
1992
+ ...includeCommands && { addCommands() {
1993
+ return { insertColumns: (count) => ({ commands, state }) => {
1994
+ if (getColumnsDepth(state.doc, state.selection.from) >= MAX_COLUMNS_DEPTH) return false;
1995
+ const nodeType = NODE_TYPE_MAP[count];
1996
+ const children = Array.from({ length: count }, () => ({
1997
+ type: "columnsColumn",
1998
+ content: [{
1999
+ type: "paragraph",
2000
+ content: []
2001
+ }]
2002
+ }));
2003
+ return commands.insertContent({
2004
+ type: nodeType,
2005
+ content: children
2006
+ });
2007
+ } };
2008
+ } },
2009
+ renderToReactEmail({ children, node, style }) {
2010
+ const inlineStyles = inlineCssToJs(node.attrs?.style);
2011
+ return /* @__PURE__ */ jsx(Row, {
2012
+ className: node.attrs?.class || void 0,
2013
+ style: {
2014
+ ...style,
2015
+ ...inlineStyles
2016
+ },
2017
+ children
2018
+ });
2019
+ }
2020
+ });
2021
+ }
2022
+ const TwoColumns = createColumnsNode(VARIANTS[0], true);
2023
+ const ThreeColumns = createColumnsNode(VARIANTS[1], false);
2024
+ const FourColumns = createColumnsNode(VARIANTS[2], false);
2025
+ const ColumnsColumn = EmailNode.create({
2026
+ name: "columnsColumn",
2027
+ group: "columnsColumn",
2028
+ content: "block+",
2029
+ isolating: true,
2030
+ addAttributes() {
2031
+ return { ...createStandardAttributes([...LAYOUT_ATTRIBUTES, ...COMMON_HTML_ATTRIBUTES]) };
2032
+ },
2033
+ parseHTML() {
2034
+ return [{ tag: "div[data-type=\"column\"]" }];
2035
+ },
2036
+ renderHTML({ HTMLAttributes }) {
2037
+ return [
2038
+ "div",
2039
+ mergeAttributes({
2040
+ "data-type": "column",
2041
+ class: "node-column"
2042
+ }, HTMLAttributes),
2043
+ 0
2044
+ ];
2045
+ },
2046
+ addKeyboardShortcuts() {
2047
+ return {
2048
+ Backspace: ({ editor }) => {
2049
+ const { state } = editor;
2050
+ const { selection } = state;
2051
+ const { empty, $from } = selection;
2052
+ if (!empty) return false;
2053
+ for (let depth = $from.depth; depth >= 1; depth--) {
2054
+ if ($from.pos !== $from.start(depth)) break;
2055
+ const indexInParent = $from.index(depth - 1);
2056
+ if (indexInParent === 0) continue;
2057
+ const prevNode = $from.node(depth - 1).child(indexInParent - 1);
2058
+ if (COLUMN_PARENT_SET.has(prevNode.type.name)) {
2059
+ const deleteFrom = $from.before(depth) - prevNode.nodeSize;
2060
+ const deleteTo = $from.before(depth);
2061
+ editor.view.dispatch(state.tr.delete(deleteFrom, deleteTo));
2062
+ return true;
2063
+ }
2064
+ break;
2065
+ }
2066
+ return false;
2067
+ },
2068
+ "Mod-a": ({ editor }) => {
2069
+ const { state } = editor;
2070
+ const { $from } = state.selection;
2071
+ for (let d = $from.depth; d > 0; d--) {
2072
+ if ($from.node(d).type.name !== "columnsColumn") continue;
2073
+ const columnStart = $from.start(d);
2074
+ const columnEnd = $from.end(d);
2075
+ const { from, to } = state.selection;
2076
+ if (from === columnStart && to === columnEnd) return false;
2077
+ editor.view.dispatch(state.tr.setSelection(TextSelection.create(state.doc, columnStart, columnEnd)));
2078
+ return true;
2079
+ }
2080
+ return false;
2081
+ }
2082
+ };
2083
+ },
2084
+ renderToReactEmail({ children, node, style }) {
2085
+ const inlineStyles = inlineCssToJs(node.attrs?.style);
2086
+ const width = node.attrs?.width;
2087
+ return /* @__PURE__ */ jsx(Column, {
2088
+ className: node.attrs?.class || void 0,
2089
+ style: {
2090
+ ...style,
2091
+ ...inlineStyles,
2092
+ ...width ? { width } : {}
2093
+ },
2094
+ children
2095
+ });
2096
+ }
2097
+ });
2098
+
2099
+ //#endregion
2100
+ //#region src/extensions/index.ts
2101
+ const starterKitExtensions = {
2102
+ CodeBlockPrism,
2103
+ Code,
2104
+ Paragraph,
2105
+ BulletList,
2106
+ OrderedList,
2107
+ Blockquote,
2108
+ ListItem,
2109
+ HardBreak,
2110
+ Italic,
2111
+ Placeholder,
2112
+ PreviewText,
2113
+ Bold,
2114
+ Strike,
2115
+ Heading,
2116
+ Divider,
2117
+ Link,
2118
+ Sup,
2119
+ Underline,
2120
+ Uppercase,
2121
+ PreservedStyle,
2122
+ Table,
2123
+ TableRow,
2124
+ TableCell,
2125
+ TableHeader,
2126
+ Body,
2127
+ Div,
2128
+ Button,
2129
+ Section,
2130
+ GlobalContent,
2131
+ AlignmentAttribute,
2132
+ StyleAttribute,
2133
+ ClassAttribute,
2134
+ MaxNesting
2135
+ };
2136
+ const StarterKit = Extension.create({
2137
+ name: "reactEmailStarterKit",
2138
+ addOptions() {
2139
+ return {
2140
+ TiptapStarterKit: {},
2141
+ CodeBlockPrism: {
2142
+ defaultLanguage: "javascript",
2143
+ HTMLAttributes: { class: "prism node-codeBlock" }
2144
+ },
2145
+ Code: { HTMLAttributes: {
2146
+ class: "node-inlineCode",
2147
+ spellcheck: "false"
2148
+ } },
2149
+ Paragraph: { HTMLAttributes: { class: "node-paragraph" } },
2150
+ BulletList: { HTMLAttributes: { class: "node-bulletList" } },
2151
+ OrderedList: { HTMLAttributes: { class: "node-orderedList" } },
2152
+ Blockquote: { HTMLAttributes: { class: "node-blockquote" } },
2153
+ ListItem: {},
2154
+ HardBreak: {},
2155
+ Italic: {},
2156
+ Placeholder: {},
2157
+ PreviewText: {},
2158
+ Bold: {},
2159
+ Strike: {},
2160
+ Heading: {},
2161
+ Divider: {},
2162
+ Link: {},
2163
+ Sup: {},
2164
+ Underline: {},
2165
+ Uppercase: {},
2166
+ PreservedStyle: {},
2167
+ Table: {},
2168
+ TableRow: {},
2169
+ TableCell: {},
2170
+ TableHeader: {},
2171
+ Body: {},
2172
+ Div: {},
2173
+ Button: {},
2174
+ Section: {},
2175
+ GlobalContent: {},
2176
+ AlignmentAttribute: { types: [
2177
+ "heading",
2178
+ "paragraph",
2179
+ "image",
2180
+ "blockquote",
2181
+ "codeBlock",
2182
+ "bulletList",
2183
+ "orderedList",
2184
+ "listItem",
2185
+ "button",
2186
+ "youtube",
2187
+ "twitter",
2188
+ "table",
2189
+ "tableRow",
2190
+ "tableCell",
2191
+ "tableHeader",
2192
+ "columnsColumn"
2193
+ ] },
2194
+ StyleAttribute: { types: [
2195
+ "heading",
2196
+ "paragraph",
2197
+ "image",
2198
+ "blockquote",
2199
+ "codeBlock",
2200
+ "bulletList",
2201
+ "orderedList",
2202
+ "listItem",
2203
+ "button",
2204
+ "youtube",
2205
+ "twitter",
2206
+ "horizontalRule",
2207
+ "footer",
2208
+ "section",
2209
+ "div",
2210
+ "body",
2211
+ "table",
2212
+ "tableRow",
2213
+ "tableCell",
2214
+ "tableHeader",
2215
+ "columnsColumn",
2216
+ "link"
2217
+ ] },
2218
+ ClassAttribute: { types: [
2219
+ "heading",
2220
+ "paragraph",
2221
+ "image",
2222
+ "blockquote",
2223
+ "bulletList",
2224
+ "orderedList",
2225
+ "listItem",
2226
+ "button",
2227
+ "youtube",
2228
+ "twitter",
2229
+ "horizontalRule",
2230
+ "footer",
2231
+ "section",
2232
+ "div",
2233
+ "body",
2234
+ "table",
2235
+ "tableRow",
2236
+ "tableCell",
2237
+ "tableHeader",
2238
+ "columnsColumn",
2239
+ "link"
2240
+ ] },
2241
+ MaxNesting: {
2242
+ maxDepth: 50,
2243
+ nodeTypes: [
2244
+ "section",
2245
+ "bulletList",
2246
+ "orderedList"
2247
+ ]
2248
+ }
2249
+ };
2250
+ },
2251
+ addExtensions() {
2252
+ const extensions = [];
2253
+ if (this.options.TiptapStarterKit !== false) extensions.push(TipTapStarterKit.configure({
2254
+ undoRedo: false,
2255
+ heading: false,
2256
+ link: false,
2257
+ underline: false,
2258
+ trailingNode: false,
2259
+ bold: false,
2260
+ italic: false,
2261
+ strike: false,
2262
+ code: false,
2263
+ paragraph: false,
2264
+ bulletList: false,
2265
+ orderedList: false,
2266
+ listItem: false,
2267
+ blockquote: false,
2268
+ hardBreak: false,
2269
+ gapcursor: false,
2270
+ codeBlock: false,
2271
+ horizontalRule: false,
2272
+ dropcursor: {
2273
+ color: "#61a8f8",
2274
+ class: "rounded-full animate-[fade-in_300ms_ease-in-out] !z-40",
2275
+ width: 4
2276
+ },
2277
+ ...this.options.TiptapStarterKit
2278
+ }));
2279
+ for (const [name, extension] of Object.entries(starterKitExtensions)) {
2280
+ const key = name;
2281
+ const extensionOptions = this.options[key];
2282
+ if (extensionOptions !== false) extensions.push(extension.configure(extensionOptions));
2283
+ }
2284
+ return extensions;
2285
+ }
2286
+ });
2287
+
2288
+ //#endregion
2289
+ //#region src/core/create-drop-handler.ts
2290
+ function createDropHandler({ onPaste, onUploadImage }) {
2291
+ return (view, event, _slice, moved) => {
2292
+ if (!moved && event.dataTransfer && event.dataTransfer.files && event.dataTransfer.files[0]) {
2293
+ event.preventDefault();
2294
+ const file = event.dataTransfer.files[0];
2295
+ if (onPaste?.(file, view)) return true;
2296
+ if (file.type.includes("image/") && onUploadImage) {
2297
+ onUploadImage(file, view, (view.posAtCoords({
2298
+ left: event.clientX,
2299
+ top: event.clientY
2300
+ })?.pos || 0) - 1);
2301
+ return true;
2302
+ }
2303
+ }
2304
+ return false;
2305
+ };
2306
+ }
2307
+
2308
+ //#endregion
2309
+ //#region src/utils/paste-sanitizer.ts
2310
+ /**
2311
+ * Sanitizes pasted HTML.
2312
+ * - From editor (has node-* classes): pass through as-is
2313
+ * - From external: strip all styles/classes, keep only semantic HTML
2314
+ */
2315
+ /**
2316
+ * Detects content from the Resend editor by checking for node-* class names.
2317
+ */
2318
+ const EDITOR_CLASS_PATTERN = /class="[^"]*node-/;
2319
+ /**
2320
+ * Attributes to preserve on specific elements for EXTERNAL content.
2321
+ * Only functional attributes - NO style or class.
2322
+ */
2323
+ const PRESERVED_ATTRIBUTES = {
2324
+ a: [
2325
+ "href",
2326
+ "target",
2327
+ "rel"
2328
+ ],
2329
+ img: [
2330
+ "src",
2331
+ "alt",
2332
+ "width",
2333
+ "height"
2334
+ ],
2335
+ td: ["colspan", "rowspan"],
2336
+ th: [
2337
+ "colspan",
2338
+ "rowspan",
2339
+ "scope"
2340
+ ],
2341
+ table: [
2342
+ "border",
2343
+ "cellpadding",
2344
+ "cellspacing"
2345
+ ],
2346
+ "*": ["id"]
2347
+ };
2348
+ function isFromEditor(html) {
2349
+ return EDITOR_CLASS_PATTERN.test(html);
2350
+ }
2351
+ function sanitizePastedHtml(html) {
2352
+ if (isFromEditor(html)) return html;
2353
+ const doc = new DOMParser().parseFromString(html, "text/html");
2354
+ sanitizeNode(doc.body);
2355
+ return doc.body.innerHTML;
2356
+ }
2357
+ function sanitizeNode(node) {
2358
+ if (node.nodeType === Node.ELEMENT_NODE) sanitizeElement(node);
2359
+ for (const child of Array.from(node.childNodes)) sanitizeNode(child);
2360
+ }
2361
+ function sanitizeElement(el) {
2362
+ const allowedForTag = PRESERVED_ATTRIBUTES[el.tagName.toLowerCase()] || [];
2363
+ const allowedGlobal = PRESERVED_ATTRIBUTES["*"] || [];
2364
+ const allowed = new Set([...allowedForTag, ...allowedGlobal]);
2365
+ const attributesToRemove = [];
2366
+ for (const attr of Array.from(el.attributes)) {
2367
+ if (attr.name.startsWith("data-")) {
2368
+ attributesToRemove.push(attr.name);
2369
+ continue;
2370
+ }
2371
+ if (!allowed.has(attr.name)) attributesToRemove.push(attr.name);
2372
+ }
2373
+ for (const attr of attributesToRemove) el.removeAttribute(attr);
2374
+ }
2375
+
2376
+ //#endregion
2377
+ //#region src/core/create-paste-handler.ts
2378
+ function createPasteHandler({ onPaste, onUploadImage, extensions }) {
2379
+ return (view, event, slice) => {
2380
+ const text = event.clipboardData?.getData("text/plain");
2381
+ if (text && onPaste?.(text, view)) {
2382
+ event.preventDefault();
2383
+ return true;
2384
+ }
2385
+ if (event.clipboardData?.files?.[0]) {
2386
+ const file = event.clipboardData.files[0];
2387
+ if (onPaste?.(file, view)) {
2388
+ event.preventDefault();
2389
+ return true;
2390
+ }
2391
+ if (file.type.includes("image/") && onUploadImage) {
2392
+ const pos = view.state.selection.from;
2393
+ onUploadImage(file, view, pos);
2394
+ return true;
2395
+ }
2396
+ }
2397
+ /**
2398
+ * If the coming content has a single child, we can assume
2399
+ * it's a plain text and doesn't need to be parsed and
2400
+ * be introduced in a new line
2401
+ */
2402
+ if (slice.content.childCount === 1) return false;
2403
+ if (event.clipboardData?.getData?.("text/html")) {
2404
+ event.preventDefault();
2405
+ const jsonContent = generateJSON(sanitizePastedHtml(event.clipboardData.getData("text/html")), extensions);
2406
+ const node = view.state.schema.nodeFromJSON(jsonContent);
2407
+ const transaction = view.state.tr.replaceSelectionWith(node, false);
2408
+ view.dispatch(transaction);
2409
+ return true;
2410
+ }
2411
+ return false;
2412
+ };
2413
+ }
2414
+
2415
+ //#endregion
2416
+ //#region src/core/use-editor.ts
2417
+ const COLLABORATION_EXTENSION_NAMES = new Set(["liveblocksExtension", "collaboration"]);
2418
+ function hasCollaborationExtension(exts) {
2419
+ return exts.some((ext) => COLLABORATION_EXTENSION_NAMES.has(ext.name));
2420
+ }
2421
+ function useEditor({ content, extensions = [], onUpdate, onPaste, onUploadImage, onReady, editable = true, ...rest }) {
2422
+ const [contentError, setContentError] = React.useState(null);
2423
+ const isCollaborative = hasCollaborationExtension(extensions);
2424
+ const effectiveExtensions = React.useMemo(() => [
2425
+ StarterKit,
2426
+ ...isCollaborative ? [] : [UndoRedo],
2427
+ ...extensions
2428
+ ], [extensions, isCollaborative]);
2429
+ const editor = useEditor$1({
2430
+ content: isCollaborative ? void 0 : content,
2431
+ extensions: effectiveExtensions,
2432
+ editable,
2433
+ immediatelyRender: false,
2434
+ enableContentCheck: true,
2435
+ onContentError({ editor: editor$1, error, disableCollaboration }) {
2436
+ disableCollaboration();
2437
+ setContentError(error);
2438
+ console.error(error);
2439
+ editor$1.setEditable(false);
2440
+ },
2441
+ onCreate({ editor: editor$1 }) {
2442
+ onReady?.(editor$1);
2443
+ },
2444
+ onUpdate({ editor: editor$1, transaction }) {
2445
+ onUpdate?.(editor$1, transaction);
2446
+ },
2447
+ editorProps: {
2448
+ handleDOMEvents: { click: (view, event) => {
2449
+ if (!view.editable) {
2450
+ if (event.target.closest("a")) {
2451
+ event.preventDefault();
2452
+ return true;
2453
+ }
2454
+ }
2455
+ return false;
2456
+ } },
2457
+ handlePaste: createPasteHandler({
2458
+ onPaste,
2459
+ onUploadImage,
2460
+ extensions: effectiveExtensions
2461
+ }),
2462
+ handleDrop: createDropHandler({
2463
+ onPaste,
2464
+ onUploadImage
2465
+ })
2466
+ },
2467
+ ...rest
2468
+ });
2469
+ return {
2470
+ editor,
2471
+ isEditorEmpty: useEditorState({
2472
+ editor,
2473
+ selector: (context) => {
2474
+ if (!context.editor) return true;
2475
+ return isDocumentVisuallyEmpty(context.editor.state.doc);
2476
+ }
2477
+ }) ?? true,
2478
+ extensions: effectiveExtensions,
2479
+ contentError,
2480
+ isCollaborative
2481
+ };
2482
+ }
2483
+
2484
+ //#endregion
2485
+ //#region src/utils/set-text-alignment.ts
2486
+ function setTextAlignment(editor, alignment) {
2487
+ const { from, to } = editor.state.selection;
2488
+ const tr = editor.state.tr;
2489
+ editor.state.doc.nodesBetween(from, to, (node, pos) => {
2490
+ if (node.isTextblock) {
2491
+ const prop = "align" in node.attrs ? "align" : "alignment";
2492
+ tr.setNodeMarkup(pos, null, {
2493
+ ...node.attrs,
2494
+ [prop]: alignment
2495
+ });
2496
+ }
2497
+ });
2498
+ editor.view.dispatch(tr);
2499
+ }
2500
+
2501
+ //#endregion
2502
+ //#region src/ui/bubble-menu/context.tsx
2503
+ const BubbleMenuContext = React.createContext(null);
2504
+ function useBubbleMenuContext() {
2505
+ const context = React.useContext(BubbleMenuContext);
2506
+ if (!context) throw new Error("BubbleMenu compound components must be used within <BubbleMenu.Root>");
2507
+ return context;
2508
+ }
2509
+
2510
+ //#endregion
2511
+ //#region src/ui/bubble-menu/item.tsx
2512
+ function BubbleMenuItem({ name, isActive, onCommand, className, children, ...rest }) {
2513
+ return /* @__PURE__ */ jsx("button", {
2514
+ type: "button",
2515
+ "aria-label": name,
2516
+ "aria-pressed": isActive,
2517
+ className,
2518
+ "data-re-bubble-menu-item": "",
2519
+ "data-item": name,
2520
+ ...isActive ? { "data-active": "" } : {},
2521
+ onMouseDown: (e) => e.preventDefault(),
2522
+ onClick: onCommand,
2523
+ ...rest,
2524
+ children
2525
+ });
2526
+ }
2527
+
2528
+ //#endregion
2529
+ //#region src/ui/bubble-menu/align-center.tsx
2530
+ function BubbleMenuAlignCenter({ className, children }) {
2531
+ const { editor } = useBubbleMenuContext();
2532
+ return /* @__PURE__ */ jsx(BubbleMenuItem, {
2533
+ name: "align-center",
2534
+ isActive: useEditorState({
2535
+ editor,
2536
+ selector: ({ editor: editor$1 }) => editor$1?.isActive({ alignment: "center" }) ?? false
2537
+ }),
2538
+ onCommand: () => setTextAlignment(editor, "center"),
2539
+ className,
2540
+ children: children ?? /* @__PURE__ */ jsx(AlignCenterIcon, {})
2541
+ });
2542
+ }
2543
+
2544
+ //#endregion
2545
+ //#region src/ui/bubble-menu/align-left.tsx
2546
+ function BubbleMenuAlignLeft({ className, children }) {
2547
+ const { editor } = useBubbleMenuContext();
2548
+ return /* @__PURE__ */ jsx(BubbleMenuItem, {
2549
+ name: "align-left",
2550
+ isActive: useEditorState({
2551
+ editor,
2552
+ selector: ({ editor: editor$1 }) => editor$1?.isActive({ alignment: "left" }) ?? false
2553
+ }),
2554
+ onCommand: () => setTextAlignment(editor, "left"),
2555
+ className,
2556
+ children: children ?? /* @__PURE__ */ jsx(AlignLeftIcon, {})
2557
+ });
2558
+ }
2559
+
2560
+ //#endregion
2561
+ //#region src/ui/bubble-menu/align-right.tsx
2562
+ function BubbleMenuAlignRight({ className, children }) {
2563
+ const { editor } = useBubbleMenuContext();
2564
+ return /* @__PURE__ */ jsx(BubbleMenuItem, {
2565
+ name: "align-right",
2566
+ isActive: useEditorState({
2567
+ editor,
2568
+ selector: ({ editor: editor$1 }) => editor$1?.isActive({ alignment: "right" }) ?? false
2569
+ }),
2570
+ onCommand: () => setTextAlignment(editor, "right"),
2571
+ className,
2572
+ children: children ?? /* @__PURE__ */ jsx(AlignRightIcon, {})
2573
+ });
2574
+ }
2575
+
2576
+ //#endregion
2577
+ //#region src/ui/bubble-menu/create-mark-bubble-item.tsx
2578
+ function createMarkBubbleItem(config) {
2579
+ function MarkBubbleItem({ className, children }) {
2580
+ const { editor } = useBubbleMenuContext();
2581
+ const isActive = useEditorState({
2582
+ editor,
2583
+ selector: ({ editor: editor$1 }) => {
2584
+ if (config.activeParams) return editor$1?.isActive(config.activeName, config.activeParams) ?? false;
2585
+ return editor$1?.isActive(config.activeName) ?? false;
2586
+ }
2587
+ });
2588
+ const handleCommand = () => {
2589
+ const chain = editor.chain().focus();
2590
+ const method = chain[config.command];
2591
+ if (method) method.call(chain).run();
2592
+ };
2593
+ return /* @__PURE__ */ jsx(BubbleMenuItem, {
2594
+ name: config.name,
2595
+ isActive,
2596
+ onCommand: handleCommand,
2597
+ className,
2598
+ children: children ?? config.icon
2599
+ });
2600
+ }
2601
+ MarkBubbleItem.displayName = `BubbleMenu${config.name.charAt(0).toUpperCase() + config.name.slice(1)}`;
2602
+ return MarkBubbleItem;
2603
+ }
2604
+
2605
+ //#endregion
2606
+ //#region src/ui/bubble-menu/bold.tsx
2607
+ const BubbleMenuBold = createMarkBubbleItem({
2608
+ name: "bold",
2609
+ activeName: "bold",
2610
+ command: "toggleBold",
2611
+ icon: /* @__PURE__ */ jsx(BoldIcon, {})
2612
+ });
2613
+
2614
+ //#endregion
2615
+ //#region src/ui/bubble-menu/code.tsx
2616
+ const BubbleMenuCode = createMarkBubbleItem({
2617
+ name: "code",
2618
+ activeName: "code",
2619
+ command: "toggleCode",
2620
+ icon: /* @__PURE__ */ jsx(CodeIcon, {})
2621
+ });
2622
+
2623
+ //#endregion
2624
+ //#region src/ui/bubble-menu/group.tsx
2625
+ function BubbleMenuItemGroup({ className, children }) {
2626
+ return /* @__PURE__ */ jsx("fieldset", {
2627
+ className,
2628
+ "data-re-bubble-menu-group": "",
2629
+ children
2630
+ });
2631
+ }
2632
+
2633
+ //#endregion
2634
+ //#region src/ui/bubble-menu/italic.tsx
2635
+ const BubbleMenuItalic = createMarkBubbleItem({
2636
+ name: "italic",
2637
+ activeName: "italic",
2638
+ command: "toggleItalic",
2639
+ icon: /* @__PURE__ */ jsx(ItalicIcon, {})
2640
+ });
2641
+
2642
+ //#endregion
2643
+ //#region src/ui/bubble-menu/utils.ts
2644
+ const SAFE_PROTOCOLS = new Set([
2645
+ "http:",
2646
+ "https:",
2647
+ "mailto:",
2648
+ "tel:"
2649
+ ]);
2650
+ /**
2651
+ * Basic URL validation and auto-prefixing.
2652
+ * Rejects dangerous schemes (javascript:, data:, vbscript:, etc.).
2653
+ * Returns the valid URL string or null.
2654
+ */
2655
+ function getUrlFromString(str) {
2656
+ if (str === "#") return str;
2657
+ try {
2658
+ const url = new URL(str);
2659
+ if (SAFE_PROTOCOLS.has(url.protocol)) return str;
2660
+ return null;
2661
+ } catch {}
2662
+ try {
2663
+ if (str.includes(".") && !str.includes(" ")) return new URL(`https://${str}`).toString();
2664
+ } catch {}
2665
+ return null;
2666
+ }
2667
+ function setLinkHref(editor, href) {
2668
+ if (href.length === 0) {
2669
+ editor.chain().unsetLink().run();
2670
+ return;
2671
+ }
2672
+ const { from, to } = editor.state.selection;
2673
+ if (from === to) {
2674
+ editor.chain().extendMarkRange("link").setLink({ href }).setTextSelection({
2675
+ from,
2676
+ to
2677
+ }).run();
2678
+ return;
2679
+ }
2680
+ editor.chain().setLink({ href }).run();
2681
+ }
2682
+ function focusEditor(editor) {
2683
+ setTimeout(() => {
2684
+ editor.commands.focus();
2685
+ }, 0);
2686
+ }
2687
+
2688
+ //#endregion
2689
+ //#region src/ui/bubble-menu/link-selector.tsx
2690
+ function BubbleMenuLinkSelector({ className, showToggle = true, validateUrl, onLinkApply, onLinkRemove, children, open: controlledOpen, onOpenChange }) {
2691
+ const { editor } = useBubbleMenuContext();
2692
+ const [uncontrolledOpen, setUncontrolledOpen] = React.useState(false);
2693
+ const isControlled = controlledOpen !== void 0;
2694
+ const isOpen = isControlled ? controlledOpen : uncontrolledOpen;
2695
+ const setIsOpen = React.useCallback((value) => {
2696
+ if (!isControlled) setUncontrolledOpen(value);
2697
+ onOpenChange?.(value);
2698
+ }, [isControlled, onOpenChange]);
2699
+ const editorState = useEditorState({
2700
+ editor,
2701
+ selector: ({ editor: editor$1 }) => ({
2702
+ isLinkActive: editor$1?.isActive("link") ?? false,
2703
+ hasLink: Boolean(editor$1?.getAttributes("link").href),
2704
+ currentHref: editor$1?.getAttributes("link").href || ""
2705
+ })
2706
+ });
2707
+ const setIsOpenRef = React.useRef(setIsOpen);
2708
+ setIsOpenRef.current = setIsOpen;
2709
+ React.useEffect(() => {
2710
+ const subscription = editorEventBus.on("bubble-menu:add-link", () => {
2711
+ setIsOpenRef.current(true);
2712
+ });
2713
+ return () => {
2714
+ setIsOpenRef.current(false);
2715
+ subscription.unsubscribe();
2716
+ };
2717
+ }, []);
2718
+ if (!editorState) return null;
2719
+ const handleOpenLink = () => {
2720
+ setIsOpen(!isOpen);
2721
+ };
2722
+ return /* @__PURE__ */ jsxs("div", {
2723
+ "data-re-link-selector": "",
2724
+ ...isOpen ? { "data-open": "" } : {},
2725
+ ...editorState.hasLink ? { "data-has-link": "" } : {},
2726
+ className,
2727
+ children: [showToggle && /* @__PURE__ */ jsx("button", {
2728
+ type: "button",
2729
+ "aria-expanded": isOpen,
2730
+ "aria-haspopup": "true",
2731
+ "aria-label": "Add link",
2732
+ "aria-pressed": editorState.isLinkActive && editorState.hasLink,
2733
+ "data-re-link-selector-trigger": "",
2734
+ onClick: handleOpenLink,
2735
+ children: /* @__PURE__ */ jsx(LinkIcon, {})
2736
+ }), isOpen && /* @__PURE__ */ jsx(LinkForm, {
2737
+ editor,
2738
+ currentHref: editorState.currentHref,
2739
+ validateUrl,
2740
+ onLinkApply,
2741
+ onLinkRemove,
2742
+ setIsOpen,
2743
+ children
2744
+ })]
2745
+ });
2746
+ }
2747
+ function LinkForm({ editor, currentHref, validateUrl, onLinkApply, onLinkRemove, setIsOpen, children }) {
2748
+ const inputRef = React.useRef(null);
2749
+ const formRef = React.useRef(null);
2750
+ const displayHref = currentHref === "#" ? "" : currentHref;
2751
+ const [inputValue, setInputValue] = React.useState(displayHref);
2752
+ React.useEffect(() => {
2753
+ const timeoutId = setTimeout(() => {
2754
+ inputRef.current?.focus();
2755
+ }, 0);
2756
+ return () => clearTimeout(timeoutId);
2757
+ }, []);
2758
+ React.useEffect(() => {
2759
+ const handleKeyDown = (event) => {
2760
+ if (event.key === "Escape") {
2761
+ if (editor.getAttributes("link").href === "#") editor.chain().unsetLink().run();
2762
+ setIsOpen(false);
2763
+ }
2764
+ };
2765
+ const handleClickOutside = (event) => {
2766
+ if (formRef.current && !formRef.current.contains(event.target)) {
2767
+ const form = formRef.current;
2768
+ const submitEvent = new Event("submit", {
2769
+ bubbles: true,
2770
+ cancelable: true
2771
+ });
2772
+ form.dispatchEvent(submitEvent);
2773
+ setIsOpen(false);
2774
+ }
2775
+ };
2776
+ document.addEventListener("mousedown", handleClickOutside);
2777
+ window.addEventListener("keydown", handleKeyDown);
2778
+ return () => {
2779
+ window.removeEventListener("keydown", handleKeyDown);
2780
+ document.removeEventListener("mousedown", handleClickOutside);
2781
+ };
2782
+ }, [editor, setIsOpen]);
2783
+ function handleSubmit(e) {
2784
+ e.preventDefault();
2785
+ const value = inputValue.trim();
2786
+ if (value === "") {
2787
+ setLinkHref(editor, "");
2788
+ setIsOpen(false);
2789
+ focusEditor(editor);
2790
+ onLinkRemove?.();
2791
+ return;
2792
+ }
2793
+ const finalValue = (validateUrl ?? getUrlFromString)(value);
2794
+ if (!finalValue) {
2795
+ setLinkHref(editor, "");
2796
+ setIsOpen(false);
2797
+ focusEditor(editor);
2798
+ onLinkRemove?.();
2799
+ return;
2800
+ }
2801
+ setLinkHref(editor, finalValue);
2802
+ setIsOpen(false);
2803
+ focusEditor(editor);
2804
+ onLinkApply?.(finalValue);
2805
+ }
2806
+ function handleUnlink(e) {
2807
+ e.stopPropagation();
2808
+ setLinkHref(editor, "");
2809
+ setIsOpen(false);
2810
+ focusEditor(editor);
2811
+ onLinkRemove?.();
2812
+ }
2813
+ return /* @__PURE__ */ jsxs("form", {
2814
+ ref: formRef,
2815
+ "data-re-link-selector-form": "",
2816
+ onMouseDown: (e) => e.stopPropagation(),
2817
+ onClick: (e) => e.stopPropagation(),
2818
+ onKeyDown: (e) => e.stopPropagation(),
2819
+ onSubmit: handleSubmit,
2820
+ children: [
2821
+ /* @__PURE__ */ jsx("input", {
2822
+ ref: inputRef,
2823
+ "data-re-link-selector-input": "",
2824
+ value: inputValue,
2825
+ onFocus: (e) => e.stopPropagation(),
2826
+ onChange: (e) => setInputValue(e.target.value),
2827
+ placeholder: "Paste a link",
2828
+ type: "text"
2829
+ }),
2830
+ children,
2831
+ displayHref ? /* @__PURE__ */ jsx("button", {
2832
+ type: "button",
2833
+ "aria-label": "Remove link",
2834
+ "data-re-link-selector-unlink": "",
2835
+ onClick: handleUnlink,
2836
+ children: /* @__PURE__ */ jsx(UnlinkIcon, {})
2837
+ }) : /* @__PURE__ */ jsx("button", {
2838
+ type: "submit",
2839
+ "aria-label": "Apply link",
2840
+ "data-re-link-selector-apply": "",
2841
+ onMouseDown: (e) => e.stopPropagation(),
2842
+ children: /* @__PURE__ */ jsx(Check, {})
2843
+ })
2844
+ ]
2845
+ });
2846
+ }
2847
+
2848
+ //#endregion
2849
+ //#region src/ui/bubble-menu/node-selector.tsx
2850
+ const NodeSelectorContext = React.createContext(null);
2851
+ function useNodeSelectorContext() {
2852
+ const context = React.useContext(NodeSelectorContext);
2853
+ if (!context) throw new Error("NodeSelector compound components must be used within <NodeSelector.Root>");
2854
+ return context;
2855
+ }
2856
+ function NodeSelectorRoot({ omit = [], open: controlledOpen, onOpenChange, className, children }) {
2857
+ const { editor } = useBubbleMenuContext();
2858
+ const [uncontrolledOpen, setUncontrolledOpen] = React.useState(false);
2859
+ const isControlled = controlledOpen !== void 0;
2860
+ const isOpen = isControlled ? controlledOpen : uncontrolledOpen;
2861
+ const setIsOpen = React.useCallback((value) => {
2862
+ if (!isControlled) setUncontrolledOpen(value);
2863
+ onOpenChange?.(value);
2864
+ }, [isControlled, onOpenChange]);
2865
+ const editorState = useEditorState({
2866
+ editor,
2867
+ selector: ({ editor: editor$1 }) => ({
2868
+ isParagraphActive: (editor$1?.isActive("paragraph") ?? false) && !editor$1?.isActive("bulletList") && !editor$1?.isActive("orderedList"),
2869
+ isHeading1Active: editor$1?.isActive("heading", { level: 1 }) ?? false,
2870
+ isHeading2Active: editor$1?.isActive("heading", { level: 2 }) ?? false,
2871
+ isHeading3Active: editor$1?.isActive("heading", { level: 3 }) ?? false,
2872
+ isBulletListActive: editor$1?.isActive("bulletList") ?? false,
2873
+ isOrderedListActive: editor$1?.isActive("orderedList") ?? false,
2874
+ isBlockquoteActive: editor$1?.isActive("blockquote") ?? false,
2875
+ isCodeBlockActive: editor$1?.isActive("codeBlock") ?? false
2876
+ })
2877
+ });
2878
+ const allItems = React.useMemo(() => [
2879
+ {
2880
+ name: "Text",
2881
+ icon: TextIcon,
2882
+ command: () => editor.chain().focus().clearNodes().toggleNode("paragraph", "paragraph").run(),
2883
+ isActive: editorState?.isParagraphActive ?? false
2884
+ },
2885
+ {
2886
+ name: "Title",
2887
+ icon: Heading1,
2888
+ command: () => editor.chain().focus().clearNodes().toggleHeading({ level: 1 }).run(),
2889
+ isActive: editorState?.isHeading1Active ?? false
2890
+ },
2891
+ {
2892
+ name: "Subtitle",
2893
+ icon: Heading2,
2894
+ command: () => editor.chain().focus().clearNodes().toggleHeading({ level: 2 }).run(),
2895
+ isActive: editorState?.isHeading2Active ?? false
2896
+ },
2897
+ {
2898
+ name: "Heading",
2899
+ icon: Heading3,
2900
+ command: () => editor.chain().focus().clearNodes().toggleHeading({ level: 3 }).run(),
2901
+ isActive: editorState?.isHeading3Active ?? false
2902
+ },
2903
+ {
2904
+ name: "Bullet List",
2905
+ icon: List,
2906
+ command: () => editor.chain().focus().clearNodes().toggleBulletList().run(),
2907
+ isActive: editorState?.isBulletListActive ?? false
2908
+ },
2909
+ {
2910
+ name: "Numbered List",
2911
+ icon: ListOrdered,
2912
+ command: () => editor.chain().focus().clearNodes().toggleOrderedList().run(),
2913
+ isActive: editorState?.isOrderedListActive ?? false
2914
+ },
2915
+ {
2916
+ name: "Quote",
2917
+ icon: TextQuote,
2918
+ command: () => editor.chain().focus().clearNodes().toggleNode("paragraph", "paragraph").toggleBlockquote().run(),
2919
+ isActive: editorState?.isBlockquoteActive ?? false
2920
+ },
2921
+ {
2922
+ name: "Code",
2923
+ icon: Code$1,
2924
+ command: () => editor.chain().focus().clearNodes().toggleCodeBlock().run(),
2925
+ isActive: editorState?.isCodeBlockActive ?? false
2926
+ }
2927
+ ], [editor, editorState]);
2928
+ const items = React.useMemo(() => allItems.filter((item) => !omit.includes(item.name)), [allItems, omit]);
2929
+ const activeItem = React.useMemo(() => items.find((item) => item.isActive) ?? { name: "Multiple" }, [items]);
2930
+ const contextValue = React.useMemo(() => ({
2931
+ items,
2932
+ activeItem,
2933
+ isOpen,
2934
+ setIsOpen
2935
+ }), [
2936
+ items,
2937
+ activeItem,
2938
+ isOpen,
2939
+ setIsOpen
2940
+ ]);
2941
+ if (!editorState || items.length === 0) return null;
2942
+ return /* @__PURE__ */ jsx(NodeSelectorContext.Provider, {
2943
+ value: contextValue,
2944
+ children: /* @__PURE__ */ jsx(Popover.Root, {
2945
+ open: isOpen,
2946
+ onOpenChange: setIsOpen,
2947
+ children: /* @__PURE__ */ jsx("div", {
2948
+ "data-re-node-selector": "",
2949
+ ...isOpen ? { "data-open": "" } : {},
2950
+ className,
2951
+ children
2952
+ })
2953
+ })
2954
+ });
2955
+ }
2956
+ function NodeSelectorTrigger({ className, children }) {
2957
+ const { activeItem, isOpen, setIsOpen } = useNodeSelectorContext();
2958
+ return /* @__PURE__ */ jsx(Popover.Trigger, {
2959
+ "data-re-node-selector-trigger": "",
2960
+ className,
2961
+ onClick: () => setIsOpen(!isOpen),
2962
+ children: children ?? /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("span", { children: activeItem.name }), /* @__PURE__ */ jsx(ChevronDown, {})] })
2963
+ });
2964
+ }
2965
+ function NodeSelectorContent({ className, align = "start", children }) {
2966
+ const { items, setIsOpen } = useNodeSelectorContext();
2967
+ return /* @__PURE__ */ jsx(Popover.Content, {
2968
+ align,
2969
+ "data-re-node-selector-content": "",
2970
+ className,
2971
+ children: children ? children(items, () => setIsOpen(false)) : items.map((item) => {
2972
+ const Icon = item.icon;
2973
+ return /* @__PURE__ */ jsxs("button", {
2974
+ type: "button",
2975
+ "data-re-node-selector-item": "",
2976
+ ...item.isActive ? { "data-active": "" } : {},
2977
+ onClick: () => {
2978
+ item.command();
2979
+ setIsOpen(false);
2980
+ },
2981
+ children: [
2982
+ /* @__PURE__ */ jsx(Icon, {}),
2983
+ /* @__PURE__ */ jsx("span", { children: item.name }),
2984
+ item.isActive && /* @__PURE__ */ jsx(Check, {})
2985
+ ]
2986
+ }, item.name);
2987
+ })
2988
+ });
2989
+ }
2990
+ function BubbleMenuNodeSelector({ omit = [], className, triggerContent, open, onOpenChange }) {
2991
+ return /* @__PURE__ */ jsxs(NodeSelectorRoot, {
2992
+ omit,
2993
+ open,
2994
+ onOpenChange,
2995
+ className,
2996
+ children: [/* @__PURE__ */ jsx(NodeSelectorTrigger, { children: triggerContent }), /* @__PURE__ */ jsx(NodeSelectorContent, {})]
2997
+ });
2998
+ }
2999
+
3000
+ //#endregion
3001
+ //#region src/ui/bubble-menu/root.tsx
3002
+ function BubbleMenuRoot({ excludeNodes = [], placement = "bottom", offset = 8, onHide, className, children }) {
3003
+ const { editor } = useCurrentEditor();
3004
+ if (!editor) return null;
3005
+ return /* @__PURE__ */ jsx(BubbleMenu$1, {
3006
+ editor,
3007
+ "data-re-bubble-menu": "",
3008
+ shouldShow: ({ editor: editor$1, view }) => {
3009
+ for (const node of excludeNodes) if (editor$1.isActive(node)) return false;
3010
+ if (view.dom.classList.contains("dragging")) return false;
3011
+ return editor$1.view.state.selection.content().size > 0;
3012
+ },
3013
+ options: {
3014
+ placement,
3015
+ offset,
3016
+ onHide
3017
+ },
3018
+ className,
3019
+ children: /* @__PURE__ */ jsx(BubbleMenuContext.Provider, {
3020
+ value: { editor },
3021
+ children
3022
+ })
3023
+ });
3024
+ }
3025
+
3026
+ //#endregion
3027
+ //#region src/ui/bubble-menu/strike.tsx
3028
+ const BubbleMenuStrike = createMarkBubbleItem({
3029
+ name: "strike",
3030
+ activeName: "strike",
3031
+ command: "toggleStrike",
3032
+ icon: /* @__PURE__ */ jsx(StrikethroughIcon, {})
3033
+ });
3034
+
3035
+ //#endregion
3036
+ //#region src/ui/bubble-menu/underline.tsx
3037
+ const BubbleMenuUnderline = createMarkBubbleItem({
3038
+ name: "underline",
3039
+ activeName: "underline",
3040
+ command: "toggleUnderline",
3041
+ icon: /* @__PURE__ */ jsx(UnderlineIcon, {})
3042
+ });
3043
+
3044
+ //#endregion
3045
+ //#region src/ui/bubble-menu/uppercase.tsx
3046
+ const BubbleMenuUppercase = createMarkBubbleItem({
3047
+ name: "uppercase",
3048
+ activeName: "uppercase",
3049
+ command: "toggleUppercase",
3050
+ icon: /* @__PURE__ */ jsx(CaseUpperIcon, {})
3051
+ });
3052
+
3053
+ //#endregion
3054
+ //#region src/ui/bubble-menu/default.tsx
3055
+ function BubbleMenuDefault({ excludeItems = [], excludeNodes, placement, offset, onHide, className }) {
3056
+ const [isNodeSelectorOpen, setIsNodeSelectorOpen] = React.useState(false);
3057
+ const [isLinkSelectorOpen, setIsLinkSelectorOpen] = React.useState(false);
3058
+ const has = (item) => !excludeItems.includes(item);
3059
+ const handleNodeSelectorOpenChange = React.useCallback((open) => {
3060
+ setIsNodeSelectorOpen(open);
3061
+ if (open) setIsLinkSelectorOpen(false);
3062
+ }, []);
3063
+ const handleLinkSelectorOpenChange = React.useCallback((open) => {
3064
+ setIsLinkSelectorOpen(open);
3065
+ if (open) setIsNodeSelectorOpen(false);
3066
+ }, []);
3067
+ const handleHide = React.useCallback(() => {
3068
+ setIsNodeSelectorOpen(false);
3069
+ setIsLinkSelectorOpen(false);
3070
+ onHide?.();
3071
+ }, [onHide]);
3072
+ const hasFormattingItems = has("bold") || has("italic") || has("underline") || has("strike") || has("code") || has("uppercase");
3073
+ const hasAlignmentItems = has("align-left") || has("align-center") || has("align-right");
3074
+ return /* @__PURE__ */ jsxs(BubbleMenuRoot, {
3075
+ excludeNodes,
3076
+ placement,
3077
+ offset,
3078
+ onHide: handleHide,
3079
+ className,
3080
+ children: [
3081
+ has("node-selector") && /* @__PURE__ */ jsx(BubbleMenuNodeSelector, {
3082
+ open: isNodeSelectorOpen,
3083
+ onOpenChange: handleNodeSelectorOpenChange
3084
+ }),
3085
+ has("link-selector") && /* @__PURE__ */ jsx(BubbleMenuLinkSelector, {
3086
+ open: isLinkSelectorOpen,
3087
+ onOpenChange: handleLinkSelectorOpenChange
3088
+ }),
3089
+ hasFormattingItems && /* @__PURE__ */ jsxs(BubbleMenuItemGroup, { children: [
3090
+ has("bold") && /* @__PURE__ */ jsx(BubbleMenuBold, {}),
3091
+ has("italic") && /* @__PURE__ */ jsx(BubbleMenuItalic, {}),
3092
+ has("underline") && /* @__PURE__ */ jsx(BubbleMenuUnderline, {}),
3093
+ has("strike") && /* @__PURE__ */ jsx(BubbleMenuStrike, {}),
3094
+ has("code") && /* @__PURE__ */ jsx(BubbleMenuCode, {}),
3095
+ has("uppercase") && /* @__PURE__ */ jsx(BubbleMenuUppercase, {})
3096
+ ] }),
3097
+ hasAlignmentItems && /* @__PURE__ */ jsxs(BubbleMenuItemGroup, { children: [
3098
+ has("align-left") && /* @__PURE__ */ jsx(BubbleMenuAlignLeft, {}),
3099
+ has("align-center") && /* @__PURE__ */ jsx(BubbleMenuAlignCenter, {}),
3100
+ has("align-right") && /* @__PURE__ */ jsx(BubbleMenuAlignRight, {})
3101
+ ] })
3102
+ ]
3103
+ });
3104
+ }
3105
+
3106
+ //#endregion
3107
+ //#region src/ui/bubble-menu/separator.tsx
3108
+ function BubbleMenuSeparator({ className }) {
3109
+ return /* @__PURE__ */ jsx("hr", {
3110
+ className,
3111
+ "data-re-bubble-menu-separator": ""
3112
+ });
3113
+ }
3114
+
3115
+ //#endregion
3116
+ //#region src/ui/bubble-menu/index.ts
3117
+ const BubbleMenu = {
3118
+ Root: BubbleMenuRoot,
3119
+ ItemGroup: BubbleMenuItemGroup,
3120
+ Separator: BubbleMenuSeparator,
3121
+ Item: BubbleMenuItem,
3122
+ Bold: BubbleMenuBold,
3123
+ Italic: BubbleMenuItalic,
3124
+ Underline: BubbleMenuUnderline,
3125
+ Strike: BubbleMenuStrike,
3126
+ Code: BubbleMenuCode,
3127
+ Uppercase: BubbleMenuUppercase,
3128
+ AlignLeft: BubbleMenuAlignLeft,
3129
+ AlignCenter: BubbleMenuAlignCenter,
3130
+ AlignRight: BubbleMenuAlignRight,
3131
+ NodeSelector: Object.assign(BubbleMenuNodeSelector, {
3132
+ Root: NodeSelectorRoot,
3133
+ Trigger: NodeSelectorTrigger,
3134
+ Content: NodeSelectorContent
3135
+ }),
3136
+ LinkSelector: BubbleMenuLinkSelector,
3137
+ Default: BubbleMenuDefault
3138
+ };
3139
+
3140
+ //#endregion
3141
+ //#region src/ui/button-bubble-menu/context.tsx
3142
+ const ButtonBubbleMenuContext = React.createContext(null);
3143
+ function useButtonBubbleMenuContext() {
3144
+ const context = React.useContext(ButtonBubbleMenuContext);
3145
+ if (!context) throw new Error("ButtonBubbleMenu compound components must be used within <ButtonBubbleMenu.Root>");
3146
+ return context;
3147
+ }
3148
+
3149
+ //#endregion
3150
+ //#region src/ui/button-bubble-menu/edit-link.tsx
3151
+ function ButtonBubbleMenuEditLink({ className, children, onClick, onMouseDown, ...rest }) {
3152
+ const { setIsEditing } = useButtonBubbleMenuContext();
3153
+ return /* @__PURE__ */ jsx("button", {
3154
+ ...rest,
3155
+ type: "button",
3156
+ "aria-label": "Edit link",
3157
+ "data-re-btn-bm-item": "",
3158
+ "data-item": "edit-link",
3159
+ className,
3160
+ onMouseDown: (e) => {
3161
+ e.preventDefault();
3162
+ onMouseDown?.(e);
3163
+ },
3164
+ onClick: (e) => {
3165
+ onClick?.(e);
3166
+ setIsEditing(true);
3167
+ },
3168
+ children: children ?? /* @__PURE__ */ jsx(LinkIcon, {})
3169
+ });
3170
+ }
3171
+
3172
+ //#endregion
3173
+ //#region src/ui/button-bubble-menu/root.tsx
3174
+ function ButtonBubbleMenuRoot({ onHide, placement = "top", offset = 8, className, children }) {
3175
+ const { editor } = useCurrentEditor();
3176
+ const [isEditing, setIsEditing] = React.useState(false);
3177
+ if (!editor) return null;
3178
+ return /* @__PURE__ */ jsx(BubbleMenu$1, {
3179
+ editor,
3180
+ "data-re-btn-bm": "",
3181
+ shouldShow: ({ editor: e, view }) => e.isActive("button") && !view.dom.classList.contains("dragging"),
3182
+ options: {
3183
+ placement,
3184
+ offset,
3185
+ onHide: () => {
3186
+ setIsEditing(false);
3187
+ onHide?.();
3188
+ }
3189
+ },
3190
+ className,
3191
+ children: /* @__PURE__ */ jsx(ButtonBubbleMenuContext.Provider, {
3192
+ value: {
3193
+ editor,
3194
+ isEditing,
3195
+ setIsEditing
3196
+ },
3197
+ children
3198
+ })
3199
+ });
3200
+ }
3201
+
3202
+ //#endregion
3203
+ //#region src/ui/button-bubble-menu/toolbar.tsx
3204
+ function ButtonBubbleMenuToolbar({ children, ...rest }) {
3205
+ const { isEditing } = useButtonBubbleMenuContext();
3206
+ if (isEditing) return null;
3207
+ return /* @__PURE__ */ jsx("div", {
3208
+ "data-re-btn-bm-toolbar": "",
3209
+ ...rest,
3210
+ children
3211
+ });
3212
+ }
3213
+
3214
+ //#endregion
3215
+ //#region src/ui/button-bubble-menu/default.tsx
3216
+ function ButtonBubbleMenuDefault({ excludeItems = [], placement, offset, onHide, className }) {
3217
+ return /* @__PURE__ */ jsx(ButtonBubbleMenuRoot, {
3218
+ placement,
3219
+ offset,
3220
+ onHide,
3221
+ className,
3222
+ children: !excludeItems.includes("edit-link") && /* @__PURE__ */ jsx(ButtonBubbleMenuToolbar, { children: /* @__PURE__ */ jsx(ButtonBubbleMenuEditLink, {}) })
3223
+ });
3224
+ }
3225
+
3226
+ //#endregion
3227
+ //#region src/ui/button-bubble-menu/index.ts
3228
+ const ButtonBubbleMenu = {
3229
+ Root: ButtonBubbleMenuRoot,
3230
+ Toolbar: ButtonBubbleMenuToolbar,
3231
+ EditLink: ButtonBubbleMenuEditLink,
3232
+ Default: ButtonBubbleMenuDefault
3233
+ };
3234
+
3235
+ //#endregion
3236
+ //#region src/ui/image-bubble-menu/context.tsx
3237
+ const ImageBubbleMenuContext = React.createContext(null);
3238
+ function useImageBubbleMenuContext() {
3239
+ const context = React.useContext(ImageBubbleMenuContext);
3240
+ if (!context) throw new Error("ImageBubbleMenu compound components must be used within <ImageBubbleMenu.Root>");
3241
+ return context;
3242
+ }
3243
+
3244
+ //#endregion
3245
+ //#region src/ui/image-bubble-menu/edit-link.tsx
3246
+ function ImageBubbleMenuEditLink({ className, children, onClick, onMouseDown, ...rest }) {
3247
+ const { setIsEditing } = useImageBubbleMenuContext();
3248
+ return /* @__PURE__ */ jsx("button", {
3249
+ ...rest,
3250
+ type: "button",
3251
+ "aria-label": "Edit link",
3252
+ "data-re-img-bm-item": "",
3253
+ "data-item": "edit-link",
3254
+ className,
3255
+ onMouseDown: (e) => {
3256
+ e.preventDefault();
3257
+ onMouseDown?.(e);
3258
+ },
3259
+ onClick: (e) => {
3260
+ onClick?.(e);
3261
+ setIsEditing(true);
3262
+ },
3263
+ children: children ?? /* @__PURE__ */ jsx(LinkIcon, {})
3264
+ });
3265
+ }
3266
+
3267
+ //#endregion
3268
+ //#region src/ui/image-bubble-menu/root.tsx
3269
+ function ImageBubbleMenuRoot({ onHide, placement = "top", offset = 8, className, children }) {
3270
+ const { editor } = useCurrentEditor();
3271
+ const [isEditing, setIsEditing] = React.useState(false);
3272
+ if (!editor) return null;
3273
+ return /* @__PURE__ */ jsx(BubbleMenu$1, {
3274
+ editor,
3275
+ "data-re-img-bm": "",
3276
+ shouldShow: ({ editor: e, view }) => e.isActive("image") && !view.dom.classList.contains("dragging"),
3277
+ options: {
3278
+ placement,
3279
+ offset,
3280
+ onHide: () => {
3281
+ setIsEditing(false);
3282
+ onHide?.();
3283
+ }
3284
+ },
3285
+ className,
3286
+ children: /* @__PURE__ */ jsx(ImageBubbleMenuContext.Provider, {
3287
+ value: {
3288
+ editor,
3289
+ isEditing,
3290
+ setIsEditing
3291
+ },
3292
+ children
3293
+ })
3294
+ });
3295
+ }
3296
+
3297
+ //#endregion
3298
+ //#region src/ui/image-bubble-menu/toolbar.tsx
3299
+ function ImageBubbleMenuToolbar({ children, ...rest }) {
3300
+ const { isEditing } = useImageBubbleMenuContext();
3301
+ if (isEditing) return null;
3302
+ return /* @__PURE__ */ jsx("div", {
3303
+ "data-re-img-bm-toolbar": "",
3304
+ ...rest,
3305
+ children
3306
+ });
3307
+ }
3308
+
3309
+ //#endregion
3310
+ //#region src/ui/image-bubble-menu/default.tsx
3311
+ function ImageBubbleMenuDefault({ excludeItems = [], placement, offset, onHide, className }) {
3312
+ return /* @__PURE__ */ jsx(ImageBubbleMenuRoot, {
3313
+ placement,
3314
+ offset,
3315
+ onHide,
3316
+ className,
3317
+ children: !excludeItems.includes("edit-link") && /* @__PURE__ */ jsx(ImageBubbleMenuToolbar, { children: /* @__PURE__ */ jsx(ImageBubbleMenuEditLink, {}) })
3318
+ });
3319
+ }
3320
+
3321
+ //#endregion
3322
+ //#region src/ui/image-bubble-menu/index.ts
3323
+ const ImageBubbleMenu = {
3324
+ Root: ImageBubbleMenuRoot,
3325
+ Toolbar: ImageBubbleMenuToolbar,
3326
+ EditLink: ImageBubbleMenuEditLink,
3327
+ Default: ImageBubbleMenuDefault
3328
+ };
3329
+
3330
+ //#endregion
3331
+ //#region src/ui/link-bubble-menu/context.tsx
3332
+ const LinkBubbleMenuContext = React.createContext(null);
3333
+ function useLinkBubbleMenuContext() {
3334
+ const context = React.useContext(LinkBubbleMenuContext);
3335
+ if (!context) throw new Error("LinkBubbleMenu compound components must be used within <LinkBubbleMenu.Root>");
3336
+ return context;
3337
+ }
3338
+
3339
+ //#endregion
3340
+ //#region src/ui/link-bubble-menu/edit-link.tsx
3341
+ function LinkBubbleMenuEditLink({ className, children, onClick, onMouseDown, ...rest }) {
3342
+ const { setIsEditing } = useLinkBubbleMenuContext();
3343
+ return /* @__PURE__ */ jsx("button", {
3344
+ type: "button",
3345
+ "aria-label": "Edit link",
3346
+ "data-re-link-bm-item": "",
3347
+ "data-item": "edit-link",
3348
+ className,
3349
+ onMouseDown: (e) => {
3350
+ e.preventDefault();
3351
+ onMouseDown?.(e);
3352
+ },
3353
+ onClick: (e) => {
3354
+ onClick?.(e);
3355
+ setIsEditing(true);
3356
+ },
3357
+ ...rest,
3358
+ children: children ?? /* @__PURE__ */ jsx(PencilIcon, {})
3359
+ });
3360
+ }
3361
+
3362
+ //#endregion
3363
+ //#region src/ui/link-bubble-menu/form.tsx
3364
+ function LinkBubbleMenuForm({ className, validateUrl, onLinkApply, onLinkRemove, children }) {
3365
+ const { editor, linkHref, isEditing, setIsEditing } = useLinkBubbleMenuContext();
3366
+ const inputRef = React.useRef(null);
3367
+ const formRef = React.useRef(null);
3368
+ const displayHref = linkHref === "#" ? "" : linkHref;
3369
+ const [inputValue, setInputValue] = React.useState(displayHref);
3370
+ React.useEffect(() => {
3371
+ if (!isEditing) return;
3372
+ const timeoutId = setTimeout(() => {
3373
+ inputRef.current?.focus();
3374
+ }, 0);
3375
+ return () => clearTimeout(timeoutId);
3376
+ }, [isEditing]);
3377
+ React.useEffect(() => {
3378
+ if (!isEditing) return;
3379
+ const handleKeyDown = (event) => {
3380
+ if (event.key === "Escape") setIsEditing(false);
3381
+ };
3382
+ const handleClickOutside = (event) => {
3383
+ if (formRef.current && !formRef.current.contains(event.target)) {
3384
+ const form = formRef.current;
3385
+ const submitEvent = new Event("submit", {
3386
+ bubbles: true,
3387
+ cancelable: true
3388
+ });
3389
+ form.dispatchEvent(submitEvent);
3390
+ setIsEditing(false);
3391
+ }
3392
+ };
3393
+ document.addEventListener("mousedown", handleClickOutside);
3394
+ window.addEventListener("keydown", handleKeyDown);
3395
+ return () => {
3396
+ window.removeEventListener("keydown", handleKeyDown);
3397
+ document.removeEventListener("mousedown", handleClickOutside);
3398
+ };
3399
+ }, [isEditing, setIsEditing]);
3400
+ if (!isEditing) return null;
3401
+ function handleSubmit(e) {
3402
+ e.preventDefault();
3403
+ const value = inputValue.trim();
3404
+ if (value === "") {
3405
+ setLinkHref(editor, "");
3406
+ setIsEditing(false);
3407
+ focusEditor(editor);
3408
+ onLinkRemove?.();
3409
+ return;
3410
+ }
3411
+ const finalValue = (validateUrl ?? getUrlFromString)(value);
3412
+ if (!finalValue) {
3413
+ setLinkHref(editor, "");
3414
+ setIsEditing(false);
3415
+ focusEditor(editor);
3416
+ onLinkRemove?.();
3417
+ return;
3418
+ }
3419
+ setLinkHref(editor, finalValue);
3420
+ setIsEditing(false);
3421
+ focusEditor(editor);
3422
+ onLinkApply?.(finalValue);
3423
+ }
3424
+ function handleUnlink(e) {
3425
+ e.stopPropagation();
3426
+ setLinkHref(editor, "");
3427
+ setIsEditing(false);
3428
+ focusEditor(editor);
3429
+ onLinkRemove?.();
3430
+ }
3431
+ return /* @__PURE__ */ jsxs("form", {
3432
+ ref: formRef,
3433
+ "data-re-link-bm-form": "",
3434
+ className,
3435
+ onMouseDown: (e) => e.stopPropagation(),
3436
+ onClick: (e) => e.stopPropagation(),
3437
+ onKeyDown: (e) => e.stopPropagation(),
3438
+ onSubmit: handleSubmit,
3439
+ children: [
3440
+ /* @__PURE__ */ jsx("input", {
3441
+ ref: inputRef,
3442
+ "data-re-link-bm-input": "",
3443
+ value: inputValue,
3444
+ onFocus: (e) => e.stopPropagation(),
3445
+ onChange: (e) => setInputValue(e.target.value),
3446
+ placeholder: "Paste a link",
3447
+ type: "text"
3448
+ }),
3449
+ children,
3450
+ displayHref ? /* @__PURE__ */ jsx("button", {
3451
+ type: "button",
3452
+ "aria-label": "Remove link",
3453
+ "data-re-link-bm-unlink": "",
3454
+ onClick: handleUnlink,
3455
+ children: /* @__PURE__ */ jsx(UnlinkIcon, {})
3456
+ }) : /* @__PURE__ */ jsx("button", {
3457
+ type: "submit",
3458
+ "aria-label": "Apply link",
3459
+ "data-re-link-bm-apply": "",
3460
+ onMouseDown: (e) => e.stopPropagation(),
3461
+ children: /* @__PURE__ */ jsx(Check, {})
3462
+ })
3463
+ ]
3464
+ });
3465
+ }
3466
+
3467
+ //#endregion
3468
+ //#region src/ui/link-bubble-menu/open-link.tsx
3469
+ function LinkBubbleMenuOpenLink({ className, children, ...rest }) {
3470
+ const { linkHref } = useLinkBubbleMenuContext();
3471
+ return /* @__PURE__ */ jsx("a", {
3472
+ ...rest,
3473
+ href: linkHref,
3474
+ target: "_blank",
3475
+ rel: "noopener noreferrer",
3476
+ "aria-label": "Open link",
3477
+ "data-re-link-bm-item": "",
3478
+ "data-item": "open-link",
3479
+ className,
3480
+ children: children ?? /* @__PURE__ */ jsx(ExternalLinkIcon, {})
3481
+ });
3482
+ }
3483
+
3484
+ //#endregion
3485
+ //#region src/ui/link-bubble-menu/root.tsx
3486
+ function LinkBubbleMenuRoot({ onHide, placement = "top", offset = 8, className, children }) {
3487
+ const { editor } = useCurrentEditor();
3488
+ const [isEditing, setIsEditing] = React.useState(false);
3489
+ const linkHref = useEditorState({
3490
+ editor,
3491
+ selector: ({ editor: e }) => e?.getAttributes("link").href ?? ""
3492
+ });
3493
+ if (!editor) return null;
3494
+ return /* @__PURE__ */ jsx(BubbleMenu$1, {
3495
+ editor,
3496
+ "data-re-link-bm": "",
3497
+ shouldShow: ({ editor: e }) => e.isActive("link") && e.view.state.selection.content().size === 0,
3498
+ options: {
3499
+ placement,
3500
+ offset,
3501
+ onHide: () => {
3502
+ setIsEditing(false);
3503
+ onHide?.();
3504
+ }
3505
+ },
3506
+ className,
3507
+ children: /* @__PURE__ */ jsx(LinkBubbleMenuContext.Provider, {
3508
+ value: {
3509
+ editor,
3510
+ linkHref: linkHref ?? "",
3511
+ isEditing,
3512
+ setIsEditing
3513
+ },
3514
+ children
3515
+ })
3516
+ });
3517
+ }
3518
+
3519
+ //#endregion
3520
+ //#region src/ui/link-bubble-menu/toolbar.tsx
3521
+ function LinkBubbleMenuToolbar({ children, ...rest }) {
3522
+ const { isEditing } = useLinkBubbleMenuContext();
3523
+ if (isEditing) return null;
3524
+ return /* @__PURE__ */ jsx("div", {
3525
+ "data-re-link-bm-toolbar": "",
3526
+ ...rest,
3527
+ children
3528
+ });
3529
+ }
3530
+
3531
+ //#endregion
3532
+ //#region src/ui/link-bubble-menu/unlink.tsx
3533
+ function LinkBubbleMenuUnlink({ className, children, onClick, onMouseDown, ...rest }) {
3534
+ const { editor } = useLinkBubbleMenuContext();
3535
+ return /* @__PURE__ */ jsx("button", {
3536
+ type: "button",
3537
+ "aria-label": "Remove link",
3538
+ "data-re-link-bm-item": "",
3539
+ "data-item": "unlink",
3540
+ className,
3541
+ onMouseDown: (e) => {
3542
+ e.preventDefault();
3543
+ onMouseDown?.(e);
3544
+ },
3545
+ onClick: (e) => {
3546
+ onClick?.(e);
3547
+ editor.chain().focus().unsetLink().run();
3548
+ },
3549
+ ...rest,
3550
+ children: children ?? /* @__PURE__ */ jsx(UnlinkIcon, {})
3551
+ });
3552
+ }
3553
+
3554
+ //#endregion
3555
+ //#region src/ui/link-bubble-menu/default.tsx
3556
+ function LinkBubbleMenuDefault({ excludeItems = [], placement, offset, onHide, className, validateUrl, onLinkApply, onLinkRemove }) {
3557
+ const has = (item) => !excludeItems.includes(item);
3558
+ return /* @__PURE__ */ jsxs(LinkBubbleMenuRoot, {
3559
+ placement,
3560
+ offset,
3561
+ onHide,
3562
+ className,
3563
+ children: [(has("edit-link") || has("open-link") || has("unlink")) && /* @__PURE__ */ jsxs(LinkBubbleMenuToolbar, { children: [
3564
+ has("edit-link") && /* @__PURE__ */ jsx(LinkBubbleMenuEditLink, {}),
3565
+ has("open-link") && /* @__PURE__ */ jsx(LinkBubbleMenuOpenLink, {}),
3566
+ has("unlink") && /* @__PURE__ */ jsx(LinkBubbleMenuUnlink, {})
3567
+ ] }), /* @__PURE__ */ jsx(LinkBubbleMenuForm, {
3568
+ validateUrl,
3569
+ onLinkApply,
3570
+ onLinkRemove
3571
+ })]
3572
+ });
3573
+ }
3574
+
3575
+ //#endregion
3576
+ //#region src/ui/link-bubble-menu/index.ts
3577
+ const LinkBubbleMenu = {
3578
+ Root: LinkBubbleMenuRoot,
3579
+ Toolbar: LinkBubbleMenuToolbar,
3580
+ Form: LinkBubbleMenuForm,
3581
+ EditLink: LinkBubbleMenuEditLink,
3582
+ Unlink: LinkBubbleMenuUnlink,
3583
+ OpenLink: LinkBubbleMenuOpenLink,
3584
+ Default: LinkBubbleMenuDefault
3585
+ };
3586
+
3587
+ //#endregion
3588
+ //#region src/ui/slash-command/utils.ts
3589
+ function isInsideNode(editor, type) {
3590
+ const { $from } = editor.state.selection;
3591
+ for (let d = $from.depth; d > 0; d--) if ($from.node(d).type.name === type) return true;
3592
+ return false;
3593
+ }
3594
+ function isAtMaxColumnsDepth(editor) {
3595
+ const { from } = editor.state.selection;
3596
+ return getColumnsDepth(editor.state.doc, from) >= MAX_COLUMNS_DEPTH;
3597
+ }
3598
+ function updateScrollView(container, item) {
3599
+ const containerRect = container.getBoundingClientRect();
3600
+ const itemRect = item.getBoundingClientRect();
3601
+ if (itemRect.top < containerRect.top) container.scrollTop -= containerRect.top - itemRect.top;
3602
+ else if (itemRect.bottom > containerRect.bottom) container.scrollTop += itemRect.bottom - containerRect.bottom;
3603
+ }
3604
+
3605
+ //#endregion
3606
+ //#region src/ui/slash-command/command-list.tsx
3607
+ const CATEGORY_ORDER = [
3608
+ "Text",
3609
+ "Media",
3610
+ "Layout",
3611
+ "Utility"
3612
+ ];
3613
+ function groupByCategory(items) {
3614
+ const seen = /* @__PURE__ */ new Map();
3615
+ for (const item of items) {
3616
+ const existing = seen.get(item.category);
3617
+ if (existing) existing.push(item);
3618
+ else seen.set(item.category, [item]);
3619
+ }
3620
+ const ordered = [];
3621
+ for (const cat of CATEGORY_ORDER) {
3622
+ const group = seen.get(cat);
3623
+ if (group) {
3624
+ ordered.push({
3625
+ category: cat,
3626
+ items: group
3627
+ });
3628
+ seen.delete(cat);
3629
+ }
3630
+ }
3631
+ for (const [category, group] of seen) ordered.push({
3632
+ category,
3633
+ items: group
3634
+ });
3635
+ return ordered;
3636
+ }
3637
+ function CommandItem({ item, selected, onSelect }) {
3638
+ return /* @__PURE__ */ jsxs("button", {
3639
+ "data-re-slash-command-item": "",
3640
+ "data-selected": selected || void 0,
3641
+ onClick: onSelect,
3642
+ type: "button",
3643
+ children: [item.icon, /* @__PURE__ */ jsx("span", { children: item.title })]
3644
+ });
3645
+ }
3646
+ function CommandList({ items, command, query, ref }) {
3647
+ const [selectedIndex, setSelectedIndex] = useState(0);
3648
+ const containerRef = useRef(null);
3649
+ useEffect(() => {
3650
+ setSelectedIndex(0);
3651
+ }, [items]);
3652
+ useLayoutEffect(() => {
3653
+ const container = containerRef.current;
3654
+ if (!container) return;
3655
+ const selected = container.querySelector("[data-selected]");
3656
+ if (selected) updateScrollView(container, selected);
3657
+ }, [selectedIndex]);
3658
+ const selectItem = useCallback((index) => {
3659
+ const item = items[index];
3660
+ if (item) command(item);
3661
+ }, [items, command]);
3662
+ useImperativeHandle(ref, () => ({ onKeyDown: ({ event }) => {
3663
+ if (items.length === 0) return false;
3664
+ if (event.key === "ArrowUp") {
3665
+ setSelectedIndex((i) => (i + items.length - 1) % items.length);
3666
+ return true;
3667
+ }
3668
+ if (event.key === "ArrowDown") {
3669
+ setSelectedIndex((i) => (i + 1) % items.length);
3670
+ return true;
3671
+ }
3672
+ if (event.key === "Enter") {
3673
+ selectItem(selectedIndex);
3674
+ return true;
3675
+ }
3676
+ return false;
3677
+ } }), [
3678
+ items.length,
3679
+ selectItem,
3680
+ selectedIndex
3681
+ ]);
3682
+ if (items.length === 0) return /* @__PURE__ */ jsx("div", {
3683
+ "data-re-slash-command": "",
3684
+ children: /* @__PURE__ */ jsx("div", {
3685
+ "data-re-slash-command-empty": "",
3686
+ children: "No results"
3687
+ })
3688
+ });
3689
+ if (query.trim().length > 0) return /* @__PURE__ */ jsx("div", {
3690
+ "data-re-slash-command": "",
3691
+ ref: containerRef,
3692
+ children: items.map((item, index) => /* @__PURE__ */ jsx(CommandItem, {
3693
+ item,
3694
+ onSelect: () => selectItem(index),
3695
+ selected: index === selectedIndex
3696
+ }, item.title))
3697
+ });
3698
+ const groups = groupByCategory(items);
3699
+ let flatIndex = 0;
3700
+ return /* @__PURE__ */ jsx("div", {
3701
+ "data-re-slash-command": "",
3702
+ ref: containerRef,
3703
+ children: groups.map((group) => /* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx("div", {
3704
+ "data-re-slash-command-category": "",
3705
+ children: group.category
3706
+ }), group.items.map((item) => {
3707
+ const currentIndex = flatIndex++;
3708
+ return /* @__PURE__ */ jsx(CommandItem, {
3709
+ item,
3710
+ onSelect: () => selectItem(currentIndex),
3711
+ selected: currentIndex === selectedIndex
3712
+ }, item.title);
3713
+ })] }, group.category))
3714
+ });
3715
+ }
3716
+
3717
+ //#endregion
3718
+ //#region src/ui/slash-command/commands.tsx
3719
+ const TEXT = {
3720
+ title: "Text",
3721
+ description: "Plain text block",
3722
+ icon: /* @__PURE__ */ jsx(Text, { size: 20 }),
3723
+ category: "Text",
3724
+ searchTerms: ["p", "paragraph"],
3725
+ command: ({ editor, range }) => {
3726
+ editor.chain().focus().deleteRange(range).toggleNode("paragraph", "paragraph").run();
3727
+ }
3728
+ };
3729
+ const H1 = {
3730
+ title: "Title",
3731
+ description: "Large heading",
3732
+ icon: /* @__PURE__ */ jsx(Heading1, { size: 20 }),
3733
+ category: "Text",
3734
+ searchTerms: [
3735
+ "title",
3736
+ "big",
3737
+ "large",
3738
+ "h1"
3739
+ ],
3740
+ command: ({ editor, range }) => {
3741
+ editor.chain().focus().deleteRange(range).setNode("heading", { level: 1 }).run();
3742
+ }
3743
+ };
3744
+ const H2 = {
3745
+ title: "Subtitle",
3746
+ description: "Medium heading",
3747
+ icon: /* @__PURE__ */ jsx(Heading2, { size: 20 }),
3748
+ category: "Text",
3749
+ searchTerms: [
3750
+ "subtitle",
3751
+ "medium",
3752
+ "h2"
3753
+ ],
3754
+ command: ({ editor, range }) => {
3755
+ editor.chain().focus().deleteRange(range).setNode("heading", { level: 2 }).run();
3756
+ }
3757
+ };
3758
+ const H3 = {
3759
+ title: "Heading",
3760
+ description: "Small heading",
3761
+ icon: /* @__PURE__ */ jsx(Heading3, { size: 20 }),
3762
+ category: "Text",
3763
+ searchTerms: [
3764
+ "subtitle",
3765
+ "small",
3766
+ "h3"
3767
+ ],
3768
+ command: ({ editor, range }) => {
3769
+ editor.chain().focus().deleteRange(range).setNode("heading", { level: 3 }).run();
3770
+ }
3771
+ };
3772
+ const BULLET_LIST = {
3773
+ title: "Bullet list",
3774
+ description: "Unordered list",
3775
+ icon: /* @__PURE__ */ jsx(List, { size: 20 }),
3776
+ category: "Text",
3777
+ searchTerms: ["unordered", "point"],
3778
+ command: ({ editor, range }) => {
3779
+ editor.chain().focus().deleteRange(range).toggleBulletList().run();
3780
+ }
3781
+ };
3782
+ const NUMBERED_LIST = {
3783
+ title: "Numbered list",
3784
+ description: "Ordered list",
3785
+ icon: /* @__PURE__ */ jsx(ListOrdered, { size: 20 }),
3786
+ category: "Text",
3787
+ searchTerms: ["ordered"],
3788
+ command: ({ editor, range }) => {
3789
+ editor.chain().focus().deleteRange(range).toggleOrderedList().run();
3790
+ }
3791
+ };
3792
+ const QUOTE = {
3793
+ title: "Quote",
3794
+ description: "Block quote",
3795
+ icon: /* @__PURE__ */ jsx(TextQuote, { size: 20 }),
3796
+ category: "Text",
3797
+ searchTerms: ["blockquote"],
3798
+ command: ({ editor, range }) => {
3799
+ editor.chain().focus().deleteRange(range).toggleNode("paragraph", "paragraph").toggleBlockquote().run();
3800
+ }
3801
+ };
3802
+ const CODE = {
3803
+ title: "Code block",
3804
+ description: "Code snippet",
3805
+ icon: /* @__PURE__ */ jsx(SquareCode, { size: 20 }),
3806
+ category: "Text",
3807
+ searchTerms: ["codeblock"],
3808
+ command: ({ editor, range }) => {
3809
+ editor.chain().focus().deleteRange(range).toggleCodeBlock().run();
3810
+ }
3811
+ };
3812
+ const BUTTON = {
3813
+ title: "Button",
3814
+ description: "Clickable button",
3815
+ icon: /* @__PURE__ */ jsx(MousePointer, { size: 20 }),
3816
+ category: "Layout",
3817
+ searchTerms: ["button"],
3818
+ command: ({ editor, range }) => {
3819
+ editor.chain().focus().deleteRange(range).setButton().run();
3820
+ }
3821
+ };
3822
+ const DIVIDER = {
3823
+ title: "Divider",
3824
+ description: "Horizontal separator",
3825
+ icon: /* @__PURE__ */ jsx(SplitSquareVertical, { size: 20 }),
3826
+ category: "Layout",
3827
+ searchTerms: [
3828
+ "hr",
3829
+ "divider",
3830
+ "separator"
3831
+ ],
3832
+ command: ({ editor, range }) => {
3833
+ editor.chain().focus().deleteRange(range).setHorizontalRule().run();
3834
+ }
3835
+ };
3836
+ const SECTION = {
3837
+ title: "Section",
3838
+ description: "Content section",
3839
+ icon: /* @__PURE__ */ jsx(Rows2, { size: 20 }),
3840
+ category: "Layout",
3841
+ searchTerms: [
3842
+ "section",
3843
+ "row",
3844
+ "container"
3845
+ ],
3846
+ command: ({ editor, range }) => {
3847
+ editor.chain().focus().deleteRange(range).insertSection().run();
3848
+ }
3849
+ };
3850
+ const TWO_COLUMNS = {
3851
+ title: "2 columns",
3852
+ description: "Two column layout",
3853
+ icon: /* @__PURE__ */ jsx(Columns2, { size: 20 }),
3854
+ category: "Layout",
3855
+ searchTerms: [
3856
+ "columns",
3857
+ "column",
3858
+ "layout",
3859
+ "grid",
3860
+ "split",
3861
+ "side-by-side",
3862
+ "multi-column",
3863
+ "row",
3864
+ "two",
3865
+ "2"
3866
+ ],
3867
+ command: ({ editor, range }) => {
3868
+ editor.chain().focus().deleteRange(range).insertColumns(2).run();
3869
+ }
3870
+ };
3871
+ const THREE_COLUMNS = {
3872
+ title: "3 columns",
3873
+ description: "Three column layout",
3874
+ icon: /* @__PURE__ */ jsx(Columns3, { size: 20 }),
3875
+ category: "Layout",
3876
+ searchTerms: [
3877
+ "columns",
3878
+ "column",
3879
+ "layout",
3880
+ "grid",
3881
+ "split",
3882
+ "multi-column",
3883
+ "row",
3884
+ "three",
3885
+ "3"
3886
+ ],
3887
+ command: ({ editor, range }) => {
3888
+ editor.chain().focus().deleteRange(range).insertColumns(3).run();
3889
+ }
3890
+ };
3891
+ const FOUR_COLUMNS = {
3892
+ title: "4 columns",
3893
+ description: "Four column layout",
3894
+ icon: /* @__PURE__ */ jsx(Columns4, { size: 20 }),
3895
+ category: "Layout",
3896
+ searchTerms: [
3897
+ "columns",
3898
+ "column",
3899
+ "layout",
3900
+ "grid",
3901
+ "split",
3902
+ "multi-column",
3903
+ "row",
3904
+ "four",
3905
+ "4"
3906
+ ],
3907
+ command: ({ editor, range }) => {
3908
+ editor.chain().focus().deleteRange(range).insertColumns(4).run();
3909
+ }
3910
+ };
3911
+ const defaultSlashCommands = [
3912
+ TEXT,
3913
+ H1,
3914
+ H2,
3915
+ H3,
3916
+ BULLET_LIST,
3917
+ NUMBERED_LIST,
3918
+ QUOTE,
3919
+ CODE,
3920
+ BUTTON,
3921
+ DIVIDER,
3922
+ SECTION,
3923
+ TWO_COLUMNS,
3924
+ THREE_COLUMNS,
3925
+ FOUR_COLUMNS
3926
+ ];
3927
+
3928
+ //#endregion
3929
+ //#region src/ui/slash-command/extension.ts
3930
+ const SlashCommandExtension = Extension.create({
3931
+ name: "slash-command",
3932
+ addOptions() {
3933
+ return { suggestion: {
3934
+ char: "/",
3935
+ allow: ({ editor }) => !editor.isActive("codeBlock"),
3936
+ command: ({ editor, range, props }) => {
3937
+ props.command({
3938
+ editor,
3939
+ range
3940
+ });
3941
+ }
3942
+ } };
3943
+ },
3944
+ addProseMirrorPlugins() {
3945
+ return [Suggestion({
3946
+ pluginKey: new PluginKey("slash-command"),
3947
+ editor: this.editor,
3948
+ ...this.options.suggestion
3949
+ })];
3950
+ }
3951
+ });
3952
+
3953
+ //#endregion
3954
+ //#region src/ui/slash-command/render.tsx
3955
+ function createRenderItems(component = CommandList) {
3956
+ return () => {
3957
+ let renderer = null;
3958
+ let popup = null;
3959
+ return {
3960
+ onStart: (props) => {
3961
+ renderer = new ReactRenderer(component, {
3962
+ props,
3963
+ editor: props.editor
3964
+ });
3965
+ if (!props.clientRect) return;
3966
+ popup = tippy("body", {
3967
+ getReferenceClientRect: props.clientRect,
3968
+ appendTo: () => document.body,
3969
+ content: renderer.element,
3970
+ showOnCreate: true,
3971
+ interactive: true,
3972
+ trigger: "manual",
3973
+ placement: "bottom-start"
3974
+ });
3975
+ },
3976
+ onUpdate: (props) => {
3977
+ if (!renderer) return;
3978
+ renderer.updateProps(props);
3979
+ if (popup?.[0] && props.clientRect) popup[0].setProps({ getReferenceClientRect: props.clientRect });
3980
+ },
3981
+ onKeyDown: (props) => {
3982
+ if (props.event.key === "Escape") {
3983
+ popup?.[0]?.hide();
3984
+ return true;
3985
+ }
3986
+ return renderer?.ref?.onKeyDown(props) ?? false;
3987
+ },
3988
+ onExit: () => {
3989
+ popup?.[0]?.destroy();
3990
+ renderer?.destroy();
3991
+ popup = null;
3992
+ renderer = null;
3993
+ }
3994
+ };
3995
+ };
3996
+ }
3997
+
3998
+ //#endregion
3999
+ //#region src/ui/slash-command/search.ts
4000
+ function scoreItem(item, query) {
4001
+ if (!query) return 100;
4002
+ const q = query.toLowerCase();
4003
+ const title = item.title.toLowerCase();
4004
+ const description = item.description.toLowerCase();
4005
+ const terms = item.searchTerms?.map((t) => t.toLowerCase()) ?? [];
4006
+ if (title === q) return 100;
4007
+ if (title.startsWith(q)) return 90;
4008
+ if (title.split(/\s+/).some((w) => w.startsWith(q))) return 80;
4009
+ if (terms.some((t) => t === q)) return 70;
4010
+ if (terms.some((t) => t.startsWith(q))) return 60;
4011
+ if (title.includes(q)) return 40;
4012
+ if (terms.some((t) => t.includes(q))) return 30;
4013
+ if (description.includes(q)) return 20;
4014
+ return 0;
4015
+ }
4016
+ function filterAndRankItems(items, query) {
4017
+ const trimmed = query.trim();
4018
+ if (!trimmed) return items;
4019
+ const scored = items.map((item) => ({
4020
+ item,
4021
+ score: scoreItem(item, trimmed)
4022
+ })).filter(({ score }) => score > 0);
4023
+ scored.sort((a, b) => b.score - a.score);
4024
+ return scored.map(({ item }) => item);
4025
+ }
4026
+
4027
+ //#endregion
4028
+ //#region src/ui/slash-command/create-slash-command.ts
4029
+ function defaultFilterItems(items, query, editor) {
4030
+ return filterAndRankItems(isAtMaxColumnsDepth(editor) ? items.filter((item) => item.category !== "Layout" || !item.title.includes("column")) : items, query);
4031
+ }
4032
+ function createSlashCommand(options) {
4033
+ const items = options?.items ?? defaultSlashCommands;
4034
+ const filterFn = options?.filterItems ?? defaultFilterItems;
4035
+ return SlashCommandExtension.configure({ suggestion: {
4036
+ items: ({ query, editor }) => filterFn(items, query, editor),
4037
+ render: createRenderItems(options?.component)
4038
+ } });
4039
+ }
4040
+
4041
+ //#endregion
4042
+ //#region src/ui/slash-command/index.ts
4043
+ const SlashCommand = createSlashCommand();
4044
+
4045
+ //#endregion
4046
+ 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, Divider, EmailNode, FOUR_COLUMNS, FourColumns, GlobalContent, H1, H2, H3, HardBreak, Heading, ImageBubbleMenu, ImageBubbleMenuDefault, ImageBubbleMenuEditLink, ImageBubbleMenuRoot, ImageBubbleMenuToolbar, Italic, Link, 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, StarterKit, Strike, StyleAttribute, Sup, TEXT, THREE_COLUMNS, TWO_COLUMNS, Table, TableCell, TableHeader, TableRow, ThreeColumns, TwoColumns, Underline, Uppercase, composeReactEmail, createSlashCommand, defaultSlashCommands, editorEventBus, filterAndRankItems, getColumnsDepth, getGlobalContent, isAtMaxColumnsDepth, isDocumentVisuallyEmpty, isInsideNode, processStylesForUnlink, scoreItem, setTextAlignment, useButtonBubbleMenuContext, useEditor, useImageBubbleMenuContext, useLinkBubbleMenuContext };
1350
4047
  //# sourceMappingURL=index.mjs.map