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

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